1、你将会了解到源码跟进的过程;
2、你将会看到问题分析的思路;
3、你可以解决 Requests 库关于编码猜测不准确的问题;
这一篇我们来观摩 Python 中的 Requests、Scrapy 库以及 Golang 中的 Charset 库对于网页编码的处理逻辑,并让你具备提高 Requests 库编码猜测准确性的能力。
近期在工程实践中发现了一个长期潜伏的网页文本乱码问题,也就是爬取网页后,打印出来的文本是乱码。如果你是 Python 工程师,你可以试试下面这段代码:
import requests
# GB2312
resp = requests.get("http://news.inewsweek.cn/society/2022-05-30/15753.shtml")
print(resp.text)
打印出来的网页文本,中文部分就是乱码,这真是令我狗头 🐶 变大。
你心里可能会有这样的疑问:“按理说,Python 的 Requests 库应该能够帮助我们自动识别编码,然后自动转换才对的”。“但事实却并不是那么回事,为什么?”
不仅仅是 Python 的 Requests 库有这样的症状,Golang 的 Charset 库一样有类似的情况。
周树人:“我这里遇到了两个问题,一个是乱码,另一个也是乱码”
这时候,脑海中想起周树人那句话“我大抵是变菜了,这样的问题这么长时间居然都不知道”。
为了证明自己并不是变菜,而是本来就这么菜,我决定翻(拨)开它们的源码(衣服)瞧一瞧。
虽然手动指定编码(例如 resp.encoding="gb2312")可以解决这样的问题,但是手动并非我等工程师所求。而且,手动指定也会乱码。
这里要注意的是,为了方便能看到源码,Python 工程师请用 Pycharm 查看,VSCode 这类工具我没试过,不确定能否追查到底层源码。
Python 工程师,请把上面的代码复制粘贴到 Pycharm 中,然后按 Ctrl/Command 键+鼠标左键跟进 resp.text 的这个 text
它的代码不多,我们来阅读一下
大致意思是当需要获取网页文本的时候,编码部分的处理逻辑为:
1.先从对象中找编码,找到就用;2.找不到编码的话,就通过 apparent_encoding 去分析(其实就是猜)。
第 2 步 apparent_encoding 这里其实还可以继续跟进看代码,这里就是猜测的过程,因为并不是文章重点,我就不写了,感兴趣的可以自己跟进瞅一瞅。
我们要看第 1 步,它想用对象自身的编码,也就是 self.encoding。那么问题来了:self.encoding 的值它又是哪里来的呢?
你可以从对象中初始化和赋值的地方找,然后跟进看源码。也可以简单点跟着我的路线一步步走,下面是我的查找步骤。在代码里找到 requests.get() 中的 get 函数,然后一直往里面跟,路径如下:
1. requests.get
2. request
3. session.request
4. self.send
5. adapter.send
6. self.build_response
7. get_encoding_from_headers
代码只有几行,我们阅读一下
很简单,它从响应头中读取 content-type,然后试图从里面找 charset,如果找到就把对应的值传回去,找不到就把 ISO-8859-1 传回去。
这个传回去的值,就是 response.text 中用 self.encoding 想要获取的值。如果它读取到了,那就用读取到的那个值,读取不到,就用 apparent_encoding 猜出来的值,这也就是为什么它遇到 GBK/GB2312/GB18030 的时候容易出现乱码。
小知识 GB18030 能够兼容处理 GBK/GB2312 编码,例如 Golang 中遇到 GBK 或者 GB2312 的时候,直接用 GB18020 编码就不会乱码。
那这怎么办呢?
我们来看看 Golang Charset 库(golang.org/x/net/html/charset)对应那部分(也就是 DetermineEncoding 函数)的源码,跟进路径如下:
1. charset.DetermineEncoding
2. Lookup
3. htmlindex.Get
大致的意思是,通过读取你传入的网页文本,从里面找到 charset=xxx 这样的句式,然后把那个值取出来,也就是把编码类型的名称取出来。
过多的不用看了,剩下的逻辑就是按照读取到的编码类型,对网页进行重新编码,最后把编码完成的文本返回。
按理说,这样的方法是比较准确的。只要从网页中读取到编码,就能够顺利返回正确的文本,不会产生乱码。然而事实就是不按这个理,因为测试的时候出现了乱码。
通过阅读 Requests 的源码和 Charset 的源码,我们掌握了几个信息:
1、网页文本里可以读取到编码类型;
2、响应头里也可以读取到编码类型;
3、从 Requests 库的逻辑来看,响应头里面的编码类型才是正确的类型,网页文本里写的 charset=xxx 有可能会导致乱码。
Python Requests 和 Golang Charset 的逻辑,就是待会我们 1+1=3 的重要依据。
想必聪明的你现在也猜出来了“把 Requests 的优点和 Charset 的优点结合到一起就好了嘛!”
上面的发现,我跟青南分享后,他说用 Scrapy 的时候没有发生过乱码的问题,并且他给出了对应部分的源码
https://github.com/scrapy/w3lib/blob/master/w3lib/encoding.py
我们来简单阅读一下
大致的逻辑,是先尝试从响应头的 Content-Type 里面读取编码,有值的话直接使用。读不到再去网页文本里面找。
跟上面讲到的思路如出一辙,我都不知道这是我自己想到的的方法,还是抄别人的方法。
“管他呢,好用就可以了”
这下就明朗了,Requests 和 Charset 库的处理逻辑其实应该跟 Scrapy 的逻辑一样,这样就能够提高各自的编码猜测准确率。
真的是这样吗?
那也不一定,还是得动手测试。
因此我收集了很多不同站点、不同类型网页的文本进行测试,大概有五六十个网页(测试基于 Golang)。测试时候用的逻辑跟 Scrapy 一致,最后的测试结果是全部通过。
在未使用这个处理逻辑之前, Golang Charset 库的错误数是 2,Python Requests 库的错误率很高,测试了几个我就不想测了……
由于我的测试代码是 Golang 的,而且项目是商业项目,所以具体的测试代码和用例我就不放出来了。
我从中随机找几个样本给大家,大家可以自己动手玩一玩
http://news.inewsweek.cn/society/2022-05-30/15753.shtml
https://jrz.cnstock.com/yw/202206/4893506.htm
https://www.cs.com.cn/zt/2022dzt/03/202206/t20220607_6275320.html
说明:本文测试结果和解决思路基于测试样本。如果有不同的案例,可以在评论区发出来讨论。
更多每日开发小技巧
尽在****未闻 Code Telegram Channel !
END
未闻 Code·知识星球开放啦!
一对一答疑爬虫相关问题
职业生涯咨询
面试经验分享
每周直播分享
......
未闻 Code·知识星球期待与你相见~
一二线大厂在职员工
十多年码龄的编程老鸟
国内外高校在读学生
中小学刚刚入门的新人
在“未闻 Code技术交流群”等你来!
入群方式:添加微信“mekingname”,备注“粉丝群”(谢绝广告党,非诚勿扰!)