初始问题
近期在使用lxml.html.soupparser.fromstring()
载入某个网页HTML时得到一条报错:
ValueError: All strings must be XML compatible: Unicode or ASCII, no NULL bytes or control characters
仔细对比了这个HTML与其他不会产生这个错误的HTML,发现报错的HTML中有几个ascii控制字符,即错误信息中的control characters。于是搜索了解决办法,在https://github.com/html5lib/html5lib-python/issues/96 这里找到了以下函数可以删去HTML中的控制字符:
1 | import re |
或者下面的函数(http://stackoverflow.com/questions/4324790/removing-control-characters-from-a-string-in-python):
1 | import unicodedata |
测试了第一个函数发现确实可以解决问题。
另一个问题
如果程序只是我自己运行的话问题到这就结束了。但实际上相关的服务器程序也要抓取同样的页面,而后台在抓取这个HTML后出现了另一种报错:
ValueError: Comment may not contain ‘–’ or end with ‘-‘
排查过其他原因后发现问题出在lxml的版本上。我本机的lxml版本是3.4.4, 服务器程序的版本是3.6.4。于是我把本地的lxml升级到了最新版3.7.3,出现了与服务器相同的报错。
寻找解决办法
直接在Google搜索这个问题确实可以搜到一些。其中有一条http://stackoverflow.com/questions/34595275/disable-comments-check-for-in-lxml 提供了解决办法:将本地的html5parser.py
替换成github上最新版。但实测无效,仔细查看问题觉得原因是问题中使用的方法为html5parser.fromstring
,而我使用的是soupparser.fromstring
。
搜索无果之后开始在官方文档和源码中找原因和解决办法。一段时间的查看之后发现了以下几个相关点:
soupparser.fromstring
首先使用BeautifulSoup
载入HTML, 之后再依靠html.parser.makeelement
来生成树;soupparser.fromstring
虽然有makeelement
参数可以传递其他Element factory function,但没有文档说明如何定义自己的Element factory function ;makeelement
方法继承自lxml.etree
,但etree
模块是用Cython(一种类似Python但会与C模块交互的语言)写的;- 错误ValueError: Comment may not contain ‘–’ or end with ‘-‘ 及 ValueError: All strings must be XML compatible: Unicode or ASCII, no NULL bytes or control characters 也是在
etree
中定义的; BeautifulSoup
在载入源码后已经改变了comment
处的结构。
etree
模块并非Python写的,意味着想通过继承重写相关方法比较麻烦。
BeautifulSoup
会改变comment
处结构,说不定可以从这里入手。但尝试给beautifulsoup
参数传递了不同的beautifulsoup
(包括bs3,bs4以及继承重写相关方法后的bs4)后发现无法解决问题。
于是没有在这条路上找到解决办法。
回到起点
回想了一开始的问题,发现只要我把控制字符删除之后就可以正常执行fromstring
, 于是开始考虑不同版本之间的区别。查看了changelog,没有发现相关的bug fix记录。于是在virtualenv中开始测试从哪个版本开始有这个报错的:
pip install lxml==3.5.0b1
发现从3.4.4之后的版本,也就是3.5.0b1开始,就有这个报错了。看了这个版本的changelog,相关的记录只有soupparser
的BeautifulSoup
从bs3
换成bs4
了,但已经试过更换不同版本的bs
,并没有解决问题。
于是只能选最无奈的办法,把服务器的lxml
也换成3.4.4版本,并且使用之前提到的函数预先过滤HTML中的控制字符。