摘要 :新手刚接触 lxml 时,总会遇到一些奇奇怪怪的问题,解决这些问题需要花费不少时间。本文整理了一些常见的问题,希望能对你有所帮助。

XPath( XML Path Language,XML 路径语言)是一门在 XML 文档中查找信息的语言。XPath 最初设计是用来搜寻 XML 文档,但是它同样适用于 HTML 文档的搜索。在 Python 标准库提供了HTMLParserSGMLParser两个模块用于解析 HTML,然而这两个模块的实现复杂,用得很不顺手,而第三方库 lxml 则简单许多,而且它基于 Cython 实现,性能出众。

我们可以使用pip install lxml命令来安装它,具体使用教程可以参考 lxml 官方文档w3school 中文教程 ,以下我只列举一些新手容易遇到的常见问题:

一、lxml.html 与 lxml.etree 的区别

lxml.html用来解析 HTML 文档,而lxml.etree用来解析 XML 文档,etree 也可以解析正规的 HTML,但是如果 HTML 文档不完整,则会抛错。

二、lxml.html 中 fromstring, soupparser, html5lib 的区别

from lxml.html import fromstring, soupparser, html5parser

content = '<html><a>link</a></html>'
tree1 = fromstring(content)
tree2 = soupparser.fromstring(content)
tree3 = html5parser.fromstring(content)

主要区别:

  • 第一种方式 lxml.html.fromstring 使用的 parser 为 etree.HTMLParser 的子类,我们称其为 lxml 解析器。
  • 第二种方式 soupparser 模块底层是用 BeautifulSoup 来处理 html,指定的 parser 为 Python 自带的 html.parser,BeautifulSoup 对于声明编码与实际编码不一致的情况处理的更好。
  • 第三种方式 html5lib 模块实现了与浏览器类似的对 HTML5 网页的解析算法,损失一些性能换取更好的兼容性。
    >>> from lxml.html import tostring, html5parser
    >>> tostring(html5parser.fromstring("<table><td>foo"))
    '<table><tbody><tr><td>foo</td></tr></tbody></table>'
    

结论是按自己的需求来选择,1) 如果需要编码识别能力好,用 soupparser,2) 如果要更好的兼容性,用 html5lib,3) 否则用默认的fromstring性能更佳。

三、如何将 lxml HtmlElement 对象转换为 HTML 文本

可以使用lxml.html.tostring

import lxml.html as lh

tree = lh.fromstring('<div><p><a>link</a></p></div>')
elems = tree.xpath('//a')  # HtmlElement
print(lh.tostring(elems[0]))  # Output: b'<a>link</a>'

四、如何获取指定位置的节点

import lxml.html as lh

tree = lh.fromstring('<div><p><a>link1</a><a>link2</a></p></div>')
print(tree.xpath('//a[2]/text()')  # Output: link2

五、contains 有什么需要注意的地方

注意contains是纯字符串匹配(只要包含特定字符即可),有时候其行为并不是我们想要的。比如,有三个链接,前两个 class 包含link,第三个包含link3。 当我们想要获取所有包含link的链接时,首先想到的是使用=contains

from lxml.html import fromstring

tr = fromstring('<a class="link r">l1</a><a class="g link">l2</a><a class="link3">l3</a>')
print(tr.xpath('//*[@class="link")]/text()')  # Wrong Output: []

print(tr.xpath('//*[contains(@class, "link")]/text()')    # Wrong Output: [l1, l2, l3]
print(tr.xpath('//*[contains(@class, "link ")]/text()')   # Wrong Output: [l1, l3]
print(tr.xpath('//*[contains(@class, " link")]/text()')   # Wrong Output: [l2, l3]
print(tr.xpath('//*[contains(@class, " link ")]/text()')  # Wrong Output: []

如以上代码所示,我们期望的是获取 class 名为link的元素,而不是 class 名中包含link的元素。 无论怎么样使用contains=都无法得到正确的结果。解决方案是使用下文介绍的正则表达式(RegExp)来匹配。

六、如何使用正则表达式

lxml 支持 xpath 1.0 标准,不支持 xpath 2.0,这就导致有些功能不能使用,比如正则匹配(matches),第一个元素(first())等。 不过 lxml 提供了扩展的方式来解决部分遗憾,比如我们可以使用 exslt 来 扩展正则表达式功能

from lxml.html import fromstring

tr = fromstring('<a class="link r">l1</a><a class="g link">l2</a><a class="link3">l3</a>')
ns = {"re": "http://exslt.org/regular-expressions"}
print(tr.xpath('//*[contains(@class, "link")]/text()')  # Wrong Output: l1, l2, l3
print(tr.xpath(r'//*[re:match(@class, "(^|\s)link($|\s)")]/text()',
               namespaces=ns)  # Right Output: l1, l2