爬取时间:2019-10-21
爬取难度:★★★☆☆☆
请求链接:https://wh.58.com/chuzu/
爬取目标:58同城武汉出租房的所有信息
涉及知识:网站加密字体的攻克、请求库 requests、解析库 Beautiful Soup、数据库 MySQL 的操作
完整代码:https://github.com/TRHX/Python3-Spider-Practice/tree/master/58tongcheng
其他爬虫实战代码合集(持续更新):https://github.com/TRHX/Python3-Spider-Practice
爬虫实战专栏(持续更新):https://itrhx.blog.csdn.net/article/category/9351278
【1x00】加密字体攻克思路
F12 打开调试模板,通过页面分析,可以观察到,网站里面凡是涉及到有数字的地方,都是显示为乱码,这种情况就是字体加密了,那么是通过什么手段实现字体加密的呢?
CSS 中有一个 @font-face
规则,它允许为网页指定在线字体,也就是说可以引入自定义字体,这个规则本意是用来消除对电脑字体的依赖,现在不少网站也利用这个规则来实现反爬
右侧可以看到网站用的字体,其他的都是常见的微软雅黑,宋体等,但是有一个特殊的:fangchan-secret
,不难看出这应该就是58同城的自定义字体了

我们通过控制台看到的乱码事实上是由于 unicode 编码导致,查看网页源代码,我们才能看到他真正的编码信息

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

选中的蓝色部分就是 base64 编码的加密字体字符串了,我们将其解码成二进制编码,写进 .woff
的字体文件,这个过程可以通过以下代码实现:
1 | import requests |
得到字体文件后,我们可以通过 FontCreator 这个软件来看看字体对应的编码是什么:

观察我们在网页源代码中看到的编码:类似于 龤
、龒
对比字体文件对应的编码:类似于 uni9FA4
、nui9F92
可以看到除了前面三个字符不一样以外,后面的字符都是一样的,只不过英文大小写有所差异
现在我们可能会想到,直接把编码替换成对应的数字不就OK了?然而并没有这么简单
尝试刷新一下网页,可以观察到 base64 编码的加密字体字符串会改变,也就是说编码和数字并不是一一对应的,再次获取几个字体文件,通过对比就可以看出来

可以看到,虽然每次数字对应的编码都不一样,但是编码总是这10个,是不变的,那么编码与数字之间肯定存在某种对应关系,,我们可以将字体文件转换为 xml 文件来观察其中的对应关系,改进原来的代码即可实现转换功能:
1 | import requests |
打开 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文件
1 | def get_font(page_url, page_num): |
由主函数传入要发送请求的 url,利用字符串的 split()
方法,匹配 base64 编码的加密字体字符串,利用 base64
模块的 base64.decodebytes()
方法,将 base64 编码的字体字符串解码成二进制编码并保存为字体文件,利用 FontTools
库,将字体文件转换为 xml 文件
【3x02】将加密字体编码与真实字体进行匹配
1 | def find_font(): |
由前面的分析,我们知道 name 的值(即以 glyph 开头的编码)对应的数字是固定的,glyph00001
对应数字 0
、glyph00002
对应数字 1
,以此类推,所以可以将其构造成为一个字典 glyph_list
同样将十个 code(即类似于 0x9476
的加密字体编码)构造成一个列表
循环查找这十个 code
在 xml 文件里对应的 name
的值,然后将 name
的值与字典文件的 key
值进行对比,如果两者值相同,则获取这个 key
的 value
值,最终得到的列表 num_list
,里面的元素就是 unicode_list
列表里面每个加密字体的真实值
【3x03】替换掉网页中所有的加密字体编码
1 | def replace_font(num, page_response): |
传入由上一步 find_font()
函数得到的真实字体的列表,利用 replace()
方法,依次将十个加密字体编码替换掉
【4x00】租房信息提取模块
1 | def parse_pages(pages): |
利用 BeautifulSoup 解析库很容易提取到相关信息,这里要注意的是,租房信息来源分为三种:经纪人、品牌公寓和个人房源,这三个的元素节点也不一样,因此匹配的时候要注意

【5x00】MySQL数据储存模块
【5x01】创建MySQL数据库的表
1 | def create_mysql_table(): |
首先指定数据库为 58tc_spiders,需要事先使用 MySQL 语句创建,也可以通过 MySQL Workbench 手动创建
然后使用 SQL 语句创建 一个表:58tc_data,表中包含 title、price、layout、address、agent 五个字段,类型都为 varchar
此创建表的操作也可以事先手动创建,手动创建后就不需要此函数了
【5x02】将数据储存到MySQL数据库
1 | def save_to_mysql(data): |
commit()
方法的作用是实现数据插入,是真正将语句提交到数据库执行的方法,使用 try except
语句实现异常处理,如果执行失败,则调用 rollback()
方法执行数据回滚,保证原数据不被破坏
【6x00】完整代码
1 | # ============================================= |
【7x00】数据截图
