日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > python >内容正文

python

Python3 爬虫实战 — 58同城武汉出租房【加密字体对抗】

發布時間:2023/12/10 python 43 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Python3 爬虫实战 — 58同城武汉出租房【加密字体对抗】 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

  • 爬取時間:2019-10-21
  • 爬取難度:★★★☆☆☆
  • 請求鏈接:https://wh.58.com/chuzu/
  • 爬取目標:58同城武漢出租房的所有信息
  • 涉及知識:網站加密字體的攻克、請求庫 requests、解析庫 Beautiful Soup、數據庫 MySQL 的操作
  • 完整代碼:https://github.com/TRHX/Python3-Spider-Practice/tree/master/FightAgainstSpider/58tongcheng
  • 其他爬蟲實戰代碼合集(持續更新):https://github.com/TRHX/Python3-Spider-Practice
  • 爬蟲實戰專欄(持續更新):https://itrhx.blog.csdn.net/article/category/9351278

文章目錄

  • 【1x00】加密字體攻克思路
  • 【2x00】思維導圖
  • 【3x00】加密字體處理模塊
    • 【3x01】獲取字體文件并轉換為xml文件
    • 【3x02】將加密字體編碼與真實字體進行匹配
    • 【3x03】替換掉網頁中所有的加密字體編碼
  • 【4x00】租房信息提取模塊
  • 【5x00】MySQL數據儲存模塊
    • 【5x01】創建MySQL數據庫的表
    • 【5x02】將數據儲存到MySQL數據庫
  • 【6x00】完整代碼
  • 【7x00】數據截圖


【1x00】加密字體攻克思路

F12 打開調試模板,通過頁面分析,可以觀察到,網站里面凡是涉及到有數字的地方,都是顯示為亂碼,這種情況就是字體加密了,那么是通過什么手段實現字體加密的呢?

CSS 中有一個 @font-face 規則,它允許為網頁指定在線字體,也就是說可以引入自定義字體,這個規則本意是用來消除對電腦字體的依賴,現在不少網站也利用這個規則來實現反爬

右側可以看到網站用的字體,其他的都是常見的微軟雅黑,宋體等,但是有一個特殊的:fangchan-secret ,不難看出這應該就是58同城的自定義字體了

我們通過控制臺看到的亂碼事實上是由于 unicode 編碼導致,查看網頁源代碼,我們才能看到他真正的編碼信息

要攻克加密字體,那么我們肯定要分析他的字體文件了,先想辦法得到他的加密字體文件,同樣查看源代碼,在源代碼中搜索 fangchan-secret 的字體信息

選中的藍色部分就是 base64 編碼的加密字體字符串了,我們將其解碼成二進制編碼,寫進 .woff 的字體文件,這個過程可以通過以下代碼實現:

import requests import base64headers = {'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 這個軟件來看看字體對應的編碼是什么:

觀察我們在網頁源代碼中看到的編碼:類似于 龤、龒

對比字體文件對應的編碼:類似于 uni9FA4、nui9F92

可以看到除了前面三個字符不一樣以外,后面的字符都是一樣的,只不過英文大小寫有所差異

現在我們可能會想到,直接把編碼替換成對應的數字不就OK了?然而并沒有這么簡單

嘗試刷新一下網頁,可以觀察到 base64 編碼的加密字體字符串會改變,也就是說編碼和數字并不是一一對應的,再次獲取幾個字體文件,通過對比就可以看出來

可以看到,雖然每次數字對應的編碼都不一樣,但是編碼總是這10個,是不變的,那么編碼與數字之間肯定存在某種對應關系,,我們可以將字體文件轉換為 xml 文件來觀察其中的對應關系,改進原來的代碼即可實現轉換功能:

import requests import base64 from fontTools.ttLib import TTFontheaders = {'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對應的nameresult = font_data.xpath("//cmap//map[@code='{}']/@name".format(unicode))[0]# print(result)# 循環字典的key,如果code對應的name與字典的key相同,則得到key對應的valuefor 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 9FA5result = page_response.replace('&#x9476;', num[0]).replace('&#x958f;', num[1]).replace('&#x993c;', num[2]).replace('&#x9a4b;', num[3]).replace('&#x9e3a;', num[4]).replace('&#x9ea3;', num[5]).replace('&#x9f64;', num[6]).replace('&#x9f92;', num[7]).replace('&#x9fa4;', num[8]).replace('&#x9fa5;', num[9])print('已成功將所有加密字體替換!')return result

傳入由上一步 find_font() 函數得到的真實字體的列表,利用 replace() 方法,依次將十個加密字體編碼替換掉


【4x00】租房信息提取模塊

def parse_pages(pages):num = 0soup = 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 += 1print('第' + 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 TTFontheaders = {'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對應的nameresult = font_data.xpath("//cmap//map[@code='{}']/@name".format(unicode))[0]# print(result)# 循環字典的key,如果code對應的name與字典的key相同,則得到key對應的valuefor 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 9FA5result = page_response.replace('&#x9476;', num[0]).replace('&#x958f;', num[1]).replace('&#x993c;', num[2]).replace('&#x9a4b;', num[3]).replace('&#x9e3a;', num[4]).replace('&#x9ea3;', num[5]).replace('&#x9f64;', num[6]).replace('&#x9f92;', num[7]).replace('&#x9fa4;', num[8]).replace('&#x9fa5;', num[9])print('已成功將所有加密字體替換!')return result# 提取租房信息 def parse_pages(pages):num = 0soup = 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 += 1print('第' + 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】數據截圖

總結

以上是生活随笔為你收集整理的Python3 爬虫实战 — 58同城武汉出租房【加密字体对抗】的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。