動態字體加密分析

動態字體反爬html

字體反爬也就是自定義字體反爬,經過調用自定義的字體文件來渲染網頁中的文字,而網頁中的文字再也不是文字,而是相應的字體編碼,經過複製或者簡單的採集是沒法採集到編碼後的文字內容的。app

如今貌似很多網站都有采用這種反爬機制,咱們經過貓眼的實際狀況來解釋一下。ide

 

下圖的是貓眼網頁上的顯示:字體

檢查元素看一下網站

這是什麼鬼,關鍵信息全是亂碼。編碼

熟悉 CSS 的同窗會知道,CSS 中有一個 @font-face,它容許網頁開發者爲其網頁指定在線字體。本來是用來消除對用戶電腦字體的依賴,如今有了新做用——反爬。url

漢字光經常使用字就有好幾千,若是所有放到自定義的字體中,那麼字體文件就會變得很大,必然影響網頁的加載速度,所以通常網站會選取關鍵內容加以保護,如上圖,知道了等於不知道。spa

這裏的亂碼是因爲 unicode 編碼致使的,查看源文件能夠看到具體的編碼信息。3d

搜索 stonefont,找到 @font-face 的定義:code

這裏的 .woff 文件就是字體文件,咱們將其下載下來,利用 http://fontstore.baidu.com/static/editor/index.html 網頁將其打開,顯示以下:

網頁源碼中顯示的  跟這裏顯示的是否是有點像?事實上確實如此,去掉開頭的 &#x 和結尾的 ; 後,剩餘的4個16進制顯示的數字加上 uni 就是字體文件中的編碼。因此 &#xea0b 對應的就是數字「9」。

知道了原理,咱們來看下如何實現。

 

處理字體文件,咱們須要用到 FontTools 庫。

先將字體文件轉換爲 xml 文件看下:

from fontTools.ttLib import TTFont

font = TTFont('bb70be69aaed960fa6ec3549342b87d82084.woff')
font.saveXML('bb70be69aaed960fa6ec3549342b87d82084.xml')

打開 xml 文件

開頭顯示的就是所有的編碼,這裏的 id 僅僅是編號而已,千萬別當成是對應的真實值。實際上,整個字體文件中,沒有任何地方是說明 EA0B 對應的真實值是啥的。

看到下面

這裏就是每一個字對應的字體信息,計算機顯示的時候,根本不須要知道這個字是啥,只須要知道哪一個像素是黑的,哪一個像素是白的就能夠了。

 

貓眼的字體文件是動態加載的,每次刷新都會變,雖然字體中定義的只有 0-9 這9個數字,可是編碼和順序都是會變的。就是說,這個字體文件中「EA0B」表明「9」,在別的文件中就不是了。

可是,有同樣是不變的,就是這個字的形狀,也就是上圖中定義的這些點。

 

咱們先隨便下載一個字體文件,命名爲 base.woff,而後利用 fontstore 網站查看編碼和實際值的對應關係,手工作成字典並保存下來。爬蟲爬取的時候,下載字體文件,根據網頁源碼中的編碼,在字體文件中找到「字形」,再循環跟 base.woff 文件中的「字形」作比較,「字形」同樣那就說明是同一個字了。在 base.woff 中找到「字形」後,獲取「字形」的編碼,而以前咱們已經手工作好了編碼跟值的映射表,由此就能夠獲得咱們實際想要的值了。

 

這裏的前提是每一個字體文件中所定義的「字形」都是同樣的(貓眼目前是這樣的,之後也許還會更改策略),若是更復雜一點,每一個字體中的「字形」都加一點點的隨機形變,那這個方法就沒有用了,只能祭出殺手鐗「OCR」了。

 

- 第一次請求將字體文件下載到本地,手動寫出映射關係
- 第二次請求網頁時只須要比較兩個文件的字形是否相同

 

複製代碼
from fontTools.ttLib import TTFont

