Python Xpath 的使用

Xpath 的使用


正则表达式 笔记整理

Python requests 模块

在用 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/">&#32418;&#27004;&#26790;</a>
        </li><li class="pl2"><a href="https://book.douban.com/subject/4913064/">&#27963;&#30528;</a></li>
        <li class="pl2"><a href="https://book.douban.com/subject/6082808/">&#30334;&#24180;&#23396;&#29420;</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"><a href="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 选取当前节点之后的所有节点

更多轴的详细用法可参考:https://www.w3school.com.cn/xpath/xpath_axes.asp

使用轴的语法:

轴名称::节点测试[谓语]

沿用上面的例子,关于轴的简单实例:

例子 结果
//li/ancestor:: * 选取 li 节点的所有祖先节点
//li/ancestor::div 这里加了 div 加以限定,所以仅返回 div 节点
//li/attribute:: * 获取 li 节点的所有属性
//li/child::a[@href="#"] 这里加了限定条件,所以仅返回 href 属性为 # 的 a 节点
//li/descentdant:: * 获取 li 节点的所有子孙节点
//li/following:: * 获取 li 节点后续的所有节点

小结

以上就是关于 Xpath 的内容, Xpath 还有一些函数,文章未提及,如果有兴趣的话,可以参考:https://www.w3school.com.cn/xpath/xpath_functions.asp


欢迎关注微信公众号《书所集录》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,635评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,628评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,971评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,986评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,006评论 6 394
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,784评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,475评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,364评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,860评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,008评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,152评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,829评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,490评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,035评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,156评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,428评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,127评论 2 356

推荐阅读更多精彩内容