千家信息网

Python如何爬取58同城租房数据并破解字体加密

发表于:2025-01-31 作者:千家信息网编辑
千家信息网最后更新 2025年01月31日,Python如何爬取58同城租房数据并破解字体加密,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。【1】加密字体攻克思路F
千家信息网最后更新 2025年01月31日Python如何爬取58同城租房数据并破解字体加密

Python如何爬取58同城租房数据并破解字体加密,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。

【1】加密字体攻克思路

F12 打开调试模板,通过页面分析,可以观察到,网站里面凡是涉及到有数字的地方,都是显示为乱码,这种情况就是字体加密了,那么是通过什么手段实现字体加密的呢?

CSS 中有一个 @font-face 规则,它允许为网页指定在线字体,也就是说可以引入自定义字体,这个规则本意是用来消除对电脑字体的依赖,现在不少网站也利用这个规则来实现反爬

右侧可以看到网站用的字体,其他的都是常见的微软雅黑,宋体等,但是有一个特殊的:fangchan-secret ,不难看出这应该就是58同城的自定义字体了

要攻克加密字体,那么我们肯定要分析他的字体文件了,先想办法得到他的加密字体文件,同样查看源代码,在源代码中搜索 fangchan-secret 的字体信息

选中的蓝色部分就是 base64 编码的加密字体字符串了,我们将其解码成二进制编码,写进 .woff 的字体文件,这个过程可以通过以下代码实现:

import requestsimport 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 requestsimport base64from 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 文件并分析,在标签内可以看到熟悉的类似于 0x9476、0x958f 的编码,其后四位字符恰好是网页字体的加密编码,可以看到每一个编码后面都对应了一个 glyph 开头的编码

将其与 58font.woff 文件对比,可以看到 code 为 0x958f 这个编码对应的是数字 3,对应的 name 编码是 glyph00004

我们再次获取一个字体文件作为对比分析

此时,我们就知道了编码与数字的对应关系,下一步,我们可以查找 xml 文件里,编码对应的 name 的值,也就是以 glyph 开头的编码,然后返回其对应的数字,再替换掉网页源代码里的编码,就能成功获取到我们需要的信息了!

总结一下攻克加密字体的大致思路:

  • 分析网页,找到对应的加密字体文件

  • 如果引用的加密字体是一个 base64 编码的字符串,则需要转换成二进制并保存到 woff 字体文件中

  • 将字体文件转换成 xml 文件

  • 用 FontCreator 软件观察字体文件,结合 xml 文件,分析其编码与真实字体的关系

  • 搞清楚编码与字体的关系后,想办法将编码替换成正常字体

【2】思维导图

【3】加密字体处理模块

【3.1】获取字体文件并转换为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 文件

【3.2】将加密字体编码与真实字体进行匹配

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 列表里面每个加密字体的真实值

【3.3】替换掉网页中所有的加密字体编码

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() 方法,依次将十个加密字体编码替换掉

【4】租房信息提取模块

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 解析库很容易提取到相关信息,这里要注意的是,租房信息来源分为三种:经纪人、品牌公寓和个人房源,这三个的元素节点也不一样,因此匹配的时候要注意

【5】MySQL数据储存模块

【5.1】创建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

此创建表的操作也可以事先手动创建,手动创建后就不需要此函数了

【5.2】将数据储存到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() 方法执行数据回滚,保证原数据不被破坏

【6】完整代码

import requestsimport timeimport randomimport base64import pymysqlfrom lxml import etreefrom bs4 import BeautifulSoupfrom 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对应的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_datadef 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('所有数据爬取完毕!')

看完上述内容是否对您有帮助呢?如果还想对相关知识有进一步的了解或阅读更多相关文章,请关注行业资讯频道,感谢您对的支持。

字体 编码 文件 加密 数据 字符 数字 字符串 成功 网页 二进制 分析 信息 字典 数据库 方法 开头 循环 观察 就是 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 服务器填充 网络安全日志留存6个月 锦州盘古网络技术有限公司招聘 房地一体不动产数据库建设 一二三四年级网络安全绘画 没有域名数据库的 大学生网络安全宣传月主题 机房服务器怎么改线 云计算和服务器应用慕课测试答案 百度 腾讯 网络安全法 xp如何重启共享服务器 网络安全检查工总结gov 英特尔服务器主板没声音 怎么替换服务器中文件 ps4登陆原神无法连接服务器 深圳医院网络安全事故处理案例 宝塔面板是管理服务器 前端 获取页面一组数据库 数据库数据更新流程 兰州奥曼软件开发中心 怎么进入云服务器远程桌面 服务器共享文件夹 sql数据库中二进制数据是什么 极速智能破解连接服务器 软件开发平台技术协议 兰考租房软件开发 Ns暗黑2连不上服务器怎么加速 索引数据库搭建 福州市网络安全等级备案 软件开发要从谁的需求出发
0