Posted on:
Last modified:
最近要做下微信爬虫,之前写个小东西都是直接用正则提取数据就算了,如果需要更稳定的提取数据, 还是使用 xpath 定位元素比较可靠。周末没事,从爬虫的角度研究了一下 python xml/html 相关的库。
Python 标准库中自带了 xml 模块,但是性能不够好,而且缺乏一些人性化的 API。相比之下,第三方库 lxml 是用 Cython 实现的,而且增加了很多实用的功能,可谓爬虫处理网页数据的一件利器。
严格来说,html 并不是 xml 的一种,两者都是 SGML 的衍生。不过 lxml 对于 xml 和 html 都有很好的支持,可以分别使用 lxml.etree
和 lxml.html
两个模块。
网页下载以后是 bytes 形式,需要构造 DOM 树:
In [1]: html = b"""
...: <p>hello<span id="world">world</span>tail</p>
...: """
In [2]: import lxml.html
In [3]: doc = lxml.html.fromstring(html)
In [4]: doc
Out[4]: <Element p at 0x1059aa408>
lxml 生成的树是一个设计很精妙的结构,
下面演示了 lxml 的常见用法:
In [6]: doc.tag
Out[6]: "p"
# 直接当作数组读取子元素
In [7]: doc[0].tag
Out[7]: "span"
# 遍历一个节点的所有属性
In [11]: for k, v in doc[0].items():
...: print(k, v)
id world
# 使用 get 读取属性
In [12]: doc[0].get("id")
Out[12]: "world"
# attrib 直接读取所有属性
In [13]: doc[0].attrib
Out[13]: {"id": "world"}
doc 是一个树形结构,可以通过一些方法访问树中的其他节点:
In [14]: doc.getroottree() # 返回树的根
Out[14]: <lxml.etree._ElementTree at 0x105360708>
In [19]: doc.getroottree().getroot() # 返回根节点,这里是 lxml 自动生成的 html 节点
Out[19]: <Element html at 0x10599da98>
In [20]: doc.getparent() # lxml 自动生成的 body 节点
Out[20]: <Element body at 0x1059a87c8>
# p 的前一个节点,在这里是空的
In [21]: doc.getprevious()
# p 的后一个节点,在这里也是空的
In [22]: doc.getnext()
# 再次转换成文本
In [25]: lxml.html.tostring(doc, pretty_print=True, encoding="utf-8")
Out[25]: b"<p>hello<span id="world">world</span></p>\n"
注意因为我们给的是一个 html 的片段(<p>...</p>
),所以 lxml 自动生成了 html 和 body 等节点以构成完整的 html 文档。但是 fromstring 返回的还是对应的节点,而不是 html 节点。
如果需要显式地指定生成一个 html 片段文档还是完整文档,可以分别使用:lxml.html.fragment_fromstring
和 lxml.html.document_fromstring
两个方法。
lxml 还有其他一些方法,都列在下面了:
修改文档树结构
查找遍历元素
HtmlElement.get_element_by_id(id, *default)
按照 id 查找元素*
修正链接
XPath 实在太强大了,在定位元素方面绝对秒杀 CSS 选择器。在 lxml 中,节点和树都具有 xpath 函数。lxml 中的 xpath 方法总是返回一个数组,即使选取结果只有一个元素。
In [44]: p.xpath("//span")
Out[44]: [<Element span at 0x107be96d0>]
xpath 表达式的语法不在这里赘述,可以查看本站的相关教程。
lxml 中的 xpath 函数支持变量,这样省了自己拼接字符串了
print(root.xpath("$text", text="Hello World!"))
Hello World!
lxml 还支持几个函数 find/findall
,他们使用 ElementPath,是一种类似 xpath 的语言,感觉很是奇怪,lxml 的文档描述他是 xpath 的一个子集,不建议使用。
在 lxml 中读取一个 html 节点的文本有四种方式,各有利弊:
text_content
方法,这种方法是推荐的,唯一的问题是,会把格式丢掉,只剩下纯文本。lxml.html.tostring(node)
方法,这样就直接把In [8]: doc = lxml.html.fragment_fromstring("<p>Hello<br/>World</p>")
# 读取当前节点包含的文本,只包含自身的文本,并不包含子标签,所以并不常用
In [9]: doc.text
Out[9]: 'Hello'
# 特别需要注意的是,往往还需要使用 tail,来读取最后一个字标签后的文本
In [42]: doc[0].tail
Out[42]: 'World'
In [40]: doc.xpath("./text()")
Out[40]: ['Hello', 'World']
In [10]: doc.text_content()
Out[10]: 'HelloWorld'
In [43]: lxml.html.tostring(doc)
Out[43]: b'<p>Hello<br>World</p>'
另外,包含 text()
的 xpath 返回的是_ElementUnicodeResult
, 而不是 str
,这个类型是 Element 和 str 的子类,一般情况下也可以当作 str 用,但是因为还引用了树中的结构,所以是不可以 pickle 的。有两个解决方法:
str(s)
xpath(smart_strings=False)
直接返回字符串lxml 在遇到正文包含小于号<
的时候会出问题,直接把后面的文档都丢了。虽然按照标准,<
应该编码为 <
,但是浏览器兼容性比较好,不会有问题。
网页不规范,有多个标签,浏览器可以解析,但是 lxml 不可以解析。case:http://ggzyjy.quanzhou.gov.cn/govProcurement/govProcurementDetail.do?bltId=178813¢erId=-1
对于这些不规范的网页,可以使用 html5parser,这个 parser 非常慢,但是兼容性要好很多
from lxml.html import html5parser
在网页中经常出现 <
, &
, &0x0026;
这些特殊字符,这是 html 实体字符转义,用于防止 XSS 攻击。Python3 标准库中包含了 html.entities 模块,可以用于转义和反转义这些字符。
html.entities.entitydefs 中包含了名称到符号的映射:比如{"amp": "&"}
html.entities.name2codepoint 中包含了名称到数字的映射:比如 {"amp": 0x0026}
html.entities.codepoint2name 中包含了数字到名称的映射:比如 {0x0026: "amp"}
lxml.html.fromstring 无法正确处理 emoji str,但是 encode 成 bytes 之后没有问题……
# 注意最后的 encode
In [70]: lxml.html.tostring(lxml.html.fromstring('<!DOCTYPE html>\n<html"><title>😄</title></html>'.encode()))
Out[70]: b'<html><head><title>😄</title></head></html>'
In [71]: lxml.html.tostring(lxml.html.fromstring('<!DOCTYPE html>\n<html"><title>😄</title></html>'))
Out[71]: b'<html><body><p>! D O C T Y P E h t m l > \n </p></body></html>'
一个比较 hack 的方法:
xml.replace(' xmlns="', ' xmlnamespace="', 1)
不过这个方法可能出现错误替换,所以要谨慎使用,最好能用正则来修复一下这个问题。
© 2016-2022 Yifei Kong. Powered by ynotes
All contents are under the CC-BY-NC-SA license, if not otherwise specified.
Opinions expressed here are solely my own and do not express the views or opinions of my employer.
友情链接: MySQL 教程站