【Python爬蟲】:破解網站字體加密和反反爬蟲


前言:字體反爬,也是一種常見的反爬技術,例如58同城,貓眼電影票房,汽車之家,天眼查,實習僧等網站。這些網站采用了自定義的字體文件,在瀏覽器上正常顯示,但是爬蟲抓取下來的數據要么就是亂碼,要么就是變成其他字符,是因為他們采用自定義字體文件,通過在線加載來引用樣式,這是CSS3的新特性,通過 CSS3,web 設計師可以使用他們喜歡的任意字體 ,然后因為爬蟲不會主動加載在線的字體,

字體加密一般是網頁修改了默認的字符編碼集,在網頁上加載他們自己定義的字體文件作為字體的樣式,可以正確地顯示數字,但是在源碼上同樣的二進制數由於未加載自定義的字體文件就由計算機默認編碼成了亂碼。

目標

目標:我們今天來學習爬取58同城的租房信息,獲取房源信息。

數據爬取

我們先按照前面學的爬蟲基本知識,拿起鍵盤直接開干(無經驗不知道字體反爬是啥玩意),一直在用xpath進行解析,都忘記了BeautifulSoup提取了,這里來用這個提取,回顧回顧。

import requests from bs4 import BeautifulSoup url = 'https://cs.58.com/chuzu/?PGTID=0d100000-0019-e310-48ff-c90994a335ae&ClickID=4' headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.108 Safari/537.36' } response = requests.get(url,headers=headers) html_text = response.text bs = BeautifulSoup(html_text, 'lxml') # 獲取房源列表信息,通過css選擇器來 lis = bs.select('li.house-cell') # 獲取每個li下的信息 for li in lis: title = li.select('h2 a')[0].stripped_strings # stripped_strings獲取某個標簽下的子孫非標簽字符串,會去掉空白字符。返回來的是個生成器 room = li.select('div.des p')[0].stripped_strings money = li.select('.money b')[0].string # 獲取某個標簽下的非標簽字符串。返回來的是個字符串。 print(list(title)[0], list(room)[0], money)

輸出的結果:

顯示亂碼,在頁面上看也是亂碼

我們右擊選擇查看網頁源代碼

看起來是unicode 編碼導致的,這種就是對字體進行了加密了,通用解決辦法是找到字體文件,分析文件中的映射關系,一般來說,字體文件都是作為樣式加在加密字體的部位,所以我們在html頭部里面找相關樣式,找了頭部信息字體樣式font-face,CSS中的@font-face,它允許網頁開發者為其網頁指定在線字體。

我們ctrl+f搜索@font-face

發現58同城的頁面中的字體文件是經過base64加密之后放在js里面的,一大串字符串,從base64后面開始一直到后面format前面的括號中的內容,應該是字體文件的內容。是經過了base64編碼后的形式,我們把其中加密的部分取出,通過正則表達式將其中的內容取出來,然后用base64解碼后再保存成本地ttf文件(ttf是字體的一種類型)。

關於字體

fontTools操作相關

這里我們使用到一個模塊fontTools,它是用來操作字體的庫,用於將woff或ttf這種字體文件轉化成XML文件。

1.我們可以直接使用pip進行安裝:

pip install fontTools

2.加載字體文件:

font = TTFont('58.woff')

3.轉為xml文件:

font.saveXML('58.xml')

4.各節點名稱:

font.keys()

5.按序獲取GlyphOrder節點name值:

font.getGlyphOrder()  font['cmap'].tables[0].ttFont.getGlyphOrder()

6.獲取cmap節點code與name值映射:

font.getBestCmap()

7.獲取字體坐標信息:

font['glyf'][i].coordinates

8.獲取坐標的0或1:

font['glyf'][i].flags **注:** 0表示弧形區域 1表示矩形

字體基礎與XML

一個字體由數個表(ta­ble)構成,字體的信息儲存在表中。1、一個最基本的字體文件一定會包含以下的表:

  • cmap: Char­ac­ter to glyph map­ping unicode跟 Name的映射關系
  • head: Font header 字體全局信息
  • hhea: Hor­i­zon­tal header 定義了水平header
  • hmtx: Hor­i­zon­tal met­rics 定義了水平metric
  • maxp: Max­i­mum pro­file 用於為字體分配內存
  • name: Nam­ing ta­ble 定義字體名稱、風格名以及版權說明等
  • glyf: 字形數據即輪廓定義和調整指令
  • OS2: OS2 and Win­dows spe­cific met­rics
  • post: Post­Script in­for­ma­tion

我們將字體解密並保存到本地看看:

import requests from fontTools.ttLib import TTFont import re import base64 url = 'https://cs.58.com/chuzu/?PGTID=0d100000-0019-e310-48ff-c90994a335ae&ClickID=4' headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.108 Safari/537.36' } response = requests.get(url,headers=headers) html_text = response.text # print(html_text) pattern = r"base64,(.*?)'" # 提取加密信息 result = re.findall(pattern, html_text) # 返回列表 if result: # 避免有的頁面沒有使用加密 print(type(result), len(result)) base64str = result[0] fontfile_content = base64.b64decode(base64str) # 通過base64編碼的數據進行解碼,輸出二進制 with open('58.ttf', 'wb') as f: # 生成字體文件 f.write(fontfile_content) font = TTFont('58.ttf') # 加載字體文件 font.saveXML('58.xml') # 轉換成xml文件 else: print('沒有內容') base64str = ""

