溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Python如何爬取58同城租房數(shù)據(jù)并破解字體加密

發(fā)布時(shí)間:2021-10-26 09:55:17 來源:億速云 閱讀:154 作者:柒染 欄目:大數(shù)據(jù)

Python如何爬取58同城租房數(shù)據(jù)并破解字體加密,很多新手對(duì)此不是很清楚,為了幫助大家解決這個(gè)難題,下面小編將為大家詳細(xì)講解,有這方面需求的人可以來學(xué)習(xí)下,希望你能有所收獲。

【1】加密字體攻克思路

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

CSS 中有一個(gè) @font-face 規(guī)則,它允許為網(wǎng)頁指定在線字體,也就是說可以引入自定義字體,這個(gè)規(guī)則本意是用來消除對(duì)電腦字體的依賴,現(xiàn)在不少網(wǎng)站也利用這個(gè)規(guī)則來實(shí)現(xiàn)反爬

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

Python如何爬取58同城租房數(shù)據(jù)并破解字體加密

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

選中的藍(lán)色部分就是 base64 編碼的加密字體字符串了,我們將其解碼成二進(jìn)制編碼,寫進(jìn) .woff 的字體文件,這個(gè)過程可以通過以下代碼實(shí)現(xiàn):

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 編碼的字體字符串解碼成二進(jìn)制編碼
bin_data = base64.decodebytes(base64_string.encode())
# 保存為字體文件
with open('58font.woff', 'wb') as f:
    f.write(bin_data)

得到字體文件后,我們可以通過 FontCreator 這個(gè)軟件來看看字體對(duì)應(yīng)的編碼是什么:

Python如何爬取58同城租房數(shù)據(jù)并破解字體加密

觀察我們?cè)诰W(wǎng)頁源代碼中看到的編碼:類似于 龤、龒

對(duì)比字體文件對(duì)應(yīng)的編碼:類似于 uni9FA4、nui9F92

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

現(xiàn)在我們可能會(huì)想到,直接把編碼替換成對(duì)應(yīng)的數(shù)字不就OK了?然而并沒有這么簡單

嘗試刷新一下網(wǎng)頁,可以觀察到 base64 編碼的加密字體字符串會(huì)改變,也就是說編碼和數(shù)字并不是一一對(duì)應(yīng)的,再次獲取幾個(gè)字體文件,通過對(duì)比就可以看出來

可以看到,雖然每次數(shù)字對(duì)應(yīng)的編碼都不一樣,但是編碼總是這10個(gè),是不變的,那么編碼與數(shù)字之間肯定存在某種對(duì)應(yīng)關(guān)系,,我們可以將字體文件轉(zhuǎn)換為 xml 文件來觀察其中的對(duì)應(yīng)關(guān)系,改進(jìn)原來的代碼即可實(shí)現(xiàn)轉(zhuǎn)換功能:

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 編碼的字體字符串解碼成二進(jìn)制編碼
bin_data = base64.decodebytes(base64_string.encode())
# 保存為字體文件
with open('58font.woff', 'wb') as f:
    f.write(bin_data)
# 獲取字體文件,將其轉(zhuǎn)換為xml文件
font = TTFont('58font.woff')
font.saveXML('58font.xml')

打開 58font.xml 文件并分析,在<cmap>標(biāo)簽內(nèi)可以看到熟悉的類似于 0x9476、0x958f 的編碼,其后四位字符恰好是網(wǎng)頁字體的加密編碼,可以看到每一個(gè)編碼后面都對(duì)應(yīng)了一個(gè) glyph 開頭的編碼

將其與 58font.woff 文件對(duì)比,可以看到 code 為 0x958f 這個(gè)編碼對(duì)應(yīng)的是數(shù)字 3,對(duì)應(yīng)的 name 編碼是 glyph00004

Python如何爬取58同城租房數(shù)據(jù)并破解字體加密

我們?cè)俅潍@取一個(gè)字體文件作為對(duì)比分析

Python如何爬取58同城租房數(shù)據(jù)并破解字體加密

此時(shí),我們就知道了編碼與數(shù)字的對(duì)應(yīng)關(guān)系,下一步,我們可以查找 xml 文件里,編碼對(duì)應(yīng)的 name 的值,也就是以 glyph 開頭的編碼,然后返回其對(duì)應(yīng)的數(shù)字,再替換掉網(wǎng)頁源代碼里的編碼,就能成功獲取到我們需要的信息了!

總結(jié)一下攻克加密字體的大致思路:

  • 分析網(wǎng)頁,找到對(duì)應(yīng)的加密字體文件

  • 如果引用的加密字體是一個(gè) base64 編碼的字符串,則需要轉(zhuǎn)換成二進(jìn)制并保存到 woff 字體文件中

  • 將字體文件轉(zhuǎn)換成 xml 文件

  • 用 FontCreator 軟件觀察字體文件,結(jié)合 xml 文件,分析其編碼與真實(shí)字體的關(guān)系

  • 搞清楚編碼與字體的關(guān)系后,想辦法將編碼替換成正常字體