font1 = TTFont("font1.woff")

uni_list1=font1.getGlyphOrder()[2:]     # 全部編碼

font_objs = []
for unicode in uni_list1:
    font_objs.append(font1['glyf'][unicode])

uni_list = ["4","5","8","7","0","3","9","1","2","6"]

uni_cmap = []
for i in range(len(uni_list)):
    uni_cmap.append((font_objs[i], uni_list[i])) # 將字符對象對應字符以元組的形式放在列表中

# -----------------------------------------------------------------------------------
# 模擬第二次請求得到的字體數據
font2=TTFont('font2.woff')              # 打開訪問網頁新得到的字體文件02.ttf

uni_list2=font2.getGlyphOrder()[2:]     # 全部編碼信息
print(uni_list2)
# ['uniE903', 'uniF144', 'uniEEE8', 'uniF53A', 'uniEB55', 'uniEA24', 'uniF897', 'uniEE9B', 'uniE410', 'uniF25E']

for uni2 in uni_list2:
    obj2=font2['glyf'][uni2]  #獲取編碼uni2在font2.woff中對應的對象
    for obj in uni_cmap:
        if obj[0]==obj2:        # 比較兩個字體文件的字形對象是否相同
            print(uni2,obj[1])  # 打印結果,編碼uni2和對應的數字
複製代碼

實戰演示

# -*- coding: utf-8 -*-
# @Time    : 2019/8/2 14:40
import os
import re
import requests
from fontTools.ttLib import TTFont

# 將字體轉換爲xml文件
font = TTFont('font_template.woff')
# font.saveXML('font_template.xml')  # 轉換xml文件需將此註釋打開

# 獲取字體文件的unicode編碼
uni_list = font.getGlyphOrder()[2:]

font_list = ['7','5','0','2','6','8','1','9','4','3']

# 取出每一個unicode所對應的字形對象
font_objs = []
for i in uni_list:
    font_objs.append(font["glyf"][i])

# 將每一個數字和它對應的字形對象以元組的形式放在列表中[字形對象永遠不會變]
uni_cmap = []
for i in range(len(uni_list)):
    uni_cmap.append((font_objs[i],font_list[i]))
# print(uni_cmap)

# 字體映射返回映射結果
def font_map(font_name,font_url):
    # 下載每一個頁面的字體文件
    response = requests.get(font_url).content
    with open(font_name,"wb") as fp:
        fp.write(response)
    # 打開字體文件
    font_new = TTFont(font_name)
    # 獲取全部unicode編碼信息
    uni_new_list =font_new.getGlyphOrder()[2:]

    font_lis = []
    for new_uni in uni_new_list:
        # 獲取字體對應的字型對象
        obj_uni = font_new["glyf"][new_uni]

        # 循環咱們以前保存的映射關係uni_cmap[("字形對象",數字),]
        for obj in uni_cmap:
            # 判斷新下載的字形對象與uni_cmap中的字形對象是否相同
            if obj[0] == obj_uni:
                new_uni = re.sub("uni", "&#x", new_uni, count=1) + ";"
                font_lis.append({new_uni.lower():int(obj[1])})

    # 將每次下載的字體文件刪除
    os.remove(font_name)
    return font_lis

字體映射
字體映射
# 字體替換
def font_replace(font_res,response):
    ret = response       # 這裏注意咱們在字體替換的時候必定是全部字體替換完成在將整張html頁面返回
    for font_dic in font_res:
        for key,value in font_dic.items():
            if key in ret:
                ret = ret.replace(key,str(value))
    return ret

html頁面字體替換
HTML頁面字體替換
# 字體下載[下載每一個頁面的字體文件]
    font_url = 'http:' + re.findall("<style>.*?@font-face.*?\),.*?\('(.*?)'\) format.*?</style>", res, re.S)[0]
    font_name = font_url.split("/")[-1]
匹配字體文件的正則
相關文章
相關標籤/搜索