早前写了两篇文章,讲述使用 python-ftfy
库和大语言模型(LLM)来实现乱码(mojibake)的纠正:
- ftfy:修正小段乱码(Mojibake)为正常文本的Python库
- 使用大语言模型(LLM)修正小段乱码(Mojibake)为正常文本
然后就写了个脚本开始去纠正数据库里的存量数据,没想到脚本一跑,出现很奇怪的问题,日志显示纠正结果的最后一个字,仍然是乱码,如:
'Python æ\x8f\x90å\x8f–两个列表的共å\x90Œå…ƒç´'
'Python 提取两个列表的共同元ç´''Diablo 第二幕 é²\x81 · 高å›'
'Diablo 第二幕 鲁 · 高å›''2021年的最å\x90Žä¸€ç¯‡æ–‡ç«'
'2021年的最后一篇文ç«'
我以为是 ftfy
的问题,就开始探究:
>>> t = 'Python 找出åº\x8f列里的符å\x90ˆè¦\x81求的元ç´'
>>> ftfy.guess_bytes( t.encode('gb18030'))
('Python \x810Š3¡ë\x810†6\x810Š2\x816¦0\x810†2\x810Š2\x810†2\x810‚5\x810Š2\x810·3¡ª¨¦\x816¦0\x810“3\x810Š4\x810”8\x816¥7\x810Š4\x810…1\x810„7\x810Š2\x810‚6\x810·3¨¨\x810„7\x810\x811\x810Š3¡À\x816¥5\x810Š4\x810”8\x816¥7\x810Š2¡\xad\x810™6\x810Š4\x810…7', 'sloppy-windows-1252')
先是猜测到原始编码是 sloppy-windows-1252
,那就尝试进行转换编码:
>>> t.encode('sloppy-windows-1252').decode('utf-8')
Traceback (most recent call last):File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'utf-8' codec can't decode bytes in position 43-44: unexpected end of data
失败的原因是后面解码时发现数据不够,那就试一下前面的
>>> len( t.encode('sloppy-windows-1252'))
45
>>> tw = t.encode('sloppy-windows-1252')
>>> tw[:43].decode('utf-8')
'Python 找出序列里的符合要求的元'
果然除最后一个字以后,都没有问题。那就打印出来看看,
>>> t.encode('sloppy-windows-1252')
b'Python \xe6\x89\xbe\xe5\x87\xba\xe5\xba\x8f\xe5\x88\x97\xe9\x87\x8c\xe7\x9a\x84\xe7\xac\xa6\xe5\x90\x88\xe8\xa6\x81\xe6\xb1\x82\xe7\x9a\x84\xe5\x85\x83\xe7\xb4'
这一匹配,就看出问题了,查一下 unicode 表,可知最后两个字「元素」的编码是:
元 0xE5 0x85 0x83
素 0xE7 0xB4 0xA0
发现素字明显少了一个字节!这也太奇怪了。然后开始去查看全部有这种问题的标题,发现结尾字是素传章因习
,都是 0xA0
结尾的,就会出这个问题。而0xA0
字符就是NSBP
,即非空换行。至此,感觉快找到原因了,应该是 rss 解析代码把 0xA0 当作空白删除掉了。
我们使用的数据采集器是 feedparser
,当下去查看它的代码,确认是这个原因。有个strip()
调用,把最后这个空白字节删除掉了。这就离谱。
但我看了一下 feedparser
的代码,好绕啊。估计为了 rss/atom/json 还有各种不规范的处理,架构太灵活了。我一时也不知道怎么先整个文档转编码,只好先简单改一下代码,在获取 title 的时候,不要 strip()
,这样获取到的标题就是正常的非乱码文本,也就不用再使用ftfy
纠正了,我们自己项目这样改是够用的。
我把自己改过的代码放到这个github仓库,因为感觉不是完整的解决方案(应该整个文档先转编码再 parse 的),所以不打算往上游 PR 修改了,如果有读者遇到同样的问题,可以我的这个 fork 来作为临时解决方案顶一下。
最后,错怪ftfy
了,还跑去 issues 里求助。但feedparser
这样的老牌项目有这样的大Bug一直没有人修复,也是奇怪了。
另,编程真的是老天赏饭吃,我纠结半天后向Shell壳总求助,他第一反应就是「看一下是不是最后一个字符缺失了」,而我则需要debug几个小时才能认识到这一点。差距啊。