看到一篇文章,介紹在反爬蟲過程中,前端工程師的各種腦洞,文章見這里。
文章里介紹了幾個大的網站,在反爬蟲過程中,采取的各式各樣的策略,無不體現出前端工程師的奇葩腦洞。
還挺有意思的,就簡單分析了一下,針對每個方案,看看有沒有解決辦法,于是整理成博客,記錄一下。
1. 自定義字體形式
該方案是,自定義了一種字體,網頁中使用亂碼字符或者其他混淆字符,通過自定義字體的渲染成正確的顯示數據。
代表網站有貓眼電影和去哪兒手機端。
1.1 貓眼電影
如上圖,是貓眼首頁今日票房欄的前10名統計(截圖只截取了前三名),其中的票房數據,對爬蟲來說是私密數據,于是,貓眼給“加密”了。
網頁代碼顯示的是一堆亂碼,都是方框。。。
我們通過瀏覽器的開發者工具查看該部分“方框”數字,發現是用了自定義的字體渲染成可視的數字的。
woff字體是網頁開放字體格式,詳細可參見 MDN。
我們把這個woff格式的字體文件下載下來,看一下這個自定義的字體里有啥奧秘呢?
這里推薦一個在線的字體編輯工具:百度字體編輯器。
將下載后的woff文件字體,在百度字體編輯器中打開:
好了,一目了然,這個字體文件里,采用隨機的Unicode編碼來定義了 0-9這幾個數字以及一個空白符和一個小數點,而且數字定義的順序不是固定的,Unicode編碼也不是連續的。
也就是說,在HTML頁面源碼看到的方框,它的unicode應該和字體上的值是對應的,你可以用 charCodeAt()方法進行驗證一下。
如果說,woff文件是固定的,那么其實問題很簡單。但是,貓眼這個woff文件并不是固定的,而是隨機的。
如果,woff字體文件里定義字體的順序和實際數字順序一致,或者其unicode值的順序和真實數字是一致的,也簡單。但是,順序也是隨機的。。。
所以,難道就真的沒辦法搞了么?
當然不!!!任何能在頁面上顯示的,都可以搞。
找到python的一個庫 fonttools
,可以解析成字體為 xml 文件,然后再根據xml里的信息找找:
其實百度字體編輯器代碼是開源的,它其中依賴了一個核心庫 fonteditor-core ,這個庫應該也能解析字體數據,但是我在實驗時,老是報錯解析錯誤,不知為何,有興趣的小伙伴可以自行研究一下,并分享一下研究成果,謝過。
from fontTools.ttLib import TTFont
font = TTFont('/Users/coolcao/Downloads/b0a53bf9d791622d4681b8344fd118f92088.woff')
font.saveXML('/Users/coolcao/maoyan2.xml')
生成的xml文件,有兩部分很重要:
<GlyphOrder>
<!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
<GlyphID id="0" name="glyph00000"/>
<GlyphID id="1" name="x"/>
<GlyphID id="2" name="uniEABA"/>
<GlyphID id="3" name="uniEB51"/>
<GlyphID id="4" name="uniE06D"/>
<GlyphID id="5" name="uniF88C"/>
<GlyphID id="6" name="uniF012"/>
<GlyphID id="7" name="uniF6C7"/>
<GlyphID id="8" name="uniE373"/>
<GlyphID id="9" name="uniF48F"/>
<GlyphID id="10" name="uniE429"/>
<GlyphID id="11" name="uniF4CA"/>
</GlyphOrder>
第一部分是字體概覽,定義了字體集中的name,注意,這里id和實際數字并無關系,并不是實際的數字0,1,2...等等。name是采用unicode定義的 ,和在百度字體編輯器中的正好是一致的。
<TTGlyph name="uniF4CA" xMin="0" yMin="-13" xMax="511" yMax="719">
<contour>
<pt x="130" y="201" on="1"/>
<pt x="145" y="126" on="0"/>
<pt x="216" y="60" on="0"/>
<pt x="270" y="60" on="1"/>
<pt x="332" y="60" on="0"/>
<pt x="417" y="146" on="0"/>
<pt x="417" y="270" on="0"/>
<pt x="378" y="309" on="1"/>
<pt x="337" y="349" on="0"/>
<pt x="277" y="349" on="1"/>
<pt x="251" y="349" on="0"/>
<pt x="215" y="339" on="1"/>
<pt x="225" y="416" on="1"/>
<pt x="239" y="415" on="1"/>
<pt x="296" y="415" on="0"/>
<pt x="385" y="474" on="0"/>
<pt x="385" y="535" on="1"/>
<pt x="385" y="583" on="0"/>
<pt x="322" y="646" on="0"/>
<pt x="268" y="646" on="1"/>
<pt x="217" y="646" on="0"/>
<pt x="149" y="584" on="0"/>
<pt x="139" y="518" on="1"/>
<pt x="51" y="533" on="1"/>
<pt x="67" y="623" on="0"/>
<pt x="124" y="670" on="1"/>
<pt x="182" y="719" on="0"/>
<pt x="266" y="719" on="1"/>
<pt x="324" y="719" on="0"/>
<pt x="374" y="693" on="1"/>
<pt x="423" y="669" on="0"/>
<pt x="476" y="581" on="0"/>
<pt x="476" y="485" on="0"/>
<pt x="426" y="410" on="0"/>
<pt x="377" y="388" on="1"/>
<pt x="440" y="373" on="0"/>
<pt x="511" y="281" on="0"/>
<pt x="511" y="211" on="1"/>
<pt x="511" y="118" on="0"/>
<pt x="374" y="-13" on="0"/>
<pt x="270" y="-13" on="1"/>
<pt x="175" y="-13" on="0"/>
<pt x="51" y="99" on="0"/>
<pt x="42" y="189" on="1"/>
</contour>
<instructions/>
</TTGlyph>
第二部分是具體每個字體的座標集合信息,這里我只摘錄了其中的一個字符F4CA
的信息,我們多刷新兩次頁面,拿兩個不同的woff文件,轉換成xml文件,對比會發現,雖然每次定義的unicode不同,順序是隨機的,unicode也不連續,但是,但是,但是,有一樣是相同的,那就是上面第二部分字體的座標信息。為啥一樣呢?因為每個數字樣式是固定的,所以畫出圖來座標必定是一樣的。
好了同志們,到這里,基本就明朗了,我們可以人工先把幾個數字的座標點進行標記,然后每次刷新時,拿到新的woff字體時,通過fonttool將字體轉換成xml格式,根據座標點信息,判斷其uncode值分別是多少。然后再將代碼中的“方框”轉換成真實數字即可。
1.2 去哪兒手機端網頁
去哪兒手機端采用的方案和貓眼類似,都是用的自定義字體進行混淆。
但去哪兒采用的是ttf格式的字體文件。這是不同點一。
而且,去哪兒自定義的字體,采用的unicode也比較簡單,看下面:
去哪兒直接用的真實數字的uncode進行編碼,只不過順序和真實數字不是一一對應的,也就是說,網頁源碼中如果是 '183',實際顯示的數字卻是 '361'。
而且每次好像也是不一樣的。不過沒關系,只要能用 fonttool 將其轉換成xml文件,拿到里面的座標數據,那么,沒跑。
1.3 起點中文網
有一個小說閱讀網站,叫起點中文網,也是采用了自定義字體的形式,這也是在cnode上有一個小伙伴提問的,我這次也看了一下。
不看不知道,一看嚇一跳,拿到源碼里的“方框字”后,看了一下其 unicode 編碼,全是一個unicode編碼,如下面:
$ node test.js
94.37
d821
d821
d821
d821
d821
其中第一行94.37是真實顯示的閱讀數,后面的每一行是一個方框字對應的unicode編碼,當我看到結果是,崩潰了,都是一樣的,什么鬼。。。 同一個字符編碼,能渲染出不同的數字來???
怎么套路和貓眼和去哪兒不一樣呢?
從源碼中,看到,這段閱讀數加密的數字,使用了css 類名為 zxJBLkdl,順著源碼,找到類 zxJBLkdl 的定義部分,有這么一段代碼:
<p>
<em>
<style>
@font-face {
font-family: zxJBLkdl;
src: url('https://qidian.gtimg.com/qd_anti_spider/zxJBLkdl.eot?') format('eot');
src: url('https://qidian.gtimg.com/qd_anti_spider/zxJBLkdl.woff') format('woff'), url('https://qidian.gtimg.com/qd_anti_spider/zxJBLkdl.ttf') format('truetype');
}
.zxJBLkdl {
font-family: 'zxJBLkdl' !important;
display: initial !important;
color: inherit !important;
vertical-align: initial !important;
}
</style>
<span class="zxJBLkdl">𘝕𘝘𘝚𘝕𘝙</span>
</em>
<cite>萬字</cite><i>|</i><em><style>@font-face { font-family: zxJBLkdl; src: url('https://qidian.gtimg.com/qd_anti_spider/zxJBLkdl.eot?') format('eot'); src: url('https://qidian.gtimg.com/qd_anti_spider/zxJBLkdl.woff') format('woff'), url('https://qidian.gtimg.com/qd_anti_spider/zxJBLkdl.ttf') format('truetype'); } .zxJBLkdl { font-family: 'zxJBLkdl' !important; display: initial !important; color: inherit !important; vertical-align: initial !important; }</style><span class="zxJBLkdl">𘝗𘝙𘝚𘝕𘝜</span></em>
<cite>萬總點擊<span>·</span>會員周點擊
<style>
@font-face {
font-family: zxJBLkdl;
src: url('https://qidian.gtimg.com/qd_anti_spider/zxJBLkdl.eot?') format('eot');
src: url('https://qidian.gtimg.com/qd_anti_spider/zxJBLkdl.woff') format('woff'), url('https://qidian.gtimg.com/qd_anti_spider/zxJBLkdl.ttf') format('truetype');
}
.zxJBLkdl {
font-family: 'zxJBLkdl' !important;
display: initial !important;
color: inherit !important;
vertical-align: initial !important;
}
</style><span class="zxJBLkdl">𘝕𘝙𘝛𘝘</span></cite><i>|</i><em><style>@font-face { font-family: zxJBLkdl; src: url('https://qidian.gtimg.com/qd_anti_spider/zxJBLkdl.eot?') format('eot'); src: url('https://qidian.gtimg.com/qd_anti_spider/zxJBLkdl.woff') format('woff'), url('https://qidian.gtimg.com/qd_anti_spider/zxJBLkdl.ttf') format('truetype'); } .zxJBLkdl { font-family: 'zxJBLkdl' !important; display: initial !important; color: inherit !important; vertical-align: initial !important; }</style><span class="zxJBLkdl">𘝓𘝚𘝕𘝜</span></em>
<cite>萬總推薦<span>·</span>周
<style>
@font-face {
font-family: zxJBLkdl;
src: url('https://qidian.gtimg.com/qd_anti_spider/zxJBLkdl.eot?') format('eot');
src: url('https://qidian.gtimg.com/qd_anti_spider/zxJBLkdl.woff') format('woff'), url('https://qidian.gtimg.com/qd_anti_spider/zxJBLkdl.ttf') format('truetype');
}
.zxJBLkdl {
font-family: 'zxJBLkdl' !important;
display: initial !important;
color: inherit !important;
vertical-align: initial !important;
}
</style><span class="zxJBLkdl">𘝜𘝛</span></cite>
</p>
在這段代碼里發現了貓膩,其采用的也是隨機字體的形式,比如此次刷新時的字體叫zxJBLkdl.woff,這不重要。
重要的是 <span class="zxJBLkdl">𘝕𘝘𘝚𘝕𘝙</span>
這一行,這是啥,這是使用了html轉義字符輸出了幾個不知名的方塊字,然后通過字體再渲染出真實的數字顯示。
這里和普通的 <
等不同,這里叫做實體編號,實際轉義字符都要轉成實體編號才能被瀏覽器識別,具體請參閱這里。
可是為啥我拿到的unicode是一樣的呢?
答案還是得從字體文件里找。使用fonttool將字體文件轉換成xml,然后你就找到了下面的代碼:
<GlyphOrder>
<!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
<GlyphID id="0" name=".notdef"/>
<GlyphID id="1" name="period"/>
<GlyphID id="2" name="zero"/>
<GlyphID id="3" name="one"/>
<GlyphID id="4" name="two"/>
<GlyphID id="5" name="three"/>
<GlyphID id="6" name="four"/>
<GlyphID id="7" name="five"/>
<GlyphID id="8" name="six"/>
<GlyphID id="9" name="seven"/>
<GlyphID id="10" name="eight"/>
<GlyphID id="11" name="nine"/>
</GlyphOrder>
太明目張膽了,直接用英文來命名數字,再繼續找這幾個英文數字的定義,找到如下代碼:
<cmap_format_12 platformID="3" platEncID="10" format="12" reserved="0" length="148" language="0" nGroups="11">
<map code="0x18751" name="eight"/><!-- ???? -->
<map code="0x18753" name="two"/><!-- ???? -->
<map code="0x18754" name="five"/><!-- ???? -->
<map code="0x18755" name="three"/><!-- ???? -->
<map code="0x18756" name="zero"/><!-- ???? -->
<map code="0x18757" name="nine"/><!-- ???? -->
<map code="0x18758" name="six"/><!-- ???? -->
<map code="0x18759" name="four"/><!-- ???? -->
<map code="0x1875a" name="period"/><!-- ???? -->
<map code="0x1875b" name="one"/><!-- ???? -->
<map code="0x1875c" name="seven"/><!-- ???? -->
</cmap_format_12>
這里應該就是每個字符和其十六進制編碼之間的關系了。將上面的轉義編號的數字部分轉換成十六進制,正好就是這十六進制的編碼,因為這轉義字符是“自定義的”,因此瀏覽器不能識別,只顯示方框,估計在拷貝的過程中發生異常,瀏覽器不能識別具體的字符,都是按照方框去拷貝的,所以出來的unicode都是一樣的。
到這里起點中文網的過程也明朗了,其實質和貓眼也是一樣的,只是過程和形式不大一樣而已。
1.4 小結
采用自定義字體的網站,思路都一致。
后端搭一套字體生成接口,隨機生成一個woff字體,然后返回這個字體文件,以及各個數字的unicode對應關系,前端頁面進行數據填充即可。
基本采用自定義字體的方式,都可以使用上面的思路去破解,先拿到一個字體文件,然后使用fonttool轉換成xml,人工拿到每個數字的座標,然后就可以寫程序,當拿到新的字體文件時,通過座標信息去判斷每個數字到底是多少。
還有一種方案,使用無頭瀏覽器進行截圖,然后使用OCR工具進行文字識別,但這種方案問題在于,OCR識別存在一定的錯誤率,因此并不完美。這里就不說了。
2. 元素定位覆蓋
這種方式太有意思了,給兩套數據,前面一套假的,后面一套真的,然后顯示時,通過css定位,將假的數據覆蓋掉,只顯示真實數據。
這個代碼塊里面,第一個 <b>
元素中有三個<i>
元素,其中0和8是假的,是被后面的兩個<b>
元素的9和4覆蓋掉了,顯示的真實數據是 479 。
這種方式很有意思,但在反爬難度上,和第一種采用自定義字體的方式,略微低一點,感覺有點騙小孩的意思。
我們可以根據第一個b元素的寬度,以及后面b元素的寬度和偏移量來計算,拿到真正的值(實際瀏覽器不就是這樣工作的么)。
比如上面這個,第一個b元素的寬度是54px,左偏移 -54px,第二個b元素為18px,左偏移-18px,那么很明顯覆蓋的是第三個數字嘛,第三個b元素左偏移-54px,覆蓋的是第一個數字,這樣完全可以寫個程序自動判斷,拿到真實數字。
這個方式沒有寫具體代碼,但代碼應該不難寫,有興趣的可以試試。
3. 背景圖拼湊
還有一種形式是,使用背景圖片,然后給位置,截圖,拼湊出真實的數字。
如imweb這篇文章里提到的美團這種方式。但是我沒找到美團哪個頁面現在是這樣的,應該是美團現在改版了,現在都是直接顯示數字。
這種方式,和上面元素定位覆蓋差不多的思想,但稍微復雜點,先把背景圖片拿下來,然后再解析html拿到具體的 background-position具體的值,使用能夠解析圖片的類庫進行截取數字,拿到的數字是圖片格式的,沒辦法,這種只能在通過一次OCR識別了,圖片,真的沒辦法。
因為是圖片,所以與其那么復雜去解析每個位置是啥數字,倒不如直接通過無頭瀏覽器進行截圖,然后通過OCR識別來的直接,因為瀏覽器顯示的就是圖片,只能進行文字識別這條路了。
這種方式在破解時復雜點,還會存在一定的錯誤識別率,其實還是一種不錯的反爬前端方案。但有一點不好的地方在于,由于是使用的圖片,所以在顯示上,不如文字那么清晰,而且在瀏覽器縮放時,也會有一定的模糊,給用戶的體驗會不好,不如文字清晰。
4. 偽類元素代替
汽車之家現在使用的是偽類元素,將詞組拆開,使用偽類元素代替。
這種方式在搞起來,比上面字體要難感覺。
拿汽車之家舉例,偽類的類名是隨機的,而定義偽類的css樣式,是js動態生成的,搞起來比較麻煩。
沒有爬汽車之家的需求,不搞了,我在網上找到一篇關于搞汽車之家這種方式的文章,有興趣的同學可以看下: 反爬蟲破解系列-汽車之家利用css樣式替換文字破解方法
從最終的結果來看,是js動態獲取要替換的問題,然后動態替換了問題。而且現在汽車之家又升級了,要替換的文字也是動態獲取的,沒有任何標志,所以在實際操作起來,難度還是蠻大的。
汽車之家的前端,你可以的,佩服。。。
有興趣的同學真的可以搞一下,搞定這個真的很有成就感。
5. 添加干擾字符并隱藏
這類有微信公共號的文章以及全網代理ip這個網站。
微信公眾號里面,左側下劃線的部分文字為干擾文字,使用css的透明度(opacity)將透明度設置為0隱藏顯示。
全網代理ip這個網站,左側畫細框的部分為干擾文字,使用css的display:none隱藏不顯示。
這種方案的話,需要解析每個dom元素,并根據其css樣式進行選擇正確的字符進行拼裝。難度應該不大,沒具體實施。
6. 總結
這個周末主要的精力放到了搞自定義字體部分了,覺得這個特有意思,因為之前也遇到過,當時不知道咋弄。
爬蟲與反爬向來都是,道高一尺,魔高一丈。在反爬方面,除了在后端上設置反爬策略,如限制ip訪問頻率,限制登錄用戶訪問頻率等等,前端在反爬上,也絞盡腦汁做了不少動作。
還是那句話,反爬做的就是,不斷提升爬蟲解析出正確數據的成本,但沒辦法真正防止爬蟲。
對于爬蟲來說,任何你能從瀏覽器上看到的數據,爬蟲都能拿到,只是在拿數據時,難以程度有所不同而已。
希望該文章能給大家帶來一些思路,幫助大家在爬蟲與反爬蟲過程中,作出更多有創新性的工作。