用前考慮清楚,傷敵一千自損八百的字體反爬蟲

內容選自即將出版的《Python3 反爬蟲原理與繞過實戰》,本次公開書稿範圍爲第 6 章——文本混淆反爬蟲。本篇爲第 6 章中的第 4 小節,其他小節將逐步放送css

字體反爬蟲開篇概述

在 CSS3 以前,Web 開發者必須使用用戶計算機上已有的字體。可是在 CSS3 時代,開發者可使用@font-face 爲網頁指定字體,對用戶計算機字體的依賴。開發者可將心儀的字體文件放在 Web 服務器上,並在 CSS 樣式中使用它。用戶使用瀏覽器訪問 Web 應用時,對應的字體會被瀏覽器下載到用戶的計算機上。html

在學習瀏覽器和頁面渲染的相關知識時,咱們瞭解到 CSS 的做用是修飾 HTML ,因此在頁面渲染的時候不會改變 HTML 文檔內容。因爲字體的加載和映射工做是由 CSS 完成的,因此即便咱們藉助 Splash、Selenium 和 Puppeteer 工具也沒法得到對應的文字內容。字體反爬蟲正是利用了這個特色,將自定義字體應用到網頁中重要的數據上,使得爬蟲程序沒法得到正確的數據。web

6.4.1 字體反爬蟲示例

示例 7:字體反爬蟲示例。算法

網址:www.porters.vip/confusion/m…瀏覽器

任務:爬取影片信息展現頁中的影片評分、評價人數和票房數據,頁面內容如圖 6-32 所示。bash

圖 6-32 示例 7 頁面服務器

在編寫代碼以前,咱們須要肯定目標數據的元素定位。定位時,咱們在 HTML 中發現了一些奇怪的符號,HTML 代碼以下:網絡

<div class="movie-index"> 
   <p class="movie-index-title">用戶評分</p> 
   <div class="movie-index-content score normal-score"> 
       <span class="index-left info-num "> 
       <span class="stonefont"> ☒.☒ </span> 
       </span> 
   <div class="index-right"> 
   <div class="star-wrapper"> 
   <div class="star-on" style="width:90%;"></div> 
   </div> 
   		<span class="score-num"><span class="stonefont"> ☒☒. ☒☒ 萬</span>人評分</span> 
   </div> 
   </div> 
</div>
複製代碼

頁面中重要的數據都是一些奇怪的字符,本應該顯示「9.7」的地方在 HTML 中顯示的是「☒.☒」,而本應該顯示「56.83」的地方在 HTML 中顯示的是「☒☒.☒☒」。與 6.3 節中的映射反爬蟲不一樣,案例中的文字都被「☒」符號代替了,根本沒法分辨。這就很奇怪了,「☒」能表明這麼多種數字嗎?app

要注意的是,Chrome 開發者工具的元素面板中顯示的內容不必定是相應正文的原文,要想知道「☒」符號是什麼,還須要到網頁源代碼中確認。對應的網頁源代碼以下:異步

<div class="movie-index">
    <p class="movie-index-title">用戶評分</p>
    <div class="movie-index-content score normal-score">
        <span class="index-left info-num ">
            <span class="stonefont">&#xe624.&#xe9c7</span>
        </span>
        <div class="index-right">
          <div class="star-wrapper">
            <div class="star-on" style="width:90%;"></div>
          </div>
          <span class="score-num"><span class="stonefont">&#xf593&#xe9c7&#xe9c7.&#xe624萬</span>人評分</span>
        </div>
    </div>
</div>
複製代碼

從網頁源代碼中看到的並非符號,而是由&#x 開頭的一些字符,這與示例 6 中的 SVG 映射反爬蟲很是類似。咱們將頁面顯示的數字與網頁源代碼中的字符進行比較,映射關係如圖 6-33 所示。

圖 6-33 字符與數字的映射關係

字符與數字是一一對應的,咱們只須要多找一些頁面,將 0 ~ 9 數字對應的字符湊齊便可。但若是目標網站的字體是動態變化的呢?映射關係也是變化的呢?

根據 6.3 節的學習和分析,咱們知道人爲映射並不能解決這些問題,必須找到映射關係的規律,並使用 Python 代碼實現映射算法才行。繼續往下分析,難道字符映射是先異步加載數據再使用 JavaScript 渲染的?

