前言
最近一直在為找工作煩惱,剛好遇到一家公司要求我先做幾道反爬蟲的題,看了之後覺得自己還挺菜的,不過也過了幾關,剛好遇到一個之前沒遇到過的反爬蟲手段 — 字型反爬
正文
一、站點分析
題目要求:這裡有一個網站,分了1000頁,求所有數字的和。注意,是人看到的數字,不是網頁原始碼中的數字哦~
就這,從圖裡能看出數字的字型有些不同,看看原始碼是什麼樣的
可以看到,原始碼裡的內容和網頁上顯示的內容根本不一樣,當然,題目也說了;那麼這是怎麼回事呢,切換到 Network 欄,重新整理網頁看看請求
可以看到,這裡有兩個字型請求,選擇後可以預覽字型
很明顯,數字有點問題,被改過了,上面那一個請求的字型檔案是正常的字型(下圖),可以拿來做比較,以便於我們分析
一般來說字型檔案的數字就是這樣的順序 1 2 3 4 5 6 7 8 9 0 ,以這個為模板,被修改後的字型中的數字 2 處與 正常字型 中 9 的位置。回到網頁原始碼和內容,網頁上顯示 274 ,實際原始碼中是 920(下圖),用上面的字型做替換我們會發現,2 在被 修改過的字型 中的位置是 8 ,而 8 在 正常字型 中就是 8,由此可得結論:我們只要把這 修改過的字型 搞到手,然後把網頁上顯示的內容逐個拆分為單個數字,然後從字型中匹配出正常字型就行了,不過,根據題目,我們需要反著來做,也就是從原始碼入手,獲取到內容後拆分為單個字型,接著從字型中獲取網頁上顯示的內容。
我自己寫的時候都覺得頭暈,直接寫程式碼,這樣能更好的表達我要說什麼,不過,這裡要說一點,據我分析,這個網頁有1000頁,每一頁的字型都是不同的,就需要每獲取一個網頁就得重新獲取被修改的字型。我這裡用的是 scrapy 框架。
二、程式碼階段
首先新建一個scrapy專案
➜ ~ 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
➜ ~
複製程式碼
接著建立一個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:
# 這裡獲取到的是原始碼中的內容,並不是我們在網頁上看到的內容,需要去資料管道進一步處理
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 可以幫我們構造下一頁的連結
url = response.urljoin(next_page)
yield self.request(url=url, callback=self.parse_item)
def save_font(self, body):
"""
儲存字型到本地
:param response: 網頁原始碼
: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)
複製程式碼
在解析字型之前先分析一下字型檔案的內容,因為這裡面有坑(起碼我這個站點是這樣),下載好字型後,用python的 fontTools 庫把 woff格式 轉成 xml檔案,然後開啟;或者用 font-creator 直接開啟,但是這個工具只有windows上有,所以這裡就用第一種方法。
1、先把 woff格式 轉成 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 項就行了,其實直接看 GlyphOrder 一個屁都看不出來,完全和之前做的分析不一樣,不過仔細觀察後發現這裡面也被人做了手腳,1703589624 這跟電話號碼一樣的就是上面看到的 修改後的字型 預覽到的,可能這樣還是看不出什麼;其中 id 屬性的值為 修改後的字型 中的數字,name 屬性為 正常字型,但是根本不對,之前算過,網頁中的 274,正常內容是 920,而下面,2 明顯對應著 zero ,其實我在這裡被坑了,如果把 2+1=3 ,3 不就是對應著 nine 了嗎,然後發現後面 74 也是對應著 20,有 12 項 GlyphID 的目的就是坑我們的(我猜的),不過這確實挺坑的。分析過後可以開始寫程式碼了
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("@@@@@ 假數字: %s \n" % numbers)
font = TTFont(WOFF_FONT_FILENAME) # 首先建立一個TTFont物件,引數為字型檔案的路徑
true_number = ""
for num in range(len(numbers)):
fn = NUMBER_TEMP[numbers[num]] # 從模版中獲取數字對應著的英語單詞
glyph_id = int(font.getGlyphID(fn)) - 1 # font.getGlyphID 方法是根據GlyphID name屬性獲取id屬性的值,引數傳入name值,最後減一
true_number += str(glyph_id)
self.result += int(true_number)
print("@@@@@ 計算結果: %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'} # 這個模版是為了方便我計算,題目需要
複製程式碼
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,設定我就不全部貼了,只貼需要改的部分
# 這本來是註釋掉了的
ITEM_PIPELINES = {
'glidedsky.pipelines.GlidedskyPipeline': 300,
}
複製程式碼
接著直接執行即可
➜ cd /你專案儲存地址/glidedsky/
➜ scrapy startpoject glidedsky
複製程式碼
輸出結果就不展示了,賊雞兒多
結論
這種反爬蟲手段是我第一次遇到,以前遇到的也就驗證碼和ip限制,不過也算是漲了知識,最後結果是我解決了