Python爬取58同城租房数据,破解字体加密 落日映苍穹つ 2022-12-26 07:29 405阅读 0赞 本文的文字及图片来源于网络,仅供学习、交流使用,不具有任何商业用途,如有问题请及时联系我们以作处理。 以下文章来源于CSDN,作者:TRHX • 鲍勃 刚接触Python的新手、小白,可以复制下面的链接去**免费观看Python的基础入门教学视频** https://v.douyu.com/author/y6AZ4jn9jwKW ![d63dea98dca594b34ae8734ac156ae5b.png][] # 【1】加密字体攻克思路 # F12 打开调试模板,通过页面分析,可以观察到,网站里面凡是涉及到有数字的地方,都是显示为乱码,这种情况就是字体加密了,那么是通过什么手段实现字体加密的呢? CSS 中有一个 @font-face 规则,它允许为网页指定在线字体,也就是说可以引入自定义字体,这个规则本意是用来消除对电脑字体的依赖,现在不少网站也利用这个规则来实现反爬 右侧可以看到网站用的字体,其他的都是常见的微软雅黑,宋体等,但是有一个特殊的:fangchan-secret ,不难看出这应该就是58同城的自定义字体了 ![c028a19f153304d565a880ed3a3ff003.png][] 我们通过控制台看到的乱码事实上是由于 unicode 编码导致,查看网页源代码,我们才能看到他真正的编码信息 ![3f9549021c6a33ffe93f17e02749236a.png][] 要攻克加密字体,那么我们肯定要分析他的字体文件了,先想办法得到他的加密字体文件,同样查看源代码,在源代码中搜索 fangchan-secret 的字体信息 ![8396d02957410386de0d60de6fc1b3cd.png][] 选中的蓝色部分就是 base64 编码的加密字体字符串了,我们将其解码成二进制编码,写进 .woff 的字体文件,这个过程可以通过以下代码实现: 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 这个软件来看看字体对应的编码是什么: ![6fb2d994b4efa2520b049f7507819e1a.png][] 观察我们在网页源代码中看到的编码:类似于 龤、龒 对比字体文件对应的编码:类似于 uni9FA4、nui9F92 可以看到除了前面三个字符不一样以外,后面的字符都是一样的,只不过英文大小写有所差异 现在我们可能会想到,直接把编码替换成对应的数字不就OK了?然而并没有这么简单 尝试刷新一下网页,可以观察到 base64 编码的加密字体字符串会改变,也就是说编码和数字并不是一一对应的,再次获取几个字体文件,通过对比就可以看出来 ![d6a81c49da447c11c3f02b01c57b3e16.png][] 可以看到,虽然每次数字对应的编码都不一样,但是编码总是这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 ![07799e147ce01fa6e716cdec87e832d1.png][] 我们再次获取一个字体文件作为对比分析 ![ff41d55c82d08d24c90a7b2495d9ae21.png][] 依然是 0x958f 这个编码,两次对应的 name 分别是 glyph00004 和 glyph00007,两次对应的数字分别是 3 和 6,那么结论就来了,每次发送请求,code 对应的 name 会随机发生变化,而 name 对应的数字不会发生变化,glyph00001 对应数字 0、glyph00002 对应数字 1,以此类推 那么以 glyph 开头的编码是如何对应相应的数字的呢?在 xml 文件里面,每个编码都有一个 TTGlyph 的标签,标签里面是一行一行的类似于 x,y 坐标的东西,这个其实就是用来绘制字体的,用 matplotlib 根据坐标画个图,就可以看到是一个数字 ![b0cbdad3d9887b869f8422c316bfd0f5.png][] 此时,我们就知道了编码与数字的对应关系,下一步,我们可以查找 xml 文件里,编码对应的 name 的值,也就是以 glyph 开头的编码,然后返回其对应的数字,再替换掉网页源代码里的编码,就能成功获取到我们需要的信息了! 总结一下攻克加密字体的大致思路: * 分析网页,找到对应的加密字体文件 * 如果引用的加密字体是一个 base64 编码的字符串,则需要转换成二进制并保存到 woff 字体文件中 * 将字体文件转换成 xml 文件 * 用 FontCreator 软件观察字体文件,结合 xml 文件,分析其编码与真实字体的关系 * 搞清楚编码与字体的关系后,想办法将编码替换成正常字体 # 【2】思维导图 # ![a342e1f8501e39a38ca8ad821d787c5b.png][] # 【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 解析库很容易提取到相关信息,这里要注意的是,租房信息来源分为三种:经纪人、品牌公寓和个人房源,这三个的元素节点也不一样,因此匹配的时候要注意 ![55c31f3416f2eff39a811dff235ace48.png][] # 【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 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('所有数据爬取完毕!') # 【7】数据截图 # ![ffcc2d6dfd946f02fb56e329e431efe0.png][] [d63dea98dca594b34ae8734ac156ae5b.png]: /images/20221120/214bc09462734f8e80ff219e6a4259e4.png [c028a19f153304d565a880ed3a3ff003.png]: /images/20221120/c3b83efd62104fed83cb2a7b27233cb5.png [3f9549021c6a33ffe93f17e02749236a.png]: /images/20221120/dd034f89b2904a52ac9f835c9b509ad6.png [8396d02957410386de0d60de6fc1b3cd.png]: https://img-blog.csdnimg.cn/img_convert/8396d02957410386de0d60de6fc1b3cd.png [6fb2d994b4efa2520b049f7507819e1a.png]: https://img-blog.csdnimg.cn/img_convert/6fb2d994b4efa2520b049f7507819e1a.png [d6a81c49da447c11c3f02b01c57b3e16.png]: https://img-blog.csdnimg.cn/img_convert/d6a81c49da447c11c3f02b01c57b3e16.png [07799e147ce01fa6e716cdec87e832d1.png]: https://img-blog.csdnimg.cn/img_convert/07799e147ce01fa6e716cdec87e832d1.png [ff41d55c82d08d24c90a7b2495d9ae21.png]: https://img-blog.csdnimg.cn/img_convert/ff41d55c82d08d24c90a7b2495d9ae21.png [b0cbdad3d9887b869f8422c316bfd0f5.png]: https://img-blog.csdnimg.cn/img_convert/b0cbdad3d9887b869f8422c316bfd0f5.png [a342e1f8501e39a38ca8ad821d787c5b.png]: https://img-blog.csdnimg.cn/img_convert/a342e1f8501e39a38ca8ad821d787c5b.png [55c31f3416f2eff39a811dff235ace48.png]: https://img-blog.csdnimg.cn/img_convert/55c31f3416f2eff39a811dff235ace48.png [ffcc2d6dfd946f02fb56e329e431efe0.png]: https://img-blog.csdnimg.cn/img_convert/ffcc2d6dfd946f02fb56e329e431efe0.png
相关 Python爬取58同城租房数据,破解字体加密 本文的文字及图片来源于网络,仅供学习、交流使用,不具有任何商业用途,如有问题请及时联系我们以作处理。 以下文章来源于CSDN,作者:TRHX • 鲍勃 刚接触Python的 落日映苍穹つ/ 2022年12月26日 07:29/ 0 赞/ 406 阅读
相关 58同城bug 打开下面链接: [https://sh.58.com/pinpaigongyu/41756538758439x.shtml?adtype=1&from=3-list-9 àì夳堔傛蜴生んèń/ 2022年11月03日 00:41/ 0 赞/ 223 阅读
相关 利用python爬取58同城简历数据 利用python爬取58同城简历数据 最近接到一个工作,需要获取58同城上面的简历信息([http://gz.58.com/qzyewu/][http_gz.58.com 一时失言乱红尘/ 2022年08月22日 00:20/ 0 赞/ 240 阅读
相关 58同城数据库架构设计思路 [![Center][]][Center 1] [58同城数据库架构设计思路][Center 1] 58同城,一个被贴上“神奇”标签的网站,海量信息背后到底支 男娘i/ 2022年08月08日 00:50/ 0 赞/ 284 阅读
相关 58同城数据库架构最佳实践 [http://geek.csdn.net/news/detail/52070][http_geek.csdn.net_news_detail_52070] 数据 偏执的太偏执、/ 2022年07月15日 00:03/ 0 赞/ 274 阅读
相关 Python的charts和Jupyter的使用 使数据可视化 对58同城的爬取 尝试学习了将爬取的数据进行清洗,更新数据库后。进行可视化。记录一部分 主要是对两个库的使用 使用pip进行安装 jupyter安装后再cmd下输入 `jupyter 刺骨的言语ヽ痛彻心扉/ 2022年07月12日 01:21/ 0 赞/ 264 阅读
相关 python 爬取了租房数据 爬取链接:[https://sh.lianjia.com/zufang/][https_sh.lianjia.com_zufang] 代码如下: import 谁借莪1个温暖的怀抱¢/ 2022年05月14日 07:42/ 0 赞/ 341 阅读
相关 利用python爬取贝壳网租房信息 最近准备换房子,在网站上寻找各种房源信息,看得眼花缭乱,于是想着能否将基本信息汇总起来便于查找,便用python将基本信息爬下来放到excel,这样一来就容易搜索了。 1. 红太狼/ 2021年12月22日 03:45/ 0 赞/ 582 阅读
相关 Python 爬虫 58同城 目标站点需求分析 获取各类产品的名字,地区,时间,价格 涉及的库 BeautifulSoup,requests,time,pymongo 获取各大类产 桃扇骨/ 2021年12月17日 08:29/ 0 赞/ 340 阅读
还没有评论,来说两句吧...