更新于 

Xpath

Reference: Xpath的使用(详细教学)


1. Xpath的概览

XPath的选择功能十分强大,他提供了非常简洁明了的路径选择表达式。另外,它还提供了100多个内建函数,
用于字符串,数值,时间的匹配以及节点,序列的处理。几乎所有我们想要定位的节点。都可以用XPath选择。

2. XPath常用规则

表达式 描述
nodename 选取此节点的所有子节点
/ 从当前节点选取直接子节点
// 从当前节点选取子孙节点
. 选取当前节点
. . 选取当前节点的父节点
@ 选取属性
这里列出了XPath的一个常用匹配规则,如下:
//title[@lang='eng']
他代表选择所有名称为title,同时属性lang的值为eng的节点。
后面会通过python的lxml库,利用XPath对HTML进行解析

3. 准备工作

使用lxml库之前,首先要确保其已经安装好。
可以用使用pip3来安装
pip3 install lxml

4. 实例引用

下面通过实例感受一下使用Xpath对网页进行解析的过程,相关代码如下:

from lxml import etree
text = '''
<div>
<ul>
<li class="item-0"><a href="link1.html">first item</a></li>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-inactive"><a href="link3.html">third item</a></li>
<li class="item-1"><a href="link4.html">fourth item</a></li>
<li class="item-0"><a href="link5.html">fifth item</a>
</ul>
</div>
'''
html = etree.HTML(text)
result = etree.tostring(html)
print(result.decode('utf-8'))

这里首先导入lxml库的etree模块,然后声明了一段HTML文本,接着调用HTML类进行初始化,这样就构造了一个XPath解析对象,此处需要注意一点,HTML文本中的最后一个li节点是没有闭合的,而etree模块可以自动修正HTML文本。
之后调用tostring方法即可输出修正后的HTML代码,但是结果是bytes类。于是利用decode方法将其转换成str类型,结果如下:

<html><body><div>
<ul>
<li class="item-0"><a href="link1.html">first item</a></li>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-inactive"><a href="link3.html">third item</a></li>
<li class="item-1"><a href="link4.html">fourth item</a></li>
<li class="item-0"><a href="link5.html">fifth item</a>
</li></ul>
</div>
</body></html>

可以看到,经过处理之后li节点标签得以补全,并且自动添加了body、html节点。
另外,也可以不声明,直接读取文本进行解析,示例如下:

from lxml import etree

html = etree.parse('./test.html', etree.HTMLParser())
result = etree.tostring(html)
print(result.decode('utf-8'))

期中test.html的内容就是上面列子中的HTML代码,内容如下:

<div>
<ul>
<li class="item-0"><a href="link1.html">first item</a></li>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-inactive"><a href="link3.html">third item</a></li>
<li class="item-1"><a href="link4.html">fourth item</a></li>
<li class="item-0"><a href="link5.html">fifth item</a>
</ul>
</div>

这次输出的结果略有不同,多了一个DOCTYPE声明,不过对解析无任何影响,结果如下:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><div>&#13;
<ul>&#13;
<li class="item-0"><a href="link1.html">first item</a></li>&#13;
<li class="item-1"><a href="link2.html">second item</a></li>&#13;
<li class="item-inactive"><a href="link3.html">third item</a></li>&#13;
<li class="item-1"><a href="link4.html">fourth item</a></li>&#13;
<li class="item-0"><a href="link5.html">fifth item</a>&#13;
</li></ul>&#13;
</div></body></html>

5. 所有节点

我们一般会用以//开头的XPath规则,来选取所有符合要求的节点。这里还是以第一个实例中的HTML文本为例,选取其中所有节点,实现代码如下:

from lxml import etree

html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//*')
print(result)

运行结果如下:

[<Element html at 0x1a5aa580b48>, <Element body at 0x1a5aa580c48>, <Element div at 0x1a5aa580c88>, <Element ul at 0x1a5aa580cc8>, <Element li at 0x1a5aa580d08>, <Element a at 0x1a5aa580d88>, <Element li at 0x1a5aa580dc8>, <Element a at 0x1a5aa580e08>, <Element li at 0x1a5aa580e48>, <Element a at 0x1a5aa580d48>, <Element li at 0x1a5aa580e88>, <Element a at 0x1a5aa580ec8>, <Element li at 0x1a5aa580f08>, <Element a at 0x1a5aa580f48>]

这里使用*代表匹配所有节点,也就是获取整个HTML文本中的所有节点。从运行结果可以看到返回形式是一个列表,其中每个元素是Element类型,类型后面跟这节点的名称,如html、body、div等所有节点都包含在列表中。
当然,此处也可以匹配指定节点名称。列如想获取li节点,示例如下:

from lxml import etree

html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//li')
print(result)
print(result[0])

这里选取所有li节点,可以使用//,然后直接加上节点名称,调用时使用xpath方法即可。
运行结果如下:

[<Element li at 0x1c66d060c48>, <Element li at 0x1c66d060c88>, <Element li at 0x1c66d060cc8>, <Element li at 0x1c66d060d08>, <Element li at 0x1c66d060d48>]
<Element li at 0x1c66d060c48>

可以看到,提取结果也是一个列表,其中每个元素都是Element类型。要是想取出一个对象可以直接用中括号加索引获取,如[0]。

6. 子节点

通过/或//即可查找元素的子节点或子孙节点。假如现在想选择li节点的所有直接子节点a,则可以这样实现:

from lxml import etree

html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//li/a')
print(result)

这里通过追加/a的方式,选择了所有li节点的所有直接子节点a。其中//li用于选中所有li节点,/a用于选中li节点的所有直接子节点啊。
运行结果如下:

[<Element a at 0x2592a090c48>, <Element a at 0x2592a090c88>, <Element a at 0x2592a090cc8>, <Element a at 0x2592a090d08>, <Element a at 0x2592a090d48>]

上面的/用于选取节点的直接子节点,如果要获取节点的所有子孙节点,可以使用//。例如要获取ul节点下面的所有子孙节点a,可以这样实现:

from lxml import etree

html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//ul//a')
print(result)

运行结果是相同的。
但是如果这里用//ul/a,就无法获取结果了,因为/用于获取直接的子节点,而ul节点下没有直接的a子节点,只有li节点,所以无法获取任何匹配结果如下:

from lxml import etree

html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//ul/a')
print(result)

运行结果如下:

[]

因此这里要注意/和//的区别,前者用于获取直接子节点,后者用于获取子孙节点

7. 父节点

如何查找父节点呢?
示例如下:
首先选中href属性为link4.html的a节点,然后获取其父节点,在获取父节点的class属性,相关代码如下:

from lxml import etree

html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//a[@href="link4.html"]/../@class')
print(result)

运行结果如下:

['item-1']

可以通过观察这正是我们获取a标签属性为href为link4.html的父节点li节点的class属性
我们也可以用parent::获取父节点,代码如下:

from lxml import etree

html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//a[@href="link4.html"]/parent::*/@class')
print(result)

结果相同都是[‘item-1’]

8. 属性匹配

在选取节点的时候,还可以使用@符号实现属性过滤。例如,要选取class属性为item-0的li接地但,可以这样实现:

from lxml import etree

html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//li[@class="item-0"]')
print(result)

这里通过加入[@class=‘item-0’],限制了节点的class属性为item-0.HTML文本中符合这个条件的li节点有两个,所以结果因该返回两个元素。
结果如下:

[<Element li at 0x23e52010c48>, <Element li at 0x23e52010c88>]

可见,匹配结果因该返回两个元素,后面在验证

9. 文本获取

用Xpath中的text方法可以获取节点中的文本,接下来尝试获取前面li节点中的文本,相关代码如下:

from lxml import etree

html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//li[@class="item-0"]/text()')
print(result)

运行结果为:

['\r\n     ']

奇怪的是,我们没有获取任何文本,只获取了一个换行符,这是为什么呢?因为xpath中text方法的前面是/,而/的含义就是直接选取子节点,很明显li的直接子节点都是a节点,文本都是在a节点内部的,所以这里匹配到的结果就是被修正的li节点内部的换行符,因为自动修正的li节点的尾标签换行了。
及选中的是这两个节点:

<li class="item-0"><a href="link1.html">first item</a></li>
<li class="item-0"><a href="link5.html">fifth item</a>&#13;
</li>

其中一个节点因为自动修正,li节点的尾标签在添加的时候换行了,所以提取文本得到的唯一结果就是li节点的尾标签和a节点的尾标签之前的换行符。

因此,如果想获取li节点内部的文本,就有两种方法,一种是先选取a节点在获取文本,另一种是使用//。接下来我们看两种的区别。


from lxml import etree

html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//li[@class="item-0"]/a/text()')
print(result)

运行结果如下:

['first item', 'fifth item']

可以看到,这里返回两个值,内容都是class属性为item-0的li节点的文本,这也印证了前面属性匹配的结果是正确的。
这种方式下,我们是逐层选取的,先选取li节点,然后利用/选取其直接子节点a,在选取a的文本,得到的两个结果恰好符合我们的预期。