圖 6-34 請求記錄

網絡請求記錄如圖 6-34 所示,請求記錄中並無發現異步請求,這個猜想並無獲得證明。CSS 樣式方面有沒有線索呢?頁面中包裹符號的標籤的 class 屬性值都是 stonefont:

<span class="stonefont">&#xe624.&#xe9c7</span> 
<span class="stonefont">&#xf593&#xe9c7&#xe9c7.&#xe624 萬</span> 
<span class="stonefont">&#xea16&#xe339.&#xefd4&#xf19a</span>
複製代碼

但對應的 CSS 樣式中僅設置了字體:

.stonefont { 
 	font-family: stonefont; 
}
複製代碼

既然是自定義字體,就意味着會加載字體文件,咱們能夠在網絡請求中找到加載的字體文件 movie.woff,並將其下載到本地,接着使用百度字體 Editor 看一看裏面的內容。

百度字體Editor FontEditor (詳見 fontstore.baidu.com/static/edit… ttf、woff、eot、otf 格式的字體文件,具有這些格式字體文件的導入和導出功能,而且提供字形調整、輪廓調整和字體實時預覽功能,界面如圖 6-35 所示。

圖 6-35 百度字體 Editor 界面

打開頁面後,將 movie.woff 文件拖曳到百度字體 Editor的灰色區域便可,字體文件內容如圖 6-36 所示。

圖 6-36 字體文件 movie.woff 預覽

該字體文件中共有 12 個字體塊,其中包括 2 個空白字體塊和 0 ~ 9 的數字字體塊。咱們能夠大膽地猜想,評分數據和票房數據中使用的數字正是今後而來。

由此看來,咱們還須要瞭解一些字體文件格式相關的知識,在瞭解文件格式和規律後,纔可以找到更合理的解決辦法。

6.4.2 字體文件 WOFF

WOFF(Web Open Font Format,Web 開放字體格式)是一種網頁所採用的字體格式標準。本質上基於 SFNT 字體(如 TrueType),因此它具有 TrueType 的字體結構,咱們只須要了解 TrueType 字體的相關知識便可。

TrueType 字體是蘋果公司與微軟公司聯合開發的一種計算機輪廓字體,TrueType 字體中的每一個字形由網格上的一系列點描述,點是字體中的最小單位,字形與點的關係如圖 6-37 所示。

圖 6-37 字形與點的關係

字體文件中不只包含字形數據和點信息,還包括字符到字形映射、字體標題、命名和水平指標等,這些信息存在對應的表中,因此咱們也能夠認爲 TrueType 字體文件由一系列的表組成,其中經常使用的表

及其做用如圖 6-38 所示。

圖 6-38 構成字體文件的經常使用表及其做用

如何查看這些表的結構和所包含的信息呢?咱們能夠藉助第三方 Python 庫 fonttools 將 WOFF 等字體文件轉換成 XML 文件,這樣就能查看字體文件的結構和表信息了。首先咱們要安裝 fonttools 庫, 安裝命令爲:

$ pip install fonttools
複製代碼

安裝完畢後就能夠利用該庫轉換文件類型,對應的 Python 代碼爲:

from fontTools.ttLib import TTFont 
font = TTFont('movie.woff') # 打開當前目錄的 movie.woff 文件
font.saveXML('movie.xml') # 另存爲 movie.xml
複製代碼

代碼運行後就會在當前目錄生成名爲 movie 的 XML 文件。文件中字符到字形映射表 cmap 的內容以下:

<cmap_format_4 platformID="0" platEncID="3" language="0"> 
   <map code="0x78" name="x"/> 
   <map code="0xe339" name="uniE339"/> 
   <map code="0xe624" name="uniE624"/> 
   <map code="0xe7df" name="uniE7DF"/> 
   <map code="0xe9c7" name="uniE9C7"/> 
   <map code="0xea16" name="uniEA16"/> 
   <map code="0xee76" name="uniEE76"/> 
   <map code="0xefd4" name="uniEFD4"/> 
   <map code="0xf19a" name="uniF19A"/> 
   <map code="0xf57b" name="uniF57B"/> 
   <map code="0xf593" name="uniF593"/> 
</cmap_format_4>
複製代碼

map 標籤中的 code 表明字符,name 表明字形名稱,關係如圖 6-39 所示。

圖 6-39 字符到字形映射關係示例

XML 中的字符 0xe339 與網頁源代碼中的字符 對應,這樣咱們就肯定了 HTML 中的字符碼與 movie.woff 字體文件中對應的字形關係。字形數據存儲在 glyf 表中,每一個字形的數據都是獨立的,例如字形 uniE339 的字形數據以下:

<TTGlyph name="uniE339" xMin="0" yMin="-12" xMax="510" yMax="719"> 
   <contour> 
     <pt x="410" y="534" on="1"/> 
     <pt x="398" y="586" on="0"/> 
     <pt x="377" y="609" on="1"/> 
     <pt x="341" y="646" on="0"/> 
     <pt x="289" y="646" on="1"/> 
     ... 
   </contour> 
   <contour> 
     <pt x="139" y="232" on="1"/> 
     <pt x="139" y="188" on="0"/> 
     <pt x="178" y="103" on="0"/> 
     ... 
   </contour> 
   <instructions/> 
</TTGlyph>
複製代碼

TTGlyph 標籤中記錄着字形的名稱、x 軸座標和 y 軸座標(座標也能夠理解爲字形的寬高)。contour 標籤記錄的是字形的輪廓信息,也就是多個點的座標位置,正是這些點構成了如圖 6-40 所示的字形。

圖 6-40 字形 uniE339 的輪廓

咱們能夠在百度字體 Editor 中調整點的位置,而後保存字體文件並將新字體文件轉換爲 XML 格式,相同名稱的字形數據以下:

<TTGlyph name="uniE339" xMin="115" yMin="6" xMax="430" yMax="495"> 
 <contour> 
   <pt x="400" y="352" on="1"/> 
   <pt x="356" y="406" on="0"/> 
   <pt x="342" y="421" on="1"/> 
   <pt x="318" y="446" on="0"/> 
   <pt x="283" y="446" on="1"/> 
   ... 
 </contour> 
 <instructions/> 
</TTGlyph>

複製代碼

接着將調整前的字形數據和調整後的字形數據進行對比。

如圖 6-41 所示,點的位置調整後,字形數據也會發生相應的變化,如 xMin、xMax、yMin、yMax 還有 pt 標籤中的 x 座標 y 座標都與以前的不一樣了。

圖 6-41 字形數據對比

XML 文件中記錄的是字形座標信息,實際上,咱們沒有辦法直接經過字形數據得到文字,只能從其餘方面想辦法。雖然目標網站使用多套字體,但相同文字的字形也是相同的。好比如今有 movie.woff 和 food.woff 這兩套字體,它們包含的字形以下:

# movie.woff 
# 包含 10 個字形數據:[0123456789] 
<cmap_format_4 platformID="0" platEncID="3" language="0"> 
   <map code="0x78" name="x"/> 
   <map code="0xe339" name="uniE339"/> # 數字 6 
   <map code="0xe624" name="uniE624"/> # 數字 9 
   <map code="0xe7df" name="uniE7DF"/> # 數字 2 
   <map code="0xe9c7" name="uniE9C7"/> # 數字 7 
   <map code="0xea16" name="uniEA16"/> # 數字 5 
   <map code="0xee76" name="uniEE76"/> # 數字 0 
   <map code="0xefd4" name="uniEFD4"/> # 數字 8 
   <map code="0xf19a" name="uniF19A"/> # 數字 3 
   <map code="0xf57b" name="uniF57B"/> # 數字 1
   <map code="0xf593" name="uniF593"/> # 數字 4 
</cmap_format_4> 

# food.woff 
# 包含 3 個字形數據:[012] 
<cmap_format_4 platformID="0" platEncID="3" language="0"> 
   <map code="0x78" name="x"/> 
   <map code="0xe556" name="uniE556"/> # 數字 0 
   <map code="0xe667" name="uniE667"/> # 數字 1 
   <map code="0xe778" name="uniE778"/> # 數字 2 
</cmap_format_4>

複製代碼

要實現自動識別文字,須要先準備參照字形,也就是人爲地準備數字 0 ~ 9 的字形映射關係和字形數據,如:

# 0 和 7 與字形名稱的映射僞代碼,data 鍵對應的值是字形數據
font_mapping = [ 
   {'name': 'uniE9C7', 'words': '7', 'data': 'uniE9C7_contour_pt'}, 
   {'name': 'uniEE76', 'words': '0', 'data': 'uniEE76_countr_pt'}, 
]

複製代碼

當咱們遇到目標網站上其餘字體文件時,就可使用參照字形中的字形數據與目標字形進行匹配,若是字形數據很是接近,就認爲這兩個字形描述的是相同的文字。字形數據包含記錄字形名稱和字形起止座標的 TTGlyph 標籤以及記錄點座標的 pt 標籤,起止座標表明的是字形在畫布上的位置,點座標表明字形中每一個點在畫布上的位置。在起止座標中,x 軸差值表明字形寬度,y 軸差值表明字形高度。

如圖 6-42 所示,兩個字形的起止座標和寬高都有很大的差異,可是卻可以描述相同的文字,因此字形在畫布中的位置並不會影響描述的文字,字形寬度和字形高度也不會影響描述的文字。

圖 6-42 描述相同文字的兩個字形

點座標的數量和座標值能夠做爲比較條件嗎?

如圖 6-43 所示,兩個不一樣文字的字形數據是不同的。雖然這兩種字形的 name 都是 uniE9C7,可是字形數據中大部分 pt 標籤 x 和 y 的差距都很大,因此咱們能夠斷定這兩個字形描述的並非

同一個文字。你可能會想到點的數量也能夠做爲排除條件,也就是說若是點的數量不相同,那麼這個

兩個字形描述的就不是同一個文字。真的是這樣嗎?

圖 6-43 描述不一樣文字的字形數據對比

在圖 6-44 中,左側描述文字 7 的字形有 17 個點,而右側描述文字 7 的字形卻有 20 個點。對應的字形信息如圖 6-45 所示。

圖 6-44 描述相同文字的字形

圖 6-45 描述相同文字的字形信息

雖然點的數量不同,可是它們的字形並無太大的變化,也不會形成用戶誤讀,因此點的數量並不能做爲排除不一樣字形的條件。所以,只有起止座標和點座標數據徹底相同的字形,描述的纔是相同字符。

6.4.3 字體反爬蟲繞過實戰

要肯定兩組字形數據描述的是否爲相同字符,咱們必須取出 HTML 中對應的字形數據,而後將待確認的字形與咱們準備好的基準字形數據進行對比。如今咱們來整理一下這一系列工做的步驟。

(1) 準備基準字形描述信息。

(2) 訪問目標網頁。

(3) 從目標網頁中讀取字體編碼字符。

(4) 下載 WOFF 文件並用 Python 代碼打開。

(5) 根據字體編碼字符找到 WOFF 文件中的字形輪廓信息。

(6) 將該字形輪廓信息與基準字形輪廓信息進行對比。

(7) 得出對比結果。

咱們先完成前 4 個步驟的代碼。下載 WOFF 文件並將其中字形描述的文字與人類認知的文字進行映射。因爲字形數據比較龐大,因此咱們能夠將字形數據進行散列計算,這樣獲得的結果既簡短又惟一,不會影響對比結果。這裏以數字 0 ~ 9 爲例:

base_font = { 
 "font": [{"name": "uniEE76", "value": "0", "hex": "fc170db1563e66547e9100cf7784951f"}, 
 {"name": "uniF57B", "value": "1", "hex": "251357942c5160a003eec31c68a06f64"}, 
 {"name": "uniE7DF", "value": "2", "hex": "8a3ab2e9ca7db2b13ce198521010bde4"}, 
 {"name": "uniF19A", "value": "3", "hex": "712e4b5abd0ba2b09aff19be89e75146"}, 
 {"name": "uniF593", "value": "4", "hex": "e5764c45cf9de7f0a4ada6b0370b81a1"}, 
 {"name": "uniEA16", "value": "5", "hex": "c631abb5e408146eb1a17db4113f878f"}, 
 {"name": "uniE339", "value": "6", "hex": "0833d3b4f61f02258217421b4e4bde24"}, 
 {"name": "uniE9C7", "value": "7", "hex": "4aa5ac9a6741107dca4c5dd05176ec4c"}, 
 {"name": "uniEFD4", "value": "8", "hex": "c37e95c05e0dd147b47f3cb1e5ac60d7"}, 
 {"name": "uniE624", "value": "9", "hex": "704362b6e0feb6cd0b1303f10c000f95"}] 
}

複製代碼

字典中的 name 表明該字形的名稱,value 表明該字形描述的文字,hex 表明字形信息的 MD5 值。

考慮到網絡請求記錄中的字體文件路徑有可能會變化,咱們必須找到 CSS 中設定的字體文件路徑,引入 CSS 的 HTML 代碼爲:

<link href="./css/movie.css" rel="stylesheet">

複製代碼

由引入代碼得知該 CSS 文件的路徑爲 www.porters.vip/confusion/c… @font-face 處就是設置字體的代碼:

@font-face { 
   font-family: stonefont; 
   src:url('../font/movie.woff') format('woff'); 
}

複製代碼

字體文件路徑爲 www.porters.vip/confusion/f… Python 代碼以下:

import re 
from parsel import Selector 
from urllib import parse 
from fontTools.ttLib import TTFont 
url = 'http://www.porters.vip/confusion/movie.html' 
resp = requests.get(url) 
sel = Selector(resp.text) 
# 提取頁面加載的全部 css 文件路徑
css_path = sel.css('link[rel=stylesheet]::attr(href)').extract() 
woffs = [] 
for c in css_path: 
   # 拼接正確的 css 文件路徑
   css_url = parse.urljoin(url, c) 
   # 向 css 文件發起請求
   css_resp = requests.get(css_url) 
   # 匹配 css 文件中的 woff 文件路徑
   woff_path = re.findall("src:url\('..(.*.woff)'\) format\('woff'\);", 
   css_resp.text)
   if woff_path: 
       # 如故路徑存在則添加到 woffs 列表中
       woffs += woff_path 
woff_url = 'http://www.porters.vip/confusion' + woffs.pop() 
woff = requests.get(woff_url) 
filename = 'target.woff' 
with open(filename, 'wb') as f: 
   # 將文件保存到本地
   f.write(woff.content) 
# 使用 TTFont 庫打開剛纔下載的 woff 文件
font = TTFont(filename)

複製代碼

由於 TTFont 能夠直接讀取 woff 文件的結構,因此這裏不須要將 woff 保存爲 XML 文件。接着以評分數據 9.7 對應的編碼 #xe624.#xe9c7 進行測試,在原來的代碼中引入基準字體數據 base_font,而後新增如下代碼:

web_code = '&#xe624.&#xe9c7'
# 編碼文字替換
woff_code = [i.upper().replace('&#X', 'uni') for i in web_code.split('.')] 
import hashlib 
result = [] 
for w in woff_code: 
   # 從字體文件中取出對應編碼的字形信息
   content = font['glyf'].glyphs.get(w).data 
   # 字形信息 MD5 
   glyph = hashlib.md5(content).hexdigest() 
   for b in base_font.get('font'): 
       # 與基準字形中的 MD5 值進行對比,若是相同則取出該字形描述的文字
       if b.get('hex') == glyph: 
           result.append(b.get('value')) 
           break 
# 打印映射結果
print(result)

複製代碼

以上代碼運行結果爲:

['9', '7']

複製代碼

運行結果說明可以正確映射字體文件中字形描述的文字。

6.4.4 小結

字體反爬能給爬蟲工程師帶來很大的麻煩。雖然爬蟲工程師找到了應對方法,但這種方法依賴的條件比較嚴苛,若是開發者頻繁改動字體文件或準備多套字體文件並隨機切換,那真是一件令爬蟲工程師頭疼的事。不過,這些工做對於開發者來講也不是輕鬆的事。

新書福利

真是翹首以盼!《Python3 反爬蟲原理與繞過實戰》一書終於要跟你們見面了!爲了感謝你們對韋世東和本書的期待與支持,在新書發佈時會舉辦多場送書活動和限時折扣活動。

想要與韋世東交流或者參加新書發佈活動的朋友能夠掃描二維碼進羣與我互動哦!

轉載說明

本篇內容摘自出版圖書《Python3 反爬蟲原理與繞過實戰》,歡迎各位好友與同行轉載!

記得帶上相關的版權信息哦😊。

相關文章
相關標籤/搜索