Skip to content

使用XPath | lxml库

约 2060 个字 250 行代码 预计阅读时间 11 分钟

XPath简单介绍

XPath,全程为XML Path Language,即XML路径语言,最初用于搜寻XML文档内容,也同样适用于HTML文档。

XPath的强大之处在于简洁明了的路径选择表达式,丰富的可用于处理字符串、数值和节点等信息的内置函数,想要了解更多相关信息,请参阅此处

让我们来稍微了解一下XPath的几个常用规则:

表达式 描述
nodename 选取此节点的所有子节点
/ 从当前节点选取直接子节点
// 从当前节点选取子孙节点
. 选取当前节点
.. 选取当前节点的父节点
@ 选取属性

Python中主要通过lxml库来使用XPath,我们来看一个小示范:

//title[@lang='eng']

这就是一个XPath规则,它代表选择所有名称为title,同时属性为lang的值为eng的节点.

实例引入

闲话少说,先上实例!

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></li>
  </ul>
</div>
'''

html = etree.HTML(text=text)
result = etree.tostring(html)
print(result.decode('utf-8'))

# 输出为
'''
<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>
'''

在上面的实例中,我们导入了lxml库的etree模块,利用HTML类对HTML文本进行了初始化,成功构造了一个XPath解析对象。

调用tostring()方法可以输出HTML代码,其输出结果是bytes类型,利用decode()将其转换为str类型。

值得注意的是,原本的HTML是有“缺憾”的,首先是最后一个<li>并未封闭,其次参照一个完整的HTML文档,这段HTML文本还缺少<body><html>两个标签,但是etree模块都将其补齐了。

快说,谢谢etree

当然,lxml库也支持文本文件的读取,比如下面这个实例:

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

文件中的内容与上文相同,不过我们这次换成了通过读取文件来获得HTML代码,输出代码如下:

# 输出为
'''
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<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>
'''

与上一段代码输出结果不同在于,这次多出了一个DOCTYPE声明,但总体上不影响解析。

所有节点

我们一般喜欢采用//开头的XPath规则来选取所有符合要求的节点,下举一实例:

from lxml import etree

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

# 输出为
'''
[<Element html at 0x7f1a04ddbac0>, <Element body at 0x7f1a04660a80>, <Element div at 0x7f1a04660ac0>, <Element ul at 0x7f1a04660b00>, <Element li at 0x7f1a04660b40>, <Element a at 0x7f1a04660bc0>, <Element li at 0x7f1a04660c00>, <Element a at 0x7f1a04660c40>, <Element li at 0x7f1a04660c80>, <Element a at 0x7f1a04660b80>, <Element li at 0x7f1a04660cc0>, <Element a at 0x7f1a04660d00>, <Element li at 0x7f1a04660d40>, <Element a at 0x7f1a04660d80>]
'''

这里我们采用了//*来匹配所有节点,xpath()方法返回的是一个列表,列表中的每个元素都是Element类型,后跟有节点名称,例如htmlbody等等。

当然你也可以指定节点的类型:

from lxml import etree

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

# 输出为
'''
[<Element li at 0x7f54369b0ac0>, <Element li at 0x7f54369b0b00>, <Element li at 0x7f54369b0b40>, <Element li at 0x7f54369b0b80>, <Element li at 0x7f54369b0bc0>]
'''

检索结果同样是列表类型,可以通过索引访问各个元素。

子节点

到这里就不得不提一下直接子节点与子孙节点的区别了,在XPath中体现在///的不同,下举多个实例来说明:

  • 第一个实例
from lxml import etree

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

# 输出为
'''
[<Element a at 0x7fd0dae10a80>, <Element a at 0x7fd0dae10ac0>, <Element a at 0x7fd0dae10b00>, <Element a at 0x7fd0dae10b40>, <Element a at 0x7fd0dae10b80>]
'''
  • 第二个实例
from lxml import etree

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

# 输出为
'''
[]
'''
  • 第三个实例
from lxml import etree

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

# 输出为
'''
[<Element a at 0x7f4390974c40>, <Element a at 0x7f4390974c80>, <Element a at 0x7f4390974cc0>, <Element a at 0x7f4390974d00>, <Element a at 0x7f4390974d40>]
'''

让我们来看看在上面三个实例中,分别发生了什么:

  • 第一个实例:这里我们搜寻\\ul\\a,为<ul>下的子孙节点<a>,有返回值。
  • 第二个实例:这里我们搜寻\\ul\a,为<ul>下的直接子节点<a>,返回为空。
  • 第三个实例:这里我们搜寻\\li\a,为<li>下的直接子节点<a>,有返回值。

这里的关键就在直接子节点子孙节点的区别上,前者是当前节点的下一级节点,后者是属于当前节点的节点,如果你对中世纪西欧历史有点印象,那么直接子节点与子孙节点和父节点直之间可以理解为“我附庸的附庸不是我的附庸”这种关系。

父节点

既然可以查询子节点/子孙节点了,父节点当然也可以查啦,使用..(和Unix类OS内的cd ..指令真的很像啊!)

举一实例如下:

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

# 输出为
'''
['item-1']
'''

这里我们查询了一个属性有href=lin4.html<a>节点的父节点的属性class

当然还有可以采用parent::来获取父节点:

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

# 输出为
'''
['item-1']
'''

属性匹配

父节点中我们已经提前展示了一下属性匹配方法@,通过对属性进行匹配可以帮助我们有效过滤一些不需要的节点,举一实例如下:

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

# 输出为
'''
[<Element li at 0x7f79d4858d00>, <Element li at 0x7f79d4858d40>]
'''

文本获取

以上我们只是选择到了相应的节点,但实际上,很多时候我们更关心的是在各个节点中的文本内容,XPath提供了text()方法来获取相应节点中的内容,举一实例如下:

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

# 输出为
'''
['\n    ']
'''

奇怪的是我们并没有得到任何内容,除了一个换行符\n,这是为什么呢?

先让我们来研究一下我们所选中的节点文本:

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

值得注意的是,第二个选中的<li>节点在最初并未封闭,etree.parse()方法会将其补齐,具体方式是换行后补上</li>

我们选取的表达式是'//li[@class="item-0"]/text()'意思是选中带有指定属性的<li>节点下的内容,第一个被选中的节点中只有<a>节点,但是正如前文所说,对于直接子节点,“我附庸的附庸不是我的附庸”,也就是text()在第一个节点中找不到任何东西。

第二个被选中的节点稍微有些不同,由于自动补齐的因素,<li>节点内隐式包含了一个换行符\ntext()觉得来都来了,就把\n带走了。

想要解决这个bug也很简单,一种是再选中<a>,一种是直接使用//,让我们来体会一下两者的区别:

  • 再选中<a>
    from lxml import etree
    html = etree.parse('./text.html', etree.HTMLParser())
    result = html.xpath('//li[@class="item-0"]/a/text()')
    print(result)
    
    # 输出为
    '''
    ['first item', 'fifth item']
    '''
    
  • 使用//
    from lxml import etree
    html = etree.parse('./text.html', etree.HTMLParser())
    result = html.xpath('//li[@class="item-0"]//text()')
    print(result)
    
    # 输出为
    '''
    ['first item', 'fifth item', '\n    ']
    '''
    

前者精准获取了节点中的内容,后者则把所有在具有指定属性的<li>节点下的内容全部打出来了。

必须要再次强调的是:节点本身与节点的属性都不算text()的内容

属性获取

text()可以获取节点内的内容,那节点的属性该如何获取呢?

这个在前文已有涉及,点击跳转

这里我们再做实例:

from lxml import etree

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

# 输出为
'''
['link1.html', 'link2.html', 'link3.html', 'link4.html', 'link5.html']
'''

这里我们定位到需要查询的<a>节点,后加/@href查询指定属性。

查询某节点的属性的写法和先前的属性匹配十分类似,需要注意区分。

属性的多值匹配

有时候节点属性内的值可能不止一个,比如:

<li class="li li-first"><a href="link.html">first item</a></li>

此时我们单用@class="li"来匹配试一下:

from lxml import etree

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

# 输出为
'''
[]
'''

空无一物!发生甚么事了!

原因是现在的<li>节点内的class属性有两个值:lili-first,采用原先的匹配方法是无效的。

幸运的是,XPath提供了contains()函数,实例如下:

result = html.xpath('//li[contains(@class, "li")]/a/text()')

此时我们再运行,就获得了输出:

# 输出为
'''
['first item']
'''

多属性的匹配

很自然,一个节点也可以拥有很多属性,很多时候我们需要通过多个属性共同确定一个特定的节点,此时我们可以将各个属性之间用运算符and来连接,举一实例如下:

from lxml import etree

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

# 输出为
'''
['first item']
'''

这里的and是XPath众多运算符的一个,在XPath中还提供了很多运算符,列表如下:

运算符 描述 实例 返回值
or age=19 or age=20 如果 age 是 19,则返回 true。如果 age 是 21,则返回 false
and age>19 and age<21 如果 age 是 20,则返回 true。如果 age 是 18,则返回 false
mod 计算除法的余数 5 mod 2 1
| 计算两个节点集 //book | //cd 返回所有拥有 bookcd 元素的节点集。
+ 加法 6 + 4 10
- 减法 6 - 4 2
* 乘法 6 * 4 24
div 除法 8 div 4 2
= 等于 age=19 如果 age 是 19,则返回 true。如果 age 是 20,则返回 false
!= 不等于 age!=19 如果 age 是 18,则返回 true。如果 age 是 19,则返回 false
< 小于 age<19 如果 age 是 18,则返回 true。如果 age 是 19,则返回 false
<= 小于或等于 age<=19 如果 age 是 19,则返回 true。如果 age 是 20,则返回 false
> 大于 age>19 如果 age 是 20,则返回 true。如果 age 是 19,则返回 false
>= 大于或等于 age>=19 如果 age 是 19,则返回 true。如果 age 是 18,则返回 false

按需选择

有时候我们即使选择了合适的属性,也会遇到匹配到多个节点的情况,如果此时我们只想选择其中几个该如何呢?

无所谓,XPath会出手。

XPath支持我们通过在[]中传入索引的方法获取特定次序的节点,举一实例如下:

from lxml import etree

html = etree.parse('./text.html', etree.HTMLParser())
print(html.xpath('//li[2]/a/text()'))
print(html.xpath('//li[last()]/a/text()'))
print(html.xpath('//li[position()<3]/a/text()'))
print(html.xpath('//li[last()-2]/a/text()'))

# 输出为
'''
['second item']
['fifth item']
['first item', 'second item']
['third item']
'''

本实例中我们运用到了很多函数,比如position()last(),XPath还提供了多其他函数。想要了解更多请参阅此处

节点轴的选择

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

有HTML文件,内容如下:

<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>

有代码如下:

from lxml import etree

html = etree.parse('./text.html', etree.HTMLParser())
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 0x7efd84f47ac0>, <Element body at 0x7efd847ccf00>, <Element div at 0x7efd847ccf40>, <Element ul at 0x7efd847ccf80>]
[<Element div at 0x7efd847ccf40>]
['item-0']
[<Element a at 0x7efd847ccf00>]
[<Element span at 0x7efd847cce00>]
[<Element a at 0x7efd847ccd80>]
[<Element li at 0x7efd847cce40>, <Element li at 0x7efd847ccec0>, <Element li at 0x7efd847ccfc0>, <Element li at 0x7efd847cd000>]
'''

想获得更多有关轴的信息?请参阅此处

结语

本节对XPath和lxml的学习就到这里了。