生成的字體庫文件58.ttf。

生成的xml文件:

分析xml文件

我們來分析xml文件中映射關系。
點開GlyphOrder標簽,可以看到Id和name。這里id僅表示序號而已,而不是對應具體的數字:

點開glyf標簽,看到的是name和一些坐標點,這些座標點就是描繪字體形狀的,這里不需要關注這些坐標點。

點開cmap標簽,是編碼和name的對應關系:

這里將字體文件導入到 http://fontstore.baidu.com/static/editor/index.html 網頁將其打開,顯示如下:

網頁源碼中顯示的  跟這里顯示的是不是有點像?事實上確實如此,去掉開頭的 &#x 和結尾的 ; 后,剩余的4個16進制顯示的數字加上 uni 就是字體文件中的編碼。所以對應的就是數字“6”,按照此就對應到glyph00007從這2張圖我們可以發現,glyph00001對應的是數字0,glyph00002對應的是數字1,以此類推……glyph00010對應的是數字9。

用代碼來獲取編碼和name的對應關系:

from fontTools.ttLib import TTFont font = TTFont('58.ttf') # 打開本地的ttf文件 font.saveXML('58.xml') # 轉換為xml文件 bestcmap = font['cmap'].getBestCmap() # 獲取cmap節點code與name值映射 print(bestcmap)

輸出:

{38006: 'glyph00010', 38287: 'glyph00006', 39228: 'glyph00007', 39499: 'glyph00005', 40506: 'glyph00009', 40611: 'glyph00002', 40804: 'glyph00008', 40850: 'glyph00003', 40868: 'glyph00001', 40869: 'glyph00004'}

輸出的是一個字典,key是編碼的int型,我們要將其轉換為我們在xml看到的16進制一樣以及與具體的數字映射關系:

 for key,value in bestcmap.items(): key = hex(key) # 10進制轉16進制 value = int(re.search(r'(\d+)', value).group()) -1 # 通過上面分析得出glyph00001對應的是數字0依次類推。 print(key,value)

輸出結果:

0x9476 6 0x958f 5 0x993c 4 0x9a4b 3 0x9e3a 7 0x9ea3 2 0x9f64 9 0x9f92 1 0x9fa4 0 0x9fa5 8

現在就可以把頁面上的自定義字體替換成正常字體,再解析了,全部代碼如下:

import requests from bs4 import BeautifulSoup from fontTools.ttLib import TTFont import re import base64 import io def base46_str(html_text): pattern = r"base64,(.*?)'" # 提取加密部分 result = re.findall(pattern, html_text) # 返回列表 if result: # 避免有的頁面沒有使用加密 # print(type(result), len(result)) base64str = result[0] bin_data = base64.b64decode(base64str) # 通過base64編碼的數據進行解碼,輸出二進制 # # print(fontfile_content) # with open('58.ttf', 'wb') as f: # f.write(bin_data) # font = TTFont('58.ttf') # 打開本地的ttf文件 # font.saveXML('58.xml') # bestcmap = font['cmap'].getBestCmap() # print(bestcmap) fonts = TTFont(io.BytesIO(bin_data)) # BytesIO實現了在內存中讀寫bytes,提高性能 bestcmap = fonts['cmap'].getBestCmap() # print(bestcmap) # 字典 # for key,value in bestcmap.items(): # key = hex(key) # 10進制轉16進制 # value = int(re.search(r'(\d+)', value).group()) -1 # print(key,value) # 使用字典推導式 cmap = {hex(key).replace('0x', '&#x') + ';' : int(re.search(r'(\d+)', value).group(1)) - 1 for key, value in bestcmap.items()} # print(cmap) for k,v in cmap.items(): html_text = html_text.replace(k, str(v)) return html_text else: print('沒有內容') base64str = "" return html_text def parse_html(html_text): bs = BeautifulSoup(html_text, 'lxml') # 獲取房源列表信息,通過css選擇器來 lis = bs.select('li.house-cell') # 獲取每個li下的信息 for li in lis: href = li.select('h2 a')[0]['href'] title = li.select('h2 a')[0].stripped_strings # stripped_strings獲取某個標簽下的子孫非標簽字符串,會去掉空白字符。返回來的是個生成器 room = li.select('div.des p')[0].stripped_strings money = li.select('.money')[0].get_text().replace('\n','') # 獲取某個標簽下的非標簽字符串。返回來的是個字符串。 print(href, list(title)[0], list(room)[0], money) if __name__ == '__main__': url = 'https://cs.58.com/chuzu/?PGTID=0d100000-0019-e310-48ff-c90994a335ae&ClickID=4' headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.108 Safari/537.36' } response = requests.get(url, headers=headers) html_text = response.text html_text = base46_str(html_text) parse_html(html_text)

輸出結果:

至此,58同城字體相關差不多了。

拓展

上面只是簡單的字體反爬,像汽車之家,貓眼電影,我們可以去挑戰一下。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM