Python爬蟲 - 記一次字型反爬
最近一直在為找工作煩惱,剛好遇到一家公司要求我先做幾道反爬蟲的題,看了之後覺得自己還挺菜的,不過也過了幾關,剛好遇到一個之前沒遇到過的反爬蟲手段 — 字型反爬
正文
一、站點分析
題目要求: 這裡有一個網站,分了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限制,不過也算是漲了知識,最後結果是我解決了