使用XPath | lxml库
XPath简单介绍
XPath,全程为XML Path Language,即XML路径语言,最初用于搜寻XML文档内容,也同样适用于HTML文档。
XPath的强大之处在于简洁明了的路径选择表达式,丰富的可用于处理字符串、数值和节点等信息的内置函数,想要了解更多相关信息,请参阅此处
让我们来稍微了解一下XPath的几个常用规则:
表达式 | 描述 |
---|---|
nodename | 选取此节点的所有子节点 |
/ | 从当前节点选取直接子节点 |
// | 从当前节点选取子孙节点 |
. | 选取当前节点 |
.. | 选取当前节点的父节点 |
@ | 选取属性 |
Python中主要通过lxml库来使用XPath,我们来看一个小示范:
这就是一个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
类型,后跟有节点名称,例如html
、body
等等。
当然你也可以指定节点的类型:
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>
节点内隐式包含了一个换行符\n
,text()
觉得来都来了,就把\n
带走了。
想要解决这个bug也很简单,一种是再选中<a>
,一种是直接使用//
,让我们来体会一下两者的区别:
- 再选中
<a>
- 使用
//
前者精准获取了节点中的内容,后者则把所有在具有指定属性的<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
查询指定属性。
查询某节点的属性的写法和先前的属性匹配十分类似,需要注意区分。
属性的多值匹配
有时候节点属性内的值可能不止一个,比如:
此时我们单用@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
属性有两个值:li
和li-first
,采用原先的匹配方法是无效的。
幸运的是,XPath提供了contains()
函数,实例如下:
此时我们再运行,就获得了输出:
多属性的匹配
很自然,一个节点也可以拥有很多属性,很多时候我们需要通过多个属性共同确定一个特定的节点,此时我们可以将各个属性之间用运算符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 |
返回所有拥有 book 和 cd 元素的节点集。 |
+ |
加法 | 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的学习就到这里了。