再看一下啊使用//能够获取什么结果

from lxml import etree

html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//li[@class="item-0"]//text()')
print(result)

运行结果如如下:

['first item', 'fifth item', '\r\n     ']

不出所料,这里的返回结果是三个。可想而知这里选取的是所有子孙节点的文本,其中前两个是li的子节点a内部的文本,另外一个是最后一个li节点内部的文本,及换行符

如果需要选取子孙节点内部的所有文本,直接使用//text,这样可以获得全部信息,但是也会有一些换行符等特殊字符,如果获取特定子孙节点的所有文本,则先逐层选择在用text方法

10. 属性获取

获取li节点下面的所有a节点的href

from lxml import etree

html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//li/a/@href')
print(result)

这里通过@href获取节点的href属性,注意此处和属性匹配的方法不同,属性区别是用中括号加属性名和值来限定某个属性,如[@href=“link1.html”],此处的@href是指获取节点的某个属性,二者需要做好区分。
运行结果如下:

['link1.html', 'link2.html', 'link3.html', 'link4.html', 'link5.html']

可以看到,我们成功获取了所有li节点下a节点的href属性,并以列表形式返回了他们。

11. 属性多值匹配

有时候,某些节点的属性可能有多个值,例如:

from lxml import etree
text = '''
<li class="li li-first"><a href="link.html">first item</a></li>
'''
html = etree.HTML(text)
result = html.xpath('//li[@class="li"]/a/text()')
print(result)

这里HTML文本中li节点的class属性就有两个值:li和 li-first。此时如果还用之前的属性匹配获取节点,就无法获取到了
运行结果如下:

[]

这种情况需要用到contains方法(和jquery中的contains方法一致)

from lxml import etree
text = '''
<li class="li li-first"><a href="link.html">first item</a></li>
'''
html = etree.HTML(text)
result = html.xpath('//li[contains(@class,"li")]/a/text()')
print(result)

上面使用了contains方法,给其第一个参数传入属性名称,第二个参数传入属性值,只要传入的属性包含传入的属性值,就可以完成匹配了。
此时运行结果如下:

['first item']

contains方法在某个节点的某个属性有多个值时用到

12. 多属性匹配

我们还可能遇到一种情况,就是更具多个属性确定一个节点,这是需要同事匹配多个属性。运算符and用于连接多个属性,实例如下:

from lxml import etree
text = '''
<li class="li li-first" name="item"><a href="link.html">first item</a></li>
'''
html = etree.HTML(text)
result = html.xpath('//li[contains(@class, "li") and @name="item"]/a/text()')
print(result)

这里的li节点又增加了一个name属性,因此要确定li节点,需要同时考察class和name属性,一个条件是class属性里面包含了li字符串,另一个条件是name属性为item字符串,这二者同时得到满足,才是li节点。class和name属性需要用到and运算符相连。
结果如下:

['first item']

以下列举了其他运算符号

运算符 描述 实例 返回值
or age=19 or age=20 如果age是19则返回True。如果age是21则返回false 两个条件满足一个即可
and age>19 and age<20 如果age是20则返回True。如果age是18则返回false 两个条件同时满足即可
mod 计算除法的余数 5 mod 2 1
计算两个节点集 //book //cd 返回所有拥有book和cd元素的节点集
**加减乘除 + - * div(表示除法)
大于小于等比较运算符 不举例说明**

13. 按序选择

在选择节点时,某些属性可能同时匹配了多个节点,但我们只想要其中一个,如第二个或者最后一个
可以使用往中括号中传入索引的方法获取特定次序的节点如下:

from lxml import etree

text = '''
<div>
<ul>
<li class="item-0"><a href="link1.html">first item</a></li>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-inactive"><a href="link3.html">third item</a></li>
<li class="item-1"><a href="link4.html">fourth item</a></li>
<li class="item-0"><a href="link5.html">fifth item</a>
</ul>
</div>
'''
html = etree.HTML(text)
result = html.xpath('//li[1]/a/text()')
print(result)
result = html.xpath('//li[last()]/a/text()')
print(result)
result = html.xpath('//li[position()<3]/a/text()')
print(result)
result = html.xpath('//li[last()-2]/a/text()')
print(result)

第一个print 往中括号中传入索引[1],即可获取第一个数据,注意序号以1开头,而不是0
第二个last()方法表示最后一个
第三个position()❤️ 选取了位置小于3的li节点也就是第一个和第二个节点
第四次last()-2则表示倒数第三个
结果如下:

['first item']
['fifth item']
['first item', 'second item']
['third item']