【2】思維導(dǎo)圖

【3】加密字體處理模塊

【3.1】獲取字體文件并轉(zhuǎn)換為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 編碼的字體字符串解碼成二進(jìn)制編碼
    bin_data = base64.decodebytes(base64_string.encode())
    # 保存為字體文件
    with open('58font.woff', 'wb') as f:
        f.write(bin_data)
    print('第' + str(page_num) + '次訪問網(wǎng)頁,字體文件保存成功!')
    # 獲取字體文件,將其轉(zhuǎn)換為xml文件
    font = TTFont('58font.woff')
    font.saveXML('58font.xml')
    print('已成功將字體文件轉(zhuǎn)換為xml文件!')
    return response.text

由主函數(shù)傳入要發(fā)送請(qǐng)求的 url,利用字符串的 split() 方法,匹配 base64 編碼的加密字體字符串,利用 base64 模塊的 base64.decodebytes() 方法,將 base64 編碼的字體字符串解碼成二進(jìn)制編碼并保存為字體文件,利用 FontTools 庫,將字體文件轉(zhuǎn)換為 xml 文件

【3.2】將加密字體編碼與真實(shí)字體進(jìn)行匹配

def find_font():
    # 以glyph開頭的編碼對(duì)應(yīng)的數(shù)字
    glyph_list = {
        'glyph00001': '0',
        'glyph00002': '1',
        'glyph00003': '2',
        'glyph00004': '3',
        'glyph00005': '4',
        'glyph00006': '5',
        'glyph00007': '6',
        'glyph00008': '7',
        'glyph00009': '8',
        'glyph00010': '9'
    }
    # 十個(gè)加密字體編碼
    unicode_list = ['0x9476', '0x958f', '0x993c', '0x9a4b', '0x9e3a', '0x9ea3', '0x9f64', '0x9f92', '0x9fa4', '0x9fa5']
    num_list = []
    # 利用xpath語法匹配xml文件內(nèi)容
    font_data = etree.parse('./58font.xml')
    for unicode in unicode_list:
        # 依次循環(huán)查找xml文件里code對(duì)應(yīng)的name
        result = font_data.xpath("//cmap//map[@code='{}']/@name".format(unicode))[0]
        # print(result)
        # 循環(huán)字典的key,如果code對(duì)應(yīng)的name與字典的key相同,則得到key對(duì)應(yīng)的value
        for key in glyph_list.keys():
            if key == result:
                num_list.append(glyph_list[key])
    print('已成功找到編碼所對(duì)應(yīng)的數(shù)字!')
    # print(num_list)
    # 返回value列表
    return num_list

由前面的分析,我們知道 name 的值(即以 glyph 開頭的編碼)對(duì)應(yīng)的數(shù)字是固定的,glyph00001 對(duì)應(yīng)數(shù)字 0、glyph00002 對(duì)應(yīng)數(shù)字 1,以此類推,所以可以將其構(gòu)造成為一個(gè)字典 glyph_list

同樣將十個(gè) code(即類似于 0x9476 的加密字體編碼)構(gòu)造成一個(gè)列表

循環(huán)查找這十個(gè) code 在 xml 文件里對(duì)應(yīng)的 name 的值,然后將 name 的值與字典文件的 key 值進(jìn)行對(duì)比,如果兩者值相同,則獲取這個(gè) key 的 value 值,最終得到的列表 num_list,里面的元素就是 unicode_list 列表里面每個(gè)加密字體的真實(shí)值

【3.3】替換掉網(wǎng)頁中所有的加密字體編碼

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() 函數(shù)得到的真實(shí)字體的列表,利用 replace() 方法,依次將十個(gè)加密字體編碼替換掉

【4】租房信息提取模塊

def parse_pages(pages):
    num = 0
    soup = BeautifulSoup(pages, 'lxml')
    # 查找到包含所有租房的li標(biāo)簽
    all_house = soup.find_all('li', class_='house-cell')
    for house in all_house:
        # 標(biāo)題
        title = house.find('a', class_='strongbox').text.strip()
        # print(title)

        # 價(jià)格
        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)

        # 如果存在經(jīng)紀(jì)人
        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', '')
        # 如果存在個(gè)人房源
        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) + '條數(shù)據(jù)爬取完畢,暫停3秒!')
        time.sleep(3)

利用 BeautifulSoup 解析庫很容易提取到相關(guān)信息,這里要注意的是,租房信息來源分為三種:經(jīng)紀(jì)人、品牌公寓和個(gè)人房源,這三個(gè)的元素節(jié)點(diǎn)也不一樣,因此匹配的時(shí)候要注意

