司开星的博客

lxml.html.soupparser.fromstring() 出现ValueError Comment may not contain '--' or end with '-' 的解决办法

初始问题

近期在使用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
2
3
4
5
6
7
8
9
10
import re
def remove_control_characters(html):
def str_to_int(s, default, base=10):
if int(s, base) < 0x10000:
return unichr(int(s, base))
return default
html = re.sub(ur"&#(\d+);?", lambda c: str_to_int(c.group(1), c.group(0)), html)
html = re.sub(ur"&#[xX]([0-9a-fA-F]+);?", lambda c: str_to_int(c.group(1), c.group(0), base=16), html)
html = re.sub(ur"[\x00-\x08\x0b\x0e-\x1f\x7f]", "", html)
return html

或者下面的函数(http://stackoverflow.com/questions/4324790/removing-control-characters-from-a-string-in-python):

1
2
3
import unicodedata
def remove_control_characters(s):
return "".join(ch for ch in s if unicodedata.category(ch)[0]!="C")

测试了第一个函数发现确实可以解决问题。

另一个问题

如果程序只是我自己运行的话问题到这就结束了。但实际上相关的服务器程序也要抓取同样的页面,而后台在抓取这个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

搜索无果之后开始在官方文档和源码中找原因和解决办法。一段时间的查看之后发现了以下几个相关点:

  1. soupparser.fromstring首先使用BeautifulSoup载入HTML, 之后再依靠html.parser.makeelement来生成树;
  2. soupparser.fromstring虽然有makeelement参数可以传递其他Element factory function,但没有文档说明如何定义自己的Element factory function ;
  3. makeelement方法继承自lxml.etree,但etree模块是用Cython(一种类似Python但会与C模块交互的语言)写的;
  4. 错误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中定义的;
  5. 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,相关的记录只有soupparserBeautifulSoupbs3换成bs4了,但已经试过更换不同版本的bs,并没有解决问题。

于是只能选最无奈的办法,把服务器的lxml也换成3.4.4版本,并且使用之前提到的函数预先过滤HTML中的控制字符。