字體反爬之實習僧

字體反爬是爬蟲不可避免的一道關卡,因爲這是成本比較低,而且效果還不錯的一種方式。

今天我們先看看實習僧的字體爬蟲怎麼破解。首先我們先隨便搜索一個職業,https://www.shixiseng.com/interns?k=數據庫&p=1。F12查看源碼發現,職業的某些漢字字母和所有數字都是框框,這基本可以確定使用了自定義字體。
在這裏插入圖片描述
這裏可以看到li標籤有一個font屬性,點擊一下這個標籤,右邊就會出現詳細的css屬性。我們只看.font,發現有font-family: myFont這個信息。我們先找到這個字體文件,這個可以去原網頁或者加載的js裏搜索,可以找到這段代碼是包含在原網頁的。而字體文件是以base64字符串的格式傳輸的,先用python自帶的base64庫解碼一下字符串,然後另存爲ttf文件。

import base64

s = '加密字符串'
with open('a.ttf', 'wb') as f:
	f.write(base64.b64decode(s))

然後用FontCreato這個工具打開字體文件:
在這裏插入圖片描述
這就是被重新編碼的文字,他們的編碼和utf-8是不一樣的,所以瀏覽器會顯示爲框框。但瀏覽器在文字渲染(瀏覽器通過繪製相應像素點來達到顯示整個漢字)的時候被顯示成想要的漢字,因爲它們的渲染代碼在字體文件中被更改。

到了這裏,我想很多人肯定是去百度找別人怎麼解決這種問題的。當然,我也是,我看了很多關於字體反爬的文章,基本上是使用fonttools來尋找渲染字體代碼是否相同,只要渲染代碼相同,則判定爲同一個字。爲了方便人理解,我們先使用fonttools庫將ttf文件轉化爲xml文件。

from  fontTools.ttLib  import  TTFont

font = TTFont('a.ttf')
font.saveXML('a.xml')

使用文本編輯器打開xml文件,這裏我使用的是editplus。
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
初看文件是看不懂文件表達的含義的,這裏我粗略的說明我看懂的部分,也是爬蟲要用到的那部分,至於文件的格式就不深究了。首先第一張圖有id和name,每個ID會對應一個name,這兩個值暫時是沒有用的。我們再看第二張圖,包含code和name,code代表這個字的編碼的16進制,將0x改成\u就是網頁源碼中的框框的字符了,name會在第三張圖中用到,第三種圖表示將name所表示的code渲染成某種形狀(即相應文字),只要對照每一個文字的這一段代碼就可以判斷是不是同一文字了,既然要對照,首先我們手裏肯定要有一份已經知道渲染是什麼文字的字體了。所以我們必須手工解碼一份字體。

到這裏後面我就不多說了,因爲如果使用這種方法,那麼爬蟲的效率就有點低了,這樣解碼一套字體會耗費一定時間,雖然網站一般是每天或者每幾天更新一次字體文件的,但工作量也不小。這樣大量的工作也會長時間佔用電腦大量的CPU。而且字體渲染不同其實瀏覽器也有可能顯示爲同一漢字,稍微改變一下字體形狀就行(參考一下不同字體爲什麼不一樣,你也能看成同一個字)。這樣爬蟲就不是要判斷相等,而是判斷一個範圍,效率就更低了。於是我就秉承着程序員的核心思想繼續思考:不會偷懶的程序員不是好程序員

看着別人的博客,發現別人用fontcreator打開的字體文件和我打開的文件不僅字是一樣的,而且數量和順序都是一樣的(博客時間是2018年了),也就是說實習僧至少有一年沒有更新網頁代碼了。雖然這個字體文件變化很頻繁,但有沒有可能所有的字體文件都有一個特定的順序排序這些文字。這個猜想是很合理的,每個程序員都會有一個歸一化的思想,比如代碼結構,代碼排版等。

我們看第一張圖的ID就知道,文字順序應該是ID來決定的,而右邊的name只是文字的一個別名,這個別名不確定會不會變化,我們就當他會變化吧,那麼我只需要獲取ID和name對應的字典,還有code和name對應的字典,組合成ID和code對應的字典,再將fontcreator裏面顯示出來的文字按順序放在一個列表,再使用ID作爲索引取出列表對應的值,不就做成了一個code和文字的密碼錶了。操作一番後發現,猜想完全是正確的,當然我只是實驗了一次,後面還需要 靠時間來驗證。希望不要被打臉。

