司开星的博客

解决 lxml 处理HTML引起的标签顺序改变问题

问题细节

近期在用lxml处理某个网页HTML源码时发现<font>标签的结束标签位置会被改变,具体来说是<font>标签本身包围了一些<p>标签,当<font>外存在<div标签时,<font>标签的结束标签</font>位置会被改变,原本被包围在中间的的<p>标签全部变成<font>的兄弟节点。

寻找原因

一开始直接在 google 搜索问题原因,但一直没找到类似的问题。于是开始从使用的模块入手。笔者使用的HTML导入模块是lxml.html.soupparser,这个模块的文档中并没提到相关的问题,不过模块调用了BeautifulSoup模块处理HTML,于是查看了BeautifulSoup的源码。果然找到了影响这两个标签的部分:

1
2
3
4
5
6
7
8
9
10
#According to the HTML standard, each of these inline tags can
#contain another tag of the same type. Furthermore, it's common
#to actually use these tags this way.
NESTABLE_INLINE_TAGS = ('span', 'font', 'q', 'object', 'bdo', 'sub', 'sup',
'center')

#According to the HTML standard, these block tags can contain
#another tag of the same type. Furthermore, it's common
#to actually use these tags this way.
NESTABLE_BLOCK_TAGS = ('blockquote', 'div', 'fieldset', 'ins', 'del')

也就是BeautifulSoup是根据HTML标准来规范那些不规范的HTML,其中这两类标签根据标准只能包围类别中的标签。

## 解决方法

修改源码

最简单的方法就是直接删掉BeautifulSoup源码中这个list中的标签。不过这种方法弊端也很明显:会影响其他项目中引用的BeautifulSoup功能。要不影响其他项目可以把修改后的BeautifulSoup源码放在此项目中。不过这样也会影响此项目中其他用到BeautifulSoup的部分。

继承修改

更普遍的做法是继承之后通过子类修改相应属性。

这里有个问题,由于我们使用的实际是lxml,但修改属性只能通过继承BeautifulSoup,所以要写两个新类分别继承两个模块,这样略显麻烦。幸运的是,在查看源码的过程中发现lxml.html.soupparser.fromstring()方法中有一个beautifulsoup参数,如果没有提供这个参数则默认使用BeautifulSoup。这样就简单多了,不需要动lxml部分,只需要写个BeautifulSoup的新类即可:

1
2
3
4
5
6
7
8
9
from BeautifulSoup import BeautifulSoup


class new_BeautifulSoup(BeautifulSoup):
def __init__(self, *args, **kwargs):
super(new_BeautifulSoup, self).__init__(*args, **kwargs)
super(new_BeautifulSoup, self).NESTABLE_TAGS.pop('font', None)
super(new_BeautifulSoup, self).NESTABLE_TAGS.pop('div', None)
super(new_BeautifulSoup, self).RESET_NESTING_TAGS.pop('div', None)

虽然在源码中影响这两个标签的属性是NESTABLE_INLINE_TAGSNESTABLE_BLOCK_TAGS,但实际测试发现直接覆写这两个属性没有效果,修改NESTABLE_TAGSRESET_NESTING_TAGS则可以达到预期效果。

## 延伸问题

除了这种改变标签顺序的问题,lxml在导入HTML时还会影响其他结构,比如添加修改引号,修改标签多个属性的顺序等。这些问题可以当作是模块对非标准化源码(broken html)的标准化处理,在处理少量规范网页时没什么影响,但如果是要处理大量不规范网页则需要多多注意。