Python爬虫神器Xpath的使用
在用 python 实现爬虫时,可以使用 requests 库访问资源,然后用正则表达式提取信息。
但是,这里会有一些繁琐,因为正则表达式的书写是比较严格的,万一有一个地方写错了,可能会导致匹配失败无法提取需要的信息。
对于网页的节点来说,可以定义 id、class 或其他属性。节点之间有层次关系,在网页中,其实可以通过 xpath 定位一个或多个节点。
那么相应的,在页面解析的时候,利用 xpath 定位节点,调用相应的方法获取正文或者属性,那么完全可以获取需要的信息。
在 python 中,这个解析库叫 lxml,下面来介绍这个解析库的用法。
lxml 库
lxml 是 python 的一个解析库,支持 html 和 xml 的解析,支持 xpath 解析方式,效率非常高。
使用 lxml 之前,需要先安装,可以使用如下命令:
$ pip install lxml
xpath 简介
xpath,全称 xml path language,即是 xml 路径语言。xpath 是一门在 xml 文档中查找信息的语言,用于在 xml 文档中通过元素和属性进行导航,但同样适用于 html 文档的搜索。
在实现爬虫时,完全可以通过 xpath 进行信息提取。
xpath 的功能强大,使用路径表达式来选取 xml 或 html 文档中的节点或者节点集。xpath 有超过 100 个内建的函数。这些函数可用于字符串、数值、日期和时间比较、节点、序列处理和逻辑值等等。
xpath 于 1999 年 11 月 16 日成为 w3c 标准,被设计为供 xslt、xpointer 以及其他 xml 解析软件使用。
xpath 语法
前面提及了,xpath 使用路径表达式选取文档中的节点或节点集。
下面罗列常用的路径表达式:
表达式 | 描述说明 |
---|---|
nodename | 选取此节点的所有子节点 |
/ | 从根节点选取 |
// | 从当前节点选择子孙节点(不考虑它们的位置) |
. | 选取当前节点 |
.. | 选取当前节点的父节点 |
@ | 选取属性 |
上面罗列的内容属于常用部分,用示例来说明下具体的用法:
//div[@class="document"]
这就是一个 xpath 路径表达式,代表的是选择名称为 div,属性 class 的值为 document 的节点。
在 python 中,会通过 lxml 库,利用 xpath 进行解析。
实例应用
通过实例了解使用 xpath 对网页进行解析的过程,代码如下(下面 html 内容节选自豆瓣,稍作更改):
# 先导入 lxml 库 from lxml import etree text = """ <div> <ul> <li class="pl2"><a href="https://book.douban.com/subject/1007305/">红楼梦</a> <li class="pl2"><a href="https://book.douban.com/subject/4913064/">活着</a></li> <li class="pl2"><a href="https://book.douban.com/subject/6082808/">百年孤独</a></li> <li class="pl1"><a href="https://book.douban.com/subject/4820710/">1984</a></li> </ul> </div> """ html = etree.html(text) result = etree.tostring(html) print(result.decode('utf-8'))
在上面的实例中,先导入 lxml 库中的 etree 模块,声明一段 html 文本,然后使用 etree 的 html 类进行初始化,构造一个 xpath 解析对象。在这里需要注意一点,实例中,声明的 html 文本第 1 个节点没有闭合,但是 etree 模块会自动修正。
etree.tostring() 方法用于输出修正后的 html 内容,不过该方法返回的是 byte 类型,输出的时候需要进行解码转换为 str 类型。
上面的输出结果如下:
<html><body><div> <ul> <li class="pl2"><a href="https://book.douban.com/subject/1007305/">红楼梦</a> </li><li class="pl2"><a href="https://book.douban.com/subject/4913064/">活着</a></li> <li class="pl2"><a href="https://book.douban.com/subject/6082808/">百年孤独</a></li> <li class="pl1"><a href="https://book.douban.com/subject/4820710/">1984</a></li> </ul> </div> </body></html>
在这里可以看到 li 节点标签已经补全,同时自动添加了 body、html 节点。
上面的代码中,中文没有正常显示。这里属于编码的问题,可以将上面的代码稍微修改一下:
result = etree.tostring(html, encoding='gbk') print(result.decode('gbk'))
再看输出结果:
<?xml version='1.0' encoding='gbk'?> <html><body><div> <ul> <li class="pl2"><a href="https://book.douban.com/subject/1007305/">红楼梦</a> </li><li class="pl2"><a href="https://book.douban.com/subject/4913064/">活着 </a></li> <li class="pl2"><a href="https://book.douban.com/subject/6082808/">百年孤独< /a></li> <li class="pl1"><a href="https://book.douban.com/subject/4820710/">1984</a>< /li> </ul> </div> </body></html>
这里有所不同,前面多了个声明,同时标记编码方式为 gbk。
另外, lxml 库也可以直接读取文件进行解析,示例如下(先将上面的未修正的 html 内容放到 example.html 文件中):
from lxml import etree html = etree.parse('./example.html', etree.htmlparser()) result = etree.tostring(html) print(result.decode('utf-8'))
这个时候输出的结果会多一个 doctype 的声明。
xpath 节点
所有节点
以 //
开头的 xpath 表达式为选取所有符合要求的节点,沿用上面的例子:
... result = html.xpath('//*') print(result)
运行结果:
[<element html at 0x4b34fc8>, <element body at 0x4b3b108>, <element div at 0x4b3b088>, <element ul at 0x4b3b148>, <element li at 0x4b3b188>, <element a at 0x4b3b208>, <element li at 0x4b3b248>, <element a at 0x4b3b288>, <element li at 0x4b3b2c8>, <element a at 0x4b3b1c8>, <element li at 0x4b3b308>, <element a at 0x4b3b588>]
在这里, *
表示匹配所有的节点,由运行结果可以看出,返回的列表中,包括了 html, body, div, ul, li, a
所有节点。
当然 //
后面可以跟特定的节点,例如:
... result = html.xpath('//a') print(result)
运行结果:
[<element a at 0x2d1d688>, <element a at 0x2d1d648>, <element a at 0x2d1d748>, <element a at 0x2d1d788>]
子节点
/
或者 //
可以用来定位子节点或者子孙节点,例如定位 li 节点的所有 a 节点:
... result = html.xpath('//li/a') print(result)
运行结果:
[<element a at 0x2cfd688>, <element a at 0x2cfd648>, <element a at 0x2cfd748>, <element a at 0x2cfd788>]
在这里可以看到,与上面直接用 //a
表达式获取的结果相同,但这里有所区别,//a
表达式找的所有的 a 节点,//li/a
这里找的是所有 li 节点的所有直接 a 子节点。
比如,有如下标签内容:
<title><a href="link.html">title</a></title>
用这个示例来区分,根据上面的区分解释,在这里用 //a
是可以匹配到这项内容,但是 //li/a
则匹配不到,因为示例中 a 节点并非 li 节点的直接子节点。
在原来的 html 文档内容中,a 是 li 的直接节点,也是 ul 的子孙节点,那么要定位 a 节点,也可以按照如下的表达式来写:
... result = html.xpath('//ul//a') print(result)
这里得到的结果跟上面是一致的:
[<element a at 0x2cfd688>, <element a at 0x2cfd648>, <element a at 0x2cfd748>, <element a at 0x2cfd788>]
但是要注意,不能够写成 //ul/a
,因为 a 并非 ul 的直接子节点,如果这样写则无法匹配,返回空列表。
所以要对 /
和 //
加以区分,/
用于获取直接子节点,//
用于获取子孙节点。
父节点
获取父节点的信息,用 ..
来实现,例如:
<li class="p12"><a href="https://book.douban.com/subject/1007305/"></a>红楼梦</li>
想要获取 href 属性为 "https://book.douban.com/subject/1007305/"
的 a 节点的父节点属性。
代码如下:
... result = html.xpath('//a[@href="https://book.douban.com/subject/1007305/"]/../@class') print(result)
运行结果:
['pl2']
这个结果正是父节点的属性。
属性
节点中,属性可存在单值或多值的情况,一个节点也可以有多个属性,当出现这些情况时,使用的表达式往往不能够一成不变,需要针对性进行书写。
单值匹配
在上面的例子中,其实已经使用属性匹配,@
符号用于属性过滤。在上面的例子当中,有一个属性跟其他的不同,现在将其定位,代码实现:
... result = html.xpath('//li[@class="pl1"]') print(result)
运行结果:
[<element li at 0x2cfd688>]
[@class="pl1"]
这部分对定位进行了限制,找的是 class 属性值为 pl1 的节点。
多值匹配
属性有时候可能不止 1 个,如下示例:
<li class="pl1 pl2"><a href="https://book.douban.com/subject/4820710/">1984</a></li>
将 li 的属性值改为 pl1 pl2
,如果还是用原来的表达式的话:
... result = html.xpath('//li[@class="pl1"]') print(result)
得到的是空列表:
[]
这个时候,要考虑使用 contains()
方法,这个方法需要的参数有:第一个参数是属性名称,第二个参数是属性值。该方法的实现过程是,若第一个参数属性包含第二个参数中的属性值,则可以匹配成功。例如:
... result = html.xpath('//li[contains(@class, "pl1")]') print(result)
运行结果:
[<element li at 0x2d1d648>]
这个方法在属性值不止 1 个的情况下,非常有用。
多属性匹配
在节点中,除了单个属性可以有多个值之外,也可以有多个属性。假设有如下节点:
<li class="pl1 pl2" name="item"><ahref="https://book.douban.com/subject/4820710/">1984</a></li>
这种情况要用到 xpath 运算符,下面罗列常用的运算符:
运算符 | 描述 | 实例 | 返回值 |
---|---|---|---|
丨 | 计算两个节点集 | //book 丨 //cd | 返回拥有 book 和 cd 元素的节点 |
+ | 加法 | 6 + 4 | 10 |
- | 减法 | 6 - 4 | 2 |
* | 乘法 | 6 * 4 | 24 |
div | 除法 | 9 div 3 | 3 |
= | 等于 | stature=178 | 当 stature 为 178 时,返回 true;否则,返回 false. |
!= | 不等于 | stature!=178 | 当 stature 不是 178 时,返回 true;否则,返回 false |
< | 小于 | stature<178 | 当 stature 为 177 时,返回 true;当 stature 为 179 时,返回 false |
<= | 小于或等于 | stature<=178 | 当 stature 为 177 时,返回 true;当 stature 为 179 时,返回 false |
> | 大于 | stature>178 | 当 stature 为 179 时,返回 true;当 stature 为 177 时,返回 false |
>= | 大于 | stature>=178 | 当 stature 为 179 时,返回 true;当 stature 为 177 时,返回 false |
or | 或 | stature=178 or stature=179 | 当 stature=178 时,返回 true;当 stature=175 时,返回 false |
and | 与 | stature>175 and stature<178 | 当 stature=178 时,返回 true;当 stature=165 时,返回 false |
mod | 取余 | 5 mod 2 | 1 |
在这里,使用 and 运算符将多个属性连接:
... result = html.xpath('//li[contains(@class, "pl1") and @name="item"]') print(result)
运算结果:
[<element li at 0x2cfd688>]
获取属性
这里要与上面区分开,上面都是根据属性去定位节点。现在是想查找某个节点的确切属性。例如查找 li 下 a 节点的 href 属性:
... result = html.xpath('//li/a/@href') print(result)
返回结果:
['https://book.douban.com/subject/1007305/', 'https://book.douban.com/subject/4913064/', 'https://book.douban.com/subject/6082808/', 'https://book.douban.com/subject/4820710/']
这里 /@href
是为了获取节点属性,上面 [@class="pl1"]
是为了限定属性查找节点,要加以区分。
文本获取
xpath 用 text() 方法获取文本,现在尝试获取上面属性所演示的示例,获取节点中的文本,同时验证上面定位的是否是属性值为 pl1 的节点:
... result = html.xpath('//li[@class="pl1"]/a/text()') print(result)
运行结果:
['1984']
从结果来看,上面属性示例中返回的节点,的确是属性值为 pl1 的节点。这里需要注意,因为文本是被 a 节点包裹着的,如果直接在 li 节点下使用 /text()
是获取不到想要的信息的。如果改成 //text()
表达式,则可以获取所有子孙节点的文本,但这里可能获取的内容会有些偏差,有可能会获取到换行符,这个并不是想要的信息。如下示例:
result = html.xpath('//li[@class="pl1"]//text()') print(result) # 输出结果: # ['\n ', '1984', '\n ']
这里就是需要注意的地方,如果要想获取特定子节点的文本,首先建议先找到特定的子节点,然后在子节点下使用 text()
方法,这样确保获取的信息是整洁的。
xpath 轴
轴可定义相对当前节点的节点集。
先罗列一些简单的轴及其含义:
轴名称 | 含义 |
---|---|
ancestor | 选取当前节点的所有祖先节点 |
attribute | 选取当前节点的所有属性 |
child | 选取当前节点的所有直接子节点 |
descendant | 选取当前节点的所有子孙节点 |
following | 选取当前节点之后的所有节点 |
更多轴的详细用法可参考:
使用轴的语法:
轴名称::节点测试[谓语]
沿用上面的例子,关于轴的简单实例:
例子 | 结果 |
---|---|
//li/ancestor:: * | 选取 li 节点的所有祖先节点 |
//li/ancestor::div | 这里加了 div 加以限定,所以仅返回 div 节点 |
//li/attribute:: * | 获取 li 节点的所有属性 |
//li/child::a[@href="#"] | 这里加了限定条件,所以仅返回 href 属性为 # 的 a 节点 |
//li/descentdant:: * | 获取 li 节点的所有子孙节点 |
//li/following:: * | 获取 li 节点后续的所有节点 |