如何破解字型反爬機制
這幾天爬取58租房資訊的時候意外發現了它是一個字型反爬的網站,所謂的字型反爬就是網站將一些關鍵字替換為網站自己的字型,這樣在網頁上字型會正常顯示,但是當爬取下來的時候,經過字型加密的字元都是亂碼的,根本無法檢視
如圖所示:
可以看到,2390元/月在頁面上是正常顯示的,但是,當我們開啟檢視器檢視的時候......
好端端的2390就變成了不知道什麼字元.......
這就是網站使用了字型反爬機制,網站有自己的一套字型,只有在它的頁面上才會正常顯示,否則就是一串亂碼,毫無價值。那麼遇到這種問題該怎麼解決,在經歷幾天的摸索之後終於將正確的資訊抓取了下來。
首先,我們檢視網頁的原始碼,就是下面這樣的
這是網頁原始碼中一串base64的字串,它就是網站的字型檔案,很難想象一串base64的字串就是它的字型檔案。
我們將這一串base64的字串複製下來,將它解碼並儲存成一個字型檔案
import base64 font_face = "AAEAAAALAIAAAwAwR1NVQiCLJXoAAAE4AAAAVE9TLzL4XQjtAAABjAAAAFZjbWFwq8N/ZAAAAhAAAAIuZ2x5ZuWIN0cAAARYAAADdGhlYWQTcnjtAAAA4AAAADZoaGVhCtADIwAAALwAAAAkaG10eC7qAAAAAAHkAAAALGxvY2ED7gSyAAAEQAAAABhtYXhwARgANgAAARgAAAAgbmFtZTd6VP8AAAfMAAACanBvc3QFRAYqAAAKOAAAAEUAAQAABmb+ZgAABLEAAAAABGgAAQAAAAAAAAAAAAAAAAAAAAsAAQAAAAEAAOv6p7JfDzz1AAsIAAAAAADX9ZbuAAAAANf1lu4AAP/mBGgGLgAAAAgAAgAAAAAAAAABAAAACwAqAAMAAAAAAAIAAAAKAAoAAAD/AAAAAAAAAAEAAAAKADAAPgACREZMVAAObGF0bgAaAAQAAAAAAAAAAQAAAAQAAAAAAAAAAQAAAAFsaWdhAAgAAAABAAAAAQAEAAQAAAABAAgAAQAGAAAAAQAAAAEERAGQAAUAAAUTBZkAAAEeBRMFmQAAA9cAZAIQAAACAAUDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFBmRWQAQJR2n6UGZv5mALgGZgGaAAAAAQAAAAAAAAAAAAAEsQAABLEAAASxAAAEsQAABLEAAASxAAAEsQAABLEAAASxAAAEsQA" # 太長沒複製完...... b = base64.b64decode(font_face) with open('58.ttf','wb') as f: f.write(b)
這裡我們將它儲存成一個ttf的字型檔案,然後使用fontCreator將這個字型檔案開啟(fontCreator需要自行下載,直接百度就能下載類,非常簡單)
開啟之後的效果
我們再來看看網頁上的原始碼......
小夥伴們是不是會驚奇的發現,網頁原始碼上被替換掉的數字,使用fontCreator都能找到與之對應的原本的數字,那麼立刻就能想到的一個方法就是將這些網頁中的編碼與原本的值對應成一個字典,只要抓取到了字典中存在的值就將其替換成本來的值,但是(注意我這裡使用了但是)......
同樣的,這也是58的一個字型檔案,但是解析出來編碼與對應的數字與上一次解析的完全不一樣,這倒不是因為fontCreator解析出錯,而是因為58這個網站友好幾套字型檔案,它的每一頁的資料使用的都是隨機的字型檔案,當你解析了第一頁字型的對應關係,拿小本本美滋滋的將對應關係記下來,但是點選到第二頁的時候,發現關係又完全對不上,是不是很氣。而且又不可能將每一頁的對應關係都用本子記錄下來。這時候就需要另一個工具,python的第三方庫fontTools,直接pip安裝就行。
在進行解密之前,先將原先的字型檔案儲存成一個xml檔案。
from fontTools.ttLib import TTFont
font = TTFont('58.ttf')
font.saveXML('test.xml')
開啟這個xml看看到底是啥
看不懂......
這個就好像有點懂了,code的值不是網頁上的數字被替換的字串嗎!!!
這裡的name屬性的值實際對應的就是網頁上的數字,下面的程式碼可以幫助我們檢視對應關係
from fontTools.ttLib import TTFont
font = TTFont('58.ttf')
# font.saveXML('test.xml')
print(font.keys())
a = font['cmap'].tables[2].ttFont.getGlyphOrder()
b = font['cmap'].tables[2].ttFont.getReverseGlyphMap()
c = font['cmap'].tables[2].ttFont.tables['cmap'].tables[1].cmap
print("ppp ::::: ",a)
print("ddd ::::: ",b)
print("ddd ::::: ",c)
輸出的結果
這裡最後一行輸出的就是網頁上顯示的字串與camp標籤中的name值的對應關係。那麼網頁上顯示的0x958f這種字串究竟是什麼意思呢,它其實是一個十六進位制的數字,將這個十六進位制的數字轉換成十進位制(int("0x985f",16)),得到的值就是最後一行輸出的鍵,那麼這個鍵對應的值就是第二行輸出的鍵,第二行的值就是本來的資料,這樣一來,在抓取每一頁之前,先抓取到它這一頁的字型檔案,進行分析,得到對應關係,不就能獲取到原始的資料了嘛。
ps:需要注意的是,使用fontCreator解析時發現數字對應關係前面有一個空值,所以實際解析到的對應的數字需要減1才能得到正確的值。
程式碼思路:1.在爬取每一頁資料之前,先獲取到原始碼中的base64的字串,解碼,儲存成字型檔案2.生成對應關係的字典3.抓取到頁面的亂碼文字,解碼成16進位制的數字3.將十六進位制的數字轉為十進位制,判斷字典中是否有這個鍵,如果有,則解析為原本的數字,如果沒有,則說明這個文字沒有進行機密處理,儲存原來的文字即可。4.完成全部加密文字的替換得到正確的資料。
實際程式碼:
import re
import lxml.html
import base64
from fontTools.ttLib import TTFont
import requests
import random
import sqlite3
db = sqlite3.connect("58.db")
cursor = db.cursor()
UA = [
"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0.1) Gecko/20100101 Firefox/4.0.1",
"Mozilla/5.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox/4.0.1"
]
headers = {
"User-Agent":random.choice(UA)
}
def resp(i):
base_url = "https://tj.58.com/pinpaigongyu/pn/{}/"
response = requests.get(base_url.format(i), headers=headers)
print("正在下載:",response.url)
return response
def get_base64_str(response):
base_font = re.compile("base64,(.*?)\'")
base64_str = re.search(base_font, response.text).group().split(',')[1].split('\'')[0]
return base64_str
def make_font_file(base64_str):
b = base64.b64decode(base64_str)
with open("58.ttf","wb") as f:
f.write(b)
def make_dict():
font = TTFont('58.ttf')
b = font['cmap'].tables[2].ttFont.getReverseGlyphMap() # 編碼對應的數字
c = font['cmap'].tables[2].ttFont.tables['cmap'].tables[1].cmap # 頁面的十六進位制數對應的編碼
return b, c
def parse_title(text):
s = ""
title_re = re.compile("\s")
html = lxml.html.fromstring(text)
title = html.xpath('//div[@class="des strongbox"]/h2/text()')[0]
title = re.sub(title_re,'',title)
for i in title:
encode_str = str(i.encode("unicode-escape")).split(r'\\u')[-1].replace('\'','').replace(r'b(','').strip()
num, code = make_dict()
if len(encode_str) != 4:
i = i
elif int(encode_str,16) not in code:
i = i
else:
i = str(num[code[int(encode_str,16)]] - 1)
s += i
return s
def parse_price(text):
s = ""
html = lxml.html.fromstring(text)
price_code = html.xpath('//span[@class="strongbox"]/b/text()')[0]
price_code = price_code.strip().replace('\r\n','').replace(' ','')
price_encode_str = str(price_code.encode("unicode-escape")).split('\'')[1].split('-')
if len(price_encode_str) > 1:
s1 = ""
s2 = ""
encode_list1 = price_encode_str[0].split(r"\\u")[1:]
encode_list2 = price_encode_str[1].split(r"\\u")[1:]
for i in encode_list1:
price = int(i,16)
num, code = make_dict()
s1 += str(num[code[price]] - 1)
for i in encode_list2:
price = int(i,16)
num, code = make_dict()
s2 += str(num[code[price]] - 1)
s = s1 + '-' + s2
else:
str_list = price_encode_str[0].split(r'\\u')[1:]
for i in str_list:
price = int(i,16)
num, code = make_dict()
s += str(num[code[price]]-1)
return s
def parse_room(text):
s = ""
html = lxml.html.fromstring(text)
p_rooms = html.xpath('//p[@class="room"]/text()')[0]
room_re = re.compile('[\s]')
room_re1 = re.compile(r'[m²]')
room_re2 = re.compile(r'/')
rooms = re.sub(room_re,'',p_rooms)
rooms = re.sub(room_re1,"平米",rooms)
rooms = re.sub(room_re2,"至",rooms)
for i in rooms:
encode_str = str(i.encode("unicode-escape")).split(r'\\u')[-1].replace('\'', '').replace(r'b/','').strip()
# print(encode_str)
num, code = make_dict()
if len(encode_str) != 4:
i = i
elif int(encode_str,16) not in code:
i = i
else:
i = str(num[code[int(encode_str,16)]] - 1)
s += i
return s
def parse_dist(text):
s = ""
html = lxml.html.fromstring(text)
p_dist_re = re.compile('\skm')
try:
p_dist = html.xpath('//p[@class="dist"]//text()')[1]
p_dist = ''.join(p_dist).replace(' ','')
p_dist = re.sub(p_dist_re,'千米',p_dist)
for i in p_dist:
encode_str = str(i.encode("unicode-escape")).split(r'\\u')[-1].replace('\'', '').replace(r'\\r','').replace(r'\\n','').replace(r'b.','').strip()
num, code = make_dict()
if len(encode_str) != 4:
i = i
elif int(encode_str, 16) not in code:
i = i
else:
i = str(num[code[int(encode_str, 16)]] - 1)
s += i
dist = s
except:
dist = "暫無"
return dist
def short_rent(text):
html = lxml.html.fromstring(text)
try:
rent = html.xpath('//p[@class="room"]/b/text()')[0]
except:
rent = "不可短租"
return rent
def parse_li(response):
li_re = re.compile('<li logr([\s\S]*?)</li>')
li_list = re.findall(li_re,response.text)
return li_list
def parse_target(text):
html = lxml.html.fromstring(text)
try:
target = html.xpath('//p[@class="spec"]/span/text()')
target = ','.join(target)
except:
target = "暫無"
return target
if __name__ == '__main__':
for i in range(1,171):
response = resp(i)
base64_str = get_base64_str(response)
make_font_file(base64_str)
make_dict()
li_list = parse_li(response)
for i in li_list:
title = parse_title(i)
price = parse_price(i)
room = parse_room(i)
dist = parse_dist(i)
rent = short_rent(i)
target = parse_target(i)
city = "天津"
cursor.execute("insert into home(title, price, room, dist, rent,target, city) values (?,?,?,?,?,?,?)",[title,price,room,dist,rent,target,city])
db.commit()
由於我的正則表示式功底有點差,所以這裡的正則表示式都用的是比較low的..........
但是資料爬下來了,而且沒有問題
一次愉快的破解字型反爬機制就到此結束了