還有一種思路:只獲取當然頁面每個職業的具體url,然後訪問子頁面,子頁面是隻對十個數字重新編碼的,那麼我們只要手工獲取這十個數字的編碼表給爬蟲就行。這是一種思路,但一般情況下不可取,因爲它給爬蟲增加了相當多的工作量,有多少數據就需要多訪問多少個網頁,這並不程序員。如果只是少量數據,可以這麼操作,但如果需要大量數據的時候就顯得很不合理了。

代碼如下:

# -*- coding: utf-8 -*-
import base64
import re
import pyquery
import requests
from  fontTools.ttLib  import  TTFont


def get():
    url = 'https://www.shixiseng.com/interns?k=Python&p=31'
    headers = {'Host': 'www.shixiseng.com',
               'Referer': 'https://www.shixiseng.com/',
               'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36'
    }
    resp = requests.get(url, headers=headers)
    if resp.status_code == 200:
        return resp.text


class UniDecrypt(object):
    def __init__(self, ciphertext):
        self.ciphertext = ciphertext
        self.decrypt()
        self.analysis()
    
    def __call__(self, word):
        return word.translate(self.tab)
    
    def decrypt(self):
        s = base64.b64decode(self.ciphertext)
        with open('temp.ttf', 'wb') as f:
            f.write(s)
        font = TTFont('temp.ttf')
        font.saveXML('temp.xml')
    
    def analysis(self):
        words = '  0123456789一師X會四計財場DHLPT聘招工d周l端p年hx設程二五天tCG前KO網SWcgkosw廣市月個BF告NRVZ作bfjnrvz三互生人政AJEI件M行QUYaeim軟qu銀y聯'
        with open('temp.xml') as f:
            xml = f.read()
        temp1 = re.findall(r'<GlyphID id="(\d+)" name="(.*?)"/>',xml)
        temp2 = list(set(re.findall(r'<map code="(.*?)" name="(.*?)"/>',xml)))
        d2 = {x[1]:x[0] for x in temp2}
        #print(d2)
        wordtab = {chr(int(d2[x[1]], 16)):words[int(x[0])] for x in temp1 if not (x[0] == '0' or x[0] == '1')}
        self.tab = str.maketrans(wordtab)
        
    
if __name__ == '__main__':
#    with open('a.html') as f:
#        html = f.read()
    html = get()
    ciphertext = re.search(r'base64,(.*?)"', html).group(1)
    uni = UniDecrypt(ciphertext)
    
    doc = pyquery.PyQuery(html)
    position_list = doc('.position-list .position-item.clearfix.font').items()
    for position in position_list:
        job_name = position('.position-name').text()
        url = position('.position-name').attr('href')
        salary = position('.position-salary').text()
        place = position('.info2.clearfix span:first-of-type').text()
        work_day = position('.info2.clearfix span:nth-child(2)').text()
        least_month = position('.info2.clearfix span:last-of-type').text()
        company = position('.company-name').text()
        category = position('.company-more-info.clearfix span:first-of-type').text()
        scale = position('.company-more-info.clearfix span:last-of-type').text()
        d = {'job_name':uni(job_name),'url':uni(url),'salary':uni(salary),
             'place':uni(place),'work_day':uni(work_day),'least_month':uni(least_month),
             'company':uni(company),'category':uni(category),'scale':uni(scale)}
        print(d)
     

這樣拿到密碼錶所花費的時間是非常短的(一兩秒就行),基本拿到一次就可以在整個爬蟲週期使用,如果哪天失效,只需要在獲取一次就行。就算他每個網頁都返回一個不同的字體文件,我們所花費的時間也不會太多,效率會遠遠高於對比字體。當然,這只是針對個例,而開始介紹的方法是比較通用的,另外,如果連fonttools都解決不了了,就只能使用OCR識別了。OCR的效率很低很低,不到萬不得已,不要使用。

既然拿到了密碼錶,那麼該如何快速替換爬蟲中的字符呢?而且拿到的也只是0x一樣的字符串,怎麼變成\u一樣的字符編碼呢(replace(‘0x’, ‘\u’)或者replace(‘0x’, ‘\u’)就不用想了)。首先回答第一個問題,0x的字符變成\u的字符編碼只需要使用int將0x字符串變成十六進制的數字,然後使用chr(數字)變成\u形式的字符編碼了。替換文本中的一些字符,寫100個replace當然可行,但是不是有點太不程序員了。其實python提供了內置的方法,請百度str.maketrans和str.translate。

下一篇博客:scrapy爬取實習僧所有數據

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章