【1x00】加密字體攻克思路
F12 打開調試模板,經過頁面分析,能夠觀察到,網站裏面凡是涉及到有數字的地方,都是顯示爲亂碼,這種狀況就是字體加密了,那麼是經過什麼手段實現字體加密的呢?python
CSS 中有一個 @font-face
規則,它容許爲網頁指定在線字體,也就是說能夠引入自定義字體,這個規則本意是用來消除對電腦字體的依賴,如今很多網站也利用這個規則來實現反爬mysql
右側能夠看到網站用的字體,其餘的都是常見的微軟雅黑,宋體等,可是有一個特殊的:fangchan-secret
,不難看出這應該就是58同城的自定義字體了
git
咱們經過控制檯看到的亂碼事實上是因爲 unicode 編碼致使,查看網頁源代碼,咱們才能看到他真正的編碼信息github
要攻克加密字體,那麼咱們確定要分析他的字體文件了,先想辦法獲得他的加密字體文件,一樣查看源代碼,在源代碼中搜索 fangchan-secret
的字體信息web
選中的藍色部分就是 base64 編碼的加密字體字符串了,咱們將其解碼成二進制編碼,寫進 .woff
的字體文件,這個過程能夠經過如下代碼實現:sql
import requests import base64 headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36' } url = 'https://wh.58.com/chuzu/' response = requests.get(url=url, headers=headers) # 匹配 base64 編碼的加密字體字符串 base64_string = response.text.split("base64,")[1].split("'")[0].strip() # 將 base64 編碼的字體字符串解碼成二進制編碼 bin_data = base64.decodebytes(base64_string.encode()) # 保存爲字體文件 with open('58font.woff', 'wb') as f: f.write(bin_data)
獲得字體文件後,咱們能夠經過 FontCreator 這個軟件來看看字體對應的編碼是什麼:數據庫
觀察咱們在網頁源代碼中看到的編碼:相似於 龤
、龒
app
對比字體文件對應的編碼:相似於 uni9FA4
、nui9F92
dom
能夠看到除了前面三個字符不同之外,後面的字符都是同樣的,只不過英文大小寫有所差別ide
如今咱們可能會想到,直接把編碼替換成對應的數字不就OK了?然而並無這麼簡單
嘗試刷新一下網頁,能夠觀察到 base64 編碼的加密字體字符串會改變,也就是說編碼和數字並非一一對應的,再次獲取幾個字體文件,經過對比就能夠看出來
能夠看到,雖然每次數字對應的編碼都不同,可是編碼老是這10個,是不變的,那麼編碼與數字之間確定存在某種對應關係,,咱們能夠將字體文件轉換爲 xml 文件來觀察其中的對應關係,改進原來的代碼便可實現轉換功能:
import requests import base64 from fontTools.ttLib import TTFont headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36' } url = 'https://wh.58.com/chuzu/' response = requests.get(url=url, headers=headers) # 匹配 base64 編碼的加密字體字符串 base64_string = response.text.split("base64,")[1].split("'")[0].strip() # 將 base64 編碼的字體字符串解碼成二進制編碼 bin_data = base64.decodebytes(base64_string.encode()) # 保存爲字體文件 with open('58font.woff', 'wb') as f: f.write(bin_data) # 獲取字體文件,將其轉換爲xml文件 font = TTFont('58font.woff') font.saveXML('58font.xml')
打開 58font.xml
文件並分析,在 <cmap>
標籤內能夠看到熟悉的相似於 0x9476
、0x958f
的編碼,其後四位字符剛好是網頁字體的加密編碼,能夠看到每個編碼後面都對應了一個 glyph
開頭的編碼
將其與 58font.woff
文件對比,能夠看到 code 爲 0x958f
這個編碼對應的是數字 3
,對應的 name 編碼是 glyph00004
咱們再次獲取一個字體文件做爲對比分析
依然是 0x958f
這個編碼,兩次對應的 name 分別是 glyph00004
和 glyph00007
,兩次對應的數字分別是 3
和 6
,那麼結論就來了,每次發送請求,code 對應的 name 會隨機發生變化,而 name 對應的數字不會發生變化,glyph00001
對應數字 0
、glyph00002
對應數字 1
,以此類推
那麼以 glyph
開頭的編碼是如何對應相應的數字的呢?在 xml 文件裏面,每一個編碼都有一個 TTGlyph
的標籤,標籤裏面是一行一行的相似於 x,y 座標的東西,這個其實就是用來繪製字體的,用 matplotlib 根據座標畫個圖,就能夠看到是一個數字
此時,咱們就知道了編碼與數字的對應關係,下一步,咱們能夠查找 xml 文件裏,編碼對應的 name 的值,也就是以 glyph
開頭的編碼,而後返回其對應的數字,再替換掉網頁源代碼裏的編碼,就能成功獲取到咱們須要的信息了!
總結一下攻克加密字體的大體思路:
分析網頁,找到對應的加密字體文件
若是引用的加密字體是一個 base64 編碼的字符串,則須要轉換成二進制並保存到 woff 字體文件中
將字體文件轉換成 xml 文件
用 FontCreator 軟件觀察字體文件,結合 xml 文件,分析其編碼與真實字體的關係
搞清楚編碼與字體的關係後,想辦法將編碼替換成正常字體
【2x00】思惟導圖
【3x00】加密字體處理模塊
【3x01】獲取字體文件並轉換爲xml文件
def get_font(page_url, page_num): response = requests.get(url=page_url, headers=headers) # 匹配 base64 編碼的加密字體字符串 base64_string = response.text.split("base64,")[1].split("'")[0].strip() # print(base64_string) # 將 base64 編碼的字體字符串解碼成二進制編碼 bin_data = base64.decodebytes(base64_string.encode()) # 保存爲字體文件 with open('58font.woff', 'wb') as f: f.write(bin_data) print('第' + str(page_num) + '次訪問網頁,字體文件保存成功!') # 獲取字體文件,將其轉換爲xml文件 font = TTFont('58font.woff') font.saveXML('58font.xml') print('已成功將字體文件轉換爲xml文件!') return response.text
由主函數傳入要發送請求的 url,利用字符串的 split()
方法,匹配 base64 編碼的加密字體字符串,利用 base64
模塊的 base64.decodebytes()
方法,將 base64 編碼的字體字符串解碼成二進制編碼並保存爲字體文件,利用 FontTools
庫,將字體文件轉換爲 xml 文件
【3x02】將加密字體編碼與真實字體進行匹配
def find_font(): # 以glyph開頭的編碼對應的數字 glyph_list = { 'glyph00001': '0', 'glyph00002': '1', 'glyph00003': '2', 'glyph00004': '3', 'glyph00005': '4', 'glyph00006': '5', 'glyph00007': '6', 'glyph00008': '7', 'glyph00009': '8', 'glyph00010': '9' } # 十個加密字體編碼 unicode_list = ['0x9476', '0x958f', '0x993c', '0x9a4b', '0x9e3a', '0x9ea3', '0x9f64', '0x9f92', '0x9fa4', '0x9fa5'] num_list = [] # 利用xpath語法匹配xml文件內容 font_data = etree.parse('./58font.xml') for unicode in unicode_list: # 依次循環查找xml文件裏code對應的name result = font_data.xpath("//cmap//map[@code='{}']/@name".format(unicode))[0] # print(result) # 循環字典的key,若是code對應的name與字典的key相同,則獲得key對應的value for key in glyph_list.keys(): if key == result: num_list.append(glyph_list[key]) print('已成功找到編碼所對應的數字!') # print(num_list) # 返回value列表 return num_list
由前面的分析,咱們知道 name 的值(即以 glyph 開頭的編碼)對應的數字是固定的,glyph00001
對應數字 0
、glyph00002
對應數字 1
,以此類推,因此能夠將其構形成爲一個字典 glyph_list
一樣將十個 code(即相似於 0x9476
的加密字體編碼)構形成一個列表
循環查找這十個 code
在 xml 文件裏對應的 name
的值,而後將 name
的值與字典文件的 key
值進行對比,若是二者值相同,則獲取這個 key
的 value
值,最終獲得的列表 num_list
,裏面的元素就是 unicode_list
列表裏面每一個加密字體的真實值
【3x03】替換掉網頁中全部的加密字體編碼
def replace_font(num, page_response): # 9476 958F 993C 9A4B 9E3A 9EA3 9F64 9F92 9FA4 9FA5 result = page_response.replace('鑶', num[0]).replace('閏', num[1]).replace('餼', num[2]).replace('驋', num[3]).replace('鸺', num[4]).replace('麣', num[5]).replace('齤', num[6]).replace('龒', num[7]).replace('龤', num[8]).replace('龥', num[9]) print('已成功將全部加密字體替換!') return result
傳入由上一步 find_font()
函數獲得的真實字體的列表,利用 replace()
方法,依次將十個加密字體編碼替換掉
【4x00】租房信息提取模塊
def parse_pages(pages): num = 0 soup = BeautifulSoup(pages, 'lxml') # 查找到包含全部租房的li標籤 all_house = soup.find_all('li', class_='house-cell') for house in all_house: # 標題 title = house.find('a', class_='strongbox').text.strip() # print(title) # 價格 price = house.find('div', class_='money').text.strip() # print(price) # 戶型和麪積 layout = house.find('p', class_='room').text.replace(' ', '') # print(layout) # 樓盤和地址 address = house.find('p', class_='infor').text.replace(' ', '').replace('\n', '') # print(address) # 若是存在經紀人 if house.find('div', class_='jjr'): agent = house.find('div', class_='jjr').text.replace(' ', '').replace('\n', '') # 若是存在品牌公寓 elif house.find('p', class_='gongyu'): agent = house.find('p', class_='gongyu').text.replace(' ', '').replace('\n', '') # 若是存在我的房源 else: agent = house.find('p', class_='geren').text.replace(' ', '').replace('\n', '') # print(agent) data = [title, price, layout, address, agent] save_to_mysql(data) num += 1 print('第' + str(num) + '條數據爬取完畢,暫停3秒!') time.sleep(3)
利用 BeautifulSoup 解析庫很容易提取到相關信息,這裏要注意的是,租房信息來源分爲三種:經紀人、品牌公寓和我的房源,這三個的元素節點也不同,所以匹配的時候要注意
【5x00】MySQL數據儲存模塊
【5x01】建立MySQL數據庫的表
def create_mysql_table(): db = pymysql.connect(host='localhost', user='root', password='000000', port=3306, db='58tc_spiders') cursor = db.cursor() sql = 'CREATE TABLE IF NOT EXISTS 58tc_data (title VARCHAR(255) NOT NULL, price VARCHAR(255) NOT NULL, layout VARCHAR(255) NOT NULL, address VARCHAR(255) NOT NULL, agent VARCHAR(255) NOT NULL)' cursor.execute(sql) db.close()
首先指定數據庫爲 58tc_spiders,須要事先使用 MySQL 語句建立,也能夠經過 MySQL Workbench 手動建立
而後使用 SQL 語句建立 一個表:58tc_data,表中包含 title、price、layout、address、agent 五個字段,類型都爲 varchar
此建立表的操做也能夠事先手動建立,手動建立後就不須要此函數了
【5x02】將數據儲存到MySQL數據庫
def save_to_mysql(data): db = pymysql.connect(host='localhost', user='root', password='000000', port=3306, db='58tc_spiders') cursor = db.cursor() sql = 'INSERT INTO 58tc_data(title, price, layout, address, agent) values(%s, %s, %s, %s, %s)' try: cursor.execute(sql, (data[0], data[1], data[2], data[3], data[4])) db.commit() except: db.rollback() db.close()
commit()
方法的做用是實現數據插入,是真正將語句提交到數據庫執行的方法,使用 try except
語句實現異常處理,若是執行失敗,則調用 rollback()
方法執行數據回滾,保證原數據不被破壞
【6x00】完整代碼
# ============================================= # --*-- coding: utf-8 --*-- # @Time : 2019-10-21 # @Author : TRHX # @Blog : www.itrhx.com # @CSDN : https://blog.csdn.net/qq_36759224 # @FileName: 58tongcheng.py # @Software: PyCharm # ============================================= import requests import time import random import base64 import pymysql from lxml import etree from bs4 import BeautifulSoup from fontTools.ttLib import TTFont headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36' } # 獲取字體文件並轉換爲xml文件 def get_font(page_url, page_num): response = requests.get(url=page_url, headers=headers) # 匹配 base64 編碼的加密字體字符串 base64_string = response.text.split("base64,")[1].split("'")[0].strip() # print(base64_string) # 將 base64 編碼的字體字符串解碼成二進制編碼 bin_data = base64.decodebytes(base64_string.encode()) # 保存爲字體文件 with open('58font.woff', 'wb') as f: f.write(bin_data) print('第' + str(page_num) + '次訪問網頁,字體文件保存成功!') # 獲取字體文件,將其轉換爲xml文件 font = TTFont('58font.woff') font.saveXML('58font.xml') print('已成功將字體文件轉換爲xml文件!') return response.text # 將加密字體編碼與真實字體進行匹配 def find_font(): # 以glyph開頭的編碼對應的數字 glyph_list = { 'glyph00001': '0', 'glyph00002': '1', 'glyph00003': '2', 'glyph00004': '3', 'glyph00005': '4', 'glyph00006': '5', 'glyph00007': '6', 'glyph00008': '7', 'glyph00009': '8', 'glyph00010': '9' } # 十個加密字體編碼 unicode_list = ['0x9476', '0x958f', '0x993c', '0x9a4b', '0x9e3a', '0x9ea3', '0x9f64', '0x9f92', '0x9fa4', '0x9fa5'] num_list = [] # 利用xpath語法匹配xml文件內容 font_data = etree.parse('./58font.xml') for unicode in unicode_list: # 依次循環查找xml文件裏code對應的name result = font_data.xpath("//cmap//map[@code='{}']/@name".format(unicode))[0] # print(result) # 循環字典的key,若是code對應的name與字典的key相同,則獲得key對應的value for key in glyph_list.keys(): if key == result: num_list.append(glyph_list[key]) print('已成功找到編碼所對應的數字!') # print(num_list) # 返回value列表 return num_list # 替換掉網頁中全部的加密字體編碼 def replace_font(num, page_response): # 9476 958F 993C 9A4B 9E3A 9EA3 9F64 9F92 9FA4 9FA5 result = page_response.replace('鑶', num[0]).replace('閏', num[1]).replace('餼', num[2]).replace('驋', num[3]).replace('鸺', num[4]).replace('麣', num[5]).replace('齤', num[6]).replace('龒', num[7]).replace('龤', num[8]).replace('龥', num[9]) print('已成功將全部加密字體替換!') return result # 提取租房信息 def parse_pages(pages): num = 0 soup = BeautifulSoup(pages, 'lxml') # 查找到包含全部租房的li標籤 all_house = soup.find_all('li', class_='house-cell') for house in all_house: # 標題 title = house.find('a', class_='strongbox').text.strip() # print(title) # 價格 price = house.find('div', class_='money').text.strip() # print(price) # 戶型和麪積 layout = house.find('p', class_='room').text.replace(' ', '') # print(layout) # 樓盤和地址 address = house.find('p', class_='infor').text.replace(' ', '').replace('\n', '') # print(address) # 若是存在經紀人 if house.find('div', class_='jjr'): agent = house.find('div', class_='jjr').text.replace(' ', '').replace('\n', '') # 若是存在品牌公寓 elif house.find('p', class_='gongyu'): agent = house.find('p', class_='gongyu').text.replace(' ', '').replace('\n', '') # 若是存在我的房源 else: agent = house.find('p', class_='geren').text.replace(' ', '').replace('\n', '') # print(agent) data = [title, price, layout, address, agent] save_to_mysql(data) num += 1 print('第' + str(num) + '條數據爬取完畢,暫停3秒!') time.sleep(3) # 建立MySQL數據庫的表:58tc_data def create_mysql_table(): db = pymysql.connect(host='localhost', user='root', password='000000', port=3306, db='58tc_spiders') cursor = db.cursor() sql = 'CREATE TABLE IF NOT EXISTS 58tc_data (title VARCHAR(255) NOT NULL, price VARCHAR(255) NOT NULL, layout VARCHAR(255) NOT NULL, address VARCHAR(255) NOT NULL, agent VARCHAR(255) NOT NULL)' cursor.execute(sql) db.close() # 將數據儲存到MySQL數據庫 def save_to_mysql(data): db = pymysql.connect(host='localhost', user='root', password='000000', port=3306, db='58tc_spiders') cursor = db.cursor() sql = 'INSERT INTO 58tc_data(title, price, layout, address, agent) values(%s, %s, %s, %s, %s)' try: cursor.execute(sql, (data[0], data[1], data[2], data[3], data[4])) db.commit() except: db.rollback() db.close() if __name__ == '__main__': create_mysql_table() print('MySQL表58tc_data建立成功!') for i in range(1, 71): url = 'https://wh.58.com/chuzu/pn' + str(i) + '/' response = get_font(url, i) num_list = find_font() pro_pages = replace_font(num_list, response) parse_pages(pro_pages) print('第' + str(i) + '頁數據爬取完畢!') time.sleep(random.randint(3, 60)) print('全部數據爬取完畢!')
【7x00】數據截圖