前言
最近一直在為找工作煩惱央渣,剛好遇到一家公司要求我先做幾道反爬蟲的題,看了之后覺得自己還挺菜的碳柱,不過也過了幾關(guān)围橡,剛好遇到一個之前沒遇到過的反爬蟲手段 — 字體反爬
正文
一、站點(diǎn)分析
題目要求:這里有一個網(wǎng)站榄融,分了1000頁参淫,求所有數(shù)字的和。注意愧杯,是人看到的數(shù)字涎才,不是網(wǎng)頁源碼中的數(shù)字哦~
就這,從圖里能看出數(shù)字的字體有些不同力九,看看源碼是什么樣的
可以看到耍铜,源碼里的內(nèi)容和網(wǎng)頁上顯示的內(nèi)容根本不一樣,當(dāng)然跌前,題目也說了棕兼;那么這是怎么回事呢,切換到 Network 欄抵乓,刷新網(wǎng)頁看看請求
可以看到伴挚,這里有兩個字體請求,選擇后可以預(yù)覽字體
很明顯灾炭,數(shù)字有點(diǎn)問題茎芋,被改過了,上面那一個請求的字體文件是正常的字體(下圖)蜈出,可以拿來做比較田弥,以便于我們分析
一般來說字體文件的數(shù)字就是這樣的順序 1 2 3 4 5 6 7 8 9 0 ,以這個為模板铡原,被修改后的字體中的數(shù)字 2 處與 正常字體 中 9 的位置皱蹦∶荷保回到網(wǎng)頁源碼和內(nèi)容,網(wǎng)頁上顯示 274 沪哺,實(shí)際源碼中是 920(下圖)沈自,用上面的字體做替換我們會發(fā)現(xiàn),2 在被 修改過的字體 中的位置是 8 辜妓,而 8 在 正常字體 中就是 8枯途,由此可得結(jié)論:我們只要把這 修改過的字體 搞到手,然后把網(wǎng)頁上顯示的內(nèi)容逐個拆分為單個數(shù)字籍滴,然后從字體中匹配出正常字體就行了酪夷,不過,根據(jù)題目孽惰,我們需要反著來做晚岭,也就是從源碼入手,獲取到內(nèi)容后拆分為單個字體勋功,接著從字體中獲取網(wǎng)頁上顯示的內(nèi)容坦报。
我自己寫的時候都覺得頭暈,直接寫代碼狂鞋,這樣能更好的表達(dá)我要說什么片择,不過,這里要說一點(diǎn)骚揍,據(jù)我分析字管,這個網(wǎng)頁有1000頁,每一頁的字體都是不同的信不,就需要每獲取一個網(wǎng)頁就得重新獲取被修改的字體嘲叔。我這里用的是 scrapy 框架。
二抽活、代碼階段
首先新建一個scrapy項(xiàng)目
? ~ scrapy startproject glidedsky
New Scrapy project 'glidedsky', using template directory '/usr/local/lib/python3.7/site-packages/scrapy/templates/project', created in:
/Users/zhonglizhen/glidedsky
You can start your first spider with:
cd glidedsky
scrapy genspider example example.com
? ~
接著創(chuàng)建一個Spider
? ~ cd glidedsky
? ~ glidedsky scrapy genspider glidedsky glidesky.com
Cannot create a spider with the same name as your project
? ~ glidedsky
scrapy 怎么用我就不說了借跪,直接看代碼
# glidedsky.py
import scrapy
import requests
import re
from glidedsky.items import GlidedskyItem
from glidedsky.spiders.config import *
class GlidedskySpider(scrapy.Spider):
name = 'glidedsky'
start_urls = ['http://glidedsky.com/level/web/crawler-font-puzzle-1']
def __int__(self):
self.headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36',
}
def request(self, url, callback):
request = scrapy.Request(url=url, callback=callback)
# 添加 cookies
request.cookies['XSRF-TOKEN'] = XSRF_TOKEN
request.cookies['glidedsky_session'] = glidedsky_session
# 添加 headers
request.headers['User-Agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36'
return request
def start_requests(self):
for i, url in enumerate(self.start_urls):
yield self.request(url, self.parse_item)
def parse_item(self, response):
"""
解析numbers
:param response:
:return:
"""
body = response.css('html').get()
self.save_font(body)
col_md_nums = response.css('.col-md-1::text').extract()
items = GlidedskyItem()
for col_md_num in col_md_nums:
# 這里獲取到的是源碼中的內(nèi)容,并不是我們在網(wǎng)頁上看到的內(nèi)容酌壕,需要去數(shù)據(jù)管道進(jìn)一步處理
items['numbers'] = col_md_num.replace('\n', '').replace(' ', '')
yield items
# 獲取下一頁
next = response.xpath('//li/a[@rel="next"]')
# 判斷是否有下一頁
if len(next) > 0:
next_page = next[0].attrib['href']
# response.urljoin 可以幫我們構(gòu)造下一頁的鏈接
url = response.urljoin(next_page)
yield self.request(url=url, callback=self.parse_item)
def save_font(self, body):
"""
保存字體到本地
:param response: 網(wǎng)頁源代碼
:return:
"""
pattern = r'src:.url\("(.*?)"\).format\("woff"\)'
woff_font_url = re.findall(pattern, body, re.S)
print(woff_font_url)
resp = requests.get(woff_font_url[0], headers={'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36'})
with open(WOFF_FONT_FILENAME, 'wb') as f:
f.write(resp.content)
在解析字體之前先分析一下字體文件的內(nèi)容,因?yàn)檫@里面有坑(起碼我這個站點(diǎn)是這樣)歇由,下載好字體后卵牍,用python的 fontTools 庫把 woff格式 轉(zhuǎn)成 xml文件,然后打開沦泌;或者用 font-creator 直接打開糊昙,但是這個工具只有windows上有,所以這里就用第一種方法谢谦。
1释牺、先把 woff格式 轉(zhuǎn)成 xml格式 文件
import requests
from fontTools.ttLib import TTFont
# 先把字體文件下載下來
url = "https://guyujiezi.com/fonts/LQ1K9/1A7s3D.woff"
filename = url.split('/')[-1]
resp = requests.get(url)
with open(filename, 'wb') as f:
f.write(resp.content)
# 接著用 TTFont 打開文件
font = TTFont(filename)
# TTFont 中有一個 saveXML 的方法
font.saveXML(filename.replace(filename.split('.')[-1], 'xml'))
2萝衩、用文本編輯器打開
只需要看 GlyphOrder 項(xiàng)就行了,其實(shí)直接看 GlyphOrder 一個屁都看不出來没咙,完全和之前做的分析不一樣猩谊,不過仔細(xì)觀察后發(fā)現(xiàn)這里面也被人做了手腳,1703589624 這跟電話號碼一樣的就是上面看到的 修改后的字體 預(yù)覽到的祭刚,可能這樣還是看不出什么牌捷;其中 id 屬性的值為 修改后的字體 中的數(shù)字,name 屬性為 正常字體涡驮,但是根本不對暗甥,之前算過,網(wǎng)頁中的 274捉捅,正常內(nèi)容是 920撤防,而下面,2 明顯對應(yīng)著 zero 棒口,其實(shí)我在這里被坑了寄月,如果把 2+1=3 ,3 不就是對應(yīng)著 nine 了嗎陌凳,然后發(fā)現(xiàn)后面 74 也是對應(yīng)著 20剥懒,有 12 項(xiàng) GlyphID 的目的就是坑我們的(我猜的),不過這確實(shí)挺坑的合敦。分析過后可以開始寫代碼了
3初橘、代碼如下,這是 pipelines.py 文件
# pipelines.py
# -*- coding: utf-8 -*-
# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://doc.scrapy.org/en/latest/topics/item-pipeline.html
from scrapy.exceptions import DropItem
from fontTools.ttLib import TTFont
from glidedsky.spiders.config import *
class GlidedskyPipeline(object):
result = 0
def process_item(self, item, spider):
if item['numbers']:
numbers = item['numbers']
#print("@@@@@ 假數(shù)字: %s \n" % numbers)
font = TTFont(WOFF_FONT_FILENAME) # 首先創(chuàng)建一個TTFont對象充岛,參數(shù)為字體文件的路徑
true_number = ""
for num in range(len(numbers)):
fn = NUMBER_TEMP[numbers[num]] # 從模版中獲取數(shù)字對應(yīng)著的英語單詞
glyph_id = int(font.getGlyphID(fn)) - 1 # font.getGlyphID 方法是根據(jù)GlyphID name屬性獲取id屬性的值保檐,參數(shù)傳入name值,最后減一
true_number += str(glyph_id)
self.result += int(true_number)
print("@@@@@ 計(jì)算結(jié)果: %d" % self.result)
else:
return DropItem('Missing Number.')
config.py
DATA_PATH = '/Volumes/HDD500G/Documents/Python/Scrapy/glidedsky/glidedsky/data' # 這是我為了存儲字體文件新建的文件夾
WOFF_FONT_FILENAME = DATA_PATH + '/woff-font.woff'
XSRF_TOKEN = ''
glidedsky_session = ''
NUMBER_TEMP = {'1': 'one', '2': 'two', '3': 'three', '4': 'four', '5': 'five', '6': 'six', '7': 'seven', '8': 'eight', '9': 'nine', '0': 'zero'} # 這個模版是為了方便我計(jì)算崔梗,題目需要
items.py
# -*- coding: utf-8 -*-
# Define here the models for your scraped items
#
# See documentation in:
# https://doc.scrapy.org/en/latest/topics/items.html
import scrapy
class GlidedskyItem(scrapy.Item):
# define the fields for your item here like:
numbers = scrapy.Field()
settings.py夜只,設(shè)置我就不全部貼了,只貼需要改的部分
# 這本來是注釋掉了的
ITEM_PIPELINES = {
'glidedsky.pipelines.GlidedskyPipeline': 300,
}
接著直接運(yùn)行即可
? cd /你項(xiàng)目存儲地址/glidedsky/
? scrapy startpoject glidedsky
輸出結(jié)果就不展示了蒜魄,賊雞兒多
結(jié)論
這種反爬蟲手段是我第一次遇到扔亥,以前遇到的也就驗(yàn)證碼和ip限制,不過也算是漲了知識谈为,最后結(jié)果是我解決了