在这个实例中我们使用了last、position等方法。xpath提供了100多个方法,包括存取、数值、字符串、逻辑、节点、序列等处理功能

节点集函数 

last() 返回当前上下文中的最后一个节点的位置号数。
position() 返回当前节点的位置的数字,位于第多少个。
count(node-set) 返回节点集node-set中的节点数。
id(mark) 根据在DTD中声明为ID类型的标识符选择元素,返回一个节点集。
name() 返回节点名称。
local-name() 返回不带名称空间的节点名称。
namespace-uri() 返回名称空间。

字符串函数

string(object) 把节点集、数字、布尔值等转化成字串并返回。
format-number(num) 把数字转化成字串并返回。
concat(string1,string2...) 合并多个字串并返回。
starts-with(string1,string2) 如果字串string1开头带string2的所有字符则返回true,否则返回false。
contains(string1,string2) 如果字串string1包含string2的所有字符则返回true,否则返回false。
substring(string,number1,number2) 取string中从位置number1开始,number2长的子串,number2可省略。
substring-before(string1,string2) 取string1在string2第一次出现位置之前的子串。
substring-after(string,string) 取string1在string2第一次出现位置之后的子串。
string-length(string) 返回string的长度数字。
normalize-space(string) 清除string头尾的空白字符并且把连续的空白字符替换为一个再返回。
translate(string1,string2,string3) 假如string1中的字符在string2中有出现,那么替换为string3对应string2的同一位置的字符,假如string3这个位置取不到字符则删除string1的该字符。

布尔函数

boolean(object) 非0和NaN的数字/非空节点/长度大于0的字串返回true,非基本类型的转换有时无法估计。
not(boolean) 对布尔值取反。
true() 返回true。
false() 返回false。
lang(string) 如果上下文节点的lang属性和string相同则返回true。

数字函数

number(object) 使对象转化成数字,布尔值true为1,false为0;节点集首先转换成字符串,字符串转换成数字或者NaN。
sum(node-set) 对节点集node-set中的所有节点应用number()函数后返回和。
floor(number) 返回不大于数字number的最大整数。
ceiling(number) 返回不小于数字number的最小整数。
round(number) 返回和数字number的四舍五入结果。

14. 节点轴选择

XPath提供了很多节点轴的选择方法,包括获取子元素,兄弟元素,父元素,祖先元素等,实例如下:

from lxml import etree

text = '''
<div>
<ul>
<li class="item-0"><a href="link1.html"><span>first item</span></a></li>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-inactive"><a href="link3.html">third item</a></li>
<li class="item-1"><a href="link4.html">fourth item</a></li>
<li class="item-0"><a href="link5.html">fifth item</a>
</ul>
</div>
'''
html = etree.HTML(text)
result = html.xpath('//li[1]/ancestor::*')
print(result)
result = html.xpath('//li[1]/ancestor::div')
print(result)
result = html.xpath('//li[1]/attribute::*')
print(result)
result = html.xpath('//li[1]/child::a[@href="link1.html"]')
print(result)
result = html.xpath('//li[1]/descendant::span')
print(result)
result = html.xpath('//li[1]/following::*[2]')
print(result)
result = html.xpath('//li[1]/following-sibling::*')
print(result)

运行结果如下:

[<Element html at 0x2458d4d6bc8>, <Element body at 0x2458d4d6b48>, <Element div at 0x2458d4d6b08>, <Element ul at 0x2458d4d6c08>]
[<Element div at 0x2458d4d6b08>]
['item-0']
[<Element a at 0x2458d4d6c08>]
[<Element span at 0x2458d4d6b08>]
[<Element a at 0x2458d4d6c08>]
[<Element li at 0x2458d4d6b48>, <Element li at 0x2458d4d6c48>, <Element li at 0x2458d4d6c88>, <Element li at 0x2458d4d6cc8>]

首先介绍关于xpath轴的方法

ancestor选取当前节点的所有先辈(父、祖父等)。
ancestor-or-self选取当前节点的所有先辈(父、祖父等)以及当前节点本身。
attribute选取当前节点的所有属性。
child选取当前节点的所有子元素。
descendant选取当前节点的所有后代元素(子、孙等)。
descendant-or-self选取当前节点的所有后代元素(子、孙等)以及当前节点本身。
following选取文档中当前节点的结束标签之后的所有节点。
following-sibling选取当前节点之后的所有兄弟节点
namespace选取当前节点的所有命名空间节点。
parent选取当前节点的父节点。
preceding选取文档中当前节点的开始标签之前的所有节点。
preceding-sibling选取当前节点之前的所有同级节点。
self选取当前节点。