Python如何爬取58同城租房數(shù)據(jù)并破解字體加密

【5】MySQL數(shù)據(jù)儲(chǔ)存模塊

【5.1】創(chuàng)建MySQL數(shù)據(jù)庫的表

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()

首先指定數(shù)據(jù)庫為 58tc_spiders,需要事先使用 MySQL 語句創(chuàng)建,也可以通過 MySQL Workbench 手動(dòng)創(chuàng)建

然后使用 SQL 語句創(chuàng)建 一個(gè)表:58tc_data,表中包含 title、price、layout、address、agent 五個(gè)字段,類型都為 varchar

此創(chuàng)建表的操作也可以事先手動(dòng)創(chuàng)建,手動(dòng)創(chuàng)建后就不需要此函數(shù)了

【5.2】將數(shù)據(jù)儲(chǔ)存到MySQL數(shù)據(jù)庫

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() 方法的作用是實(shí)現(xiàn)數(shù)據(jù)插入,是真正將語句提交到數(shù)據(jù)庫執(zhí)行的方法,使用 try except 語句實(shí)現(xiàn)異常處理,如果執(zhí)行失敗,則調(diào)用 rollback() 方法執(zhí)行數(shù)據(jù)回滾,保證原數(shù)據(jù)不被破壞

【6】完整代碼

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'
}


# 獲取字體文件并轉(zhuǎn)換為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 編碼的字體字符串解碼成二進(jìn)制編碼
    bin_data = base64.decodebytes(base64_string.encode())
    # 保存為字體文件
    with open('58font.woff', 'wb') as f:
        f.write(bin_data)
    print('第' + str(page_num) + '次訪問網(wǎng)頁,字體文件保存成功!')
    # 獲取字體文件,將其轉(zhuǎn)換為xml文件
    font = TTFont('58font.woff')
    font.saveXML('58font.xml')
    print('已成功將字體文件轉(zhuǎn)換為xml文件!')
    return response.text


# 將加密字體編碼與真實(shí)字體進(jìn)行匹配
def find_font():
    # 以glyph開頭的編碼對(duì)應(yīng)的數(shù)字
    glyph_list = {
        'glyph00001': '0',
        'glyph00002': '1',
        'glyph00003': '2',
        'glyph00004': '3',
        'glyph00005': '4',
        'glyph00006': '5',
        'glyph00007': '6',
        'glyph00008': '7',
        'glyph00009': '8',
        'glyph00010': '9'
    }
    # 十個(gè)加密字體編碼
    unicode_list = ['0x9476', '0x958f', '0x993c', '0x9a4b', '0x9e3a', '0x9ea3', '0x9f64', '0x9f92', '0x9fa4', '0x9fa5']
    num_list = []
    # 利用xpath語法匹配xml文件內(nèi)容
    font_data = etree.parse('./58font.xml')
    for unicode in unicode_list:
        # 依次循環(huán)查找xml文件里code對(duì)應(yīng)的name
        result = font_data.xpath("//cmap//map[@code='{}']/@name".format(unicode))[0]
        # print(result)
        # 循環(huán)字典的key,如果code對(duì)應(yīng)的name與字典的key相同,則得到key對(duì)應(yīng)的value
        for key in glyph_list.keys():
            if key == result:
                num_list.append(glyph_list[key])
    print('已成功找到編碼所對(duì)應(yīng)的數(shù)字!')
    # print(num_list)
    # 返回value列表
    return num_list


# 替換掉網(wǎng)頁中所有的加密字體編碼
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標(biāo)簽
    all_house = soup.find_all('li', class_='house-cell')
    for house in all_house:
        # 標(biāo)題
        title = house.find('a', class_='strongbox').text.strip()
        # print(title)

        # 價(jià)格
        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)

        # 如果存在經(jīng)紀(jì)人
        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', '')
        # 如果存在個(gè)人房源
        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) + '條數(shù)據(jù)爬取完畢,暫停3秒!')
        time.sleep(3)


# 創(chuàng)建MySQL數(shù)據(jù)庫的表: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()


# 將數(shù)據(jù)儲(chǔ)存到MySQL數(shù)據(jù)庫
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創(chuàng)建成功!')
    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) + '頁數(shù)據(jù)爬取完畢!')
        time.sleep(random.randint(3, 60))
    print('所有數(shù)據(jù)爬取完畢!')

看完上述內(nèi)容是否對(duì)您有幫助呢?如果還想對(duì)相關(guān)知識(shí)有進(jìn)一步的了解或閱讀更多相關(guān)文章,請(qǐng)關(guān)注億速云行業(yè)資訊頻道,感謝您對(duì)億速云的支持。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI