Python爬虫之xpath解析库

一、什么是XPath?

xpath(XML Path Language)是一门在XML和HTML文档中查找信息的语言,可用来在XML和HTML文档中对元素和属性进行遍历与匹配。通俗一点说,通过XPath你可以从HTML或者XML结构的数据中筛选出来你想要的信息,比如<h2>标签中的文本内容、<a>标签中的href对应的链接等等。类似于正则表达式的功能。

二、XPath依赖包安装

XPath的安装可以通过不同的方式进行,主要包括在浏览器中安装XPath插件和在编程环境中安装相关的库。‌

1. 在浏览器中安装XPath插件

‌进入Chrome浏览器的应用商店‌,搜索“XPath Helper”并安装该插件。安装成功后,可以通过快捷键Ctrl+Shift+X来调用该插件。‌

2. 在编程环境中安装XPath相关库

‌在Python环境中‌,XPath通常被封装在lxml库中。可以通过pip安装lxml库来使用XPath功能。安装命令为:

pip3 install lxml

三、XPath节点

在 XPath 中,有七种类型的节点:元素属性文本命名空间处理指令注释以及文档(根)节点。XML 文档是被作为节点树来对待的。树的根被称为文档节点或者根节点。

🔊:其中元素属性文本注释以及文档(根)节点在HTML文档中,我们经常看到。

  • 元素节点:表示XML或HTML文档中的元素。

    例如,在HTML文档中,<body>、<div>、<p>等都是元素节点。在XPath中,可以使用元素名称来选择元素节点,例如://div 表示选择所有的<div>元素。

 

  • 属性节点:表示XML或HTML文档中元素的属性。

    例如,在HTML文档中,元素的class、id、src等属性都是属性节点。在XPath中,可以使用@符号来选择属性节点,例如://img/@src 表示选择所有<img>元素的src属性。

 

  • 文本节点:表示XML或HTML文档中的文本内容。

    例如,在HTML文档中,<p>标签中的文本内容就是文本节点。在XPath中,可以使用text()函数来选择文本节点,例如://p/text() 表示选择所有<p>元素中的文本内容。
  • 注释节点:表示XML或HTML文档中的注释。

    注释是一种用来添加说明和备注的机制。在XPath中,可以使用comment()函数来选择注释节点,例如://comment() 表示选择所有的注释节点。
  • 文档节点:表示整个XML或HTML文档。

    文档节点也被称为根节点。在XPath中,可以使用/符号来选择文档节点,例如:/ 表示选择整个文档节点。
🔊:命名空间处理指令,在XML常见

  • 命名空间节点:表示XML文档中的命名空间。命名空间是一种避免元素命名冲突的方法。

    在XPath中,可以使用namespace轴来选择命名空间节点,例如://namespace::* 表示选择所有的命名空间节点。
  • 处理指令节点:表示XML文档中的处理指令。处理指令是一种用来给处理器传递指令的机制。

    在XPath中,可以使用processing-instruction()函数来选择处理指令节点,例如://processing-instruction('xml-stylesheet') 表示选择所有的xml-stylesheet处理指令节点。

四、XPath语法

XPath 使用路径表达式来选取 XML 文档中的节点或节点集。节点是通过沿着路径 (path) 或者步 (steps) 来选取的。

1. 常用规则

表达式描述示例结果
/ 如果是在最前面,代表从根节点选取。 /li 选取根元素下所有的ul子节点,⚠️注意:只到子节点,到不了子节点以下的节点
如果在某个节点的后面,代表从这个节点下选取。 //ul/li 选取所有ul元素下所有的li子节点,⚠️注意:只到子节点,到不了子节点以下的节点
// 从当前节点中选择文档中的直接和间接子节点——这使我们能够“跳过关卡” or 从全局节点中选择节点,随便在哪个位置(取子孙节点) //book 从全局节点中找到所有的book节点或选取所有 book 子元素,而不管它们在文档中的位置。
. 当前节点 ./a 选取当前节点下的a标签
.. 选取当前节点的父节点。 //div[@class="book-list list"]//div[@class="title"]/../a

选择拥有class属性的

所有的a节点

@ 选取某个节点的属性 //a[@class] 选取div[@class="title"]结点的父节点的a标签

2. 选取所有节点

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

from lxml import etree
#🌾:HTML文本
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>
'''
#🌾:通过 etree 将 html结果的文本字符串 转换为 待处理的 html文本对象
html = etree.HTML(text)
#🌾:通过 xpath 选取所需的内容
result = html.xpath('//*')
#🌾:输出结果
print(result)

'''
[
    <Element html at 0x104274d00>, 
    <Element body at 0x104631940>, 
    <Element div at 0x1046319c0>,
    <Element ul at 0x104631a00>, 
    <Element li at 0x104631a80>, 
    <Element a at 0x104631ac0>, 
    <Element li at 0x104631b00>, 
    <Element a at 0x104631b40>,
    <Element li at 0x104631a40>, 
    <Element a at 0x104631b80>, 
    <Element li at 0x104631bc0>, 
    <Element a at 0x104631c00>, 
    <Element li at 0x104631c40>, 
    <Element a at 0x104631c80>
]
'''
🤔:为什么输出内容里面有<html>,<body> 节点对象?
这时因为通过etree处理后的HTML文本会被自动修正,没有html, body标签的会给补上,缺失自闭合标签的也会给你添加上。

这里使用 * 代表匹配所有节点,也就是获取整个HTML文本中的所有节点。从运行结果可以看到,返回形式是一个列表,其中每个元素是Element类型,类型后面跟着节点的名称,html,body,div,ul,li,a等,所有节点都包含在了列表中。

当然,此处匹配也可以指定节点名称。例如想获取 所有li节点,实例如下:

#🌾:通过 xpath 选取所需的内容,通过//获取所有的li节点
result = html.xpath('//li')

3. 选取子节点

通过 '/' 和 '//' 即可查找元素的 子节点 或者 子孙节点。 假如现在想选择 ol节点子li 节点的所有直接子节点a, 

则可以这样实现

#🌾:通过 xpath 选取所需的内容,先通过//选取所有的li节点,然后再去选取li节点下存在的 "直接子节点"a
result = html.xpath('//li/a')

这里通过追加 /a 的方式,选择了所有 li 节点的所有直接子节点 a。 其中 //li 用于选中所有 li 节点, /a 用于选中 li 节点的所有直接子节点 a

'''
[
    <Element a at 0x100979880>, 
    <Element a at 0x1009798c0>, 
    <Element a at 0x100979900>, 
    <Element a at 0x100979940>, 
    <Element a at 0x100979980>
]
'''

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

#🌾:通过 xpath 选取所需的内容。通过//选取所有的ul标签,通过//a 选取所有ul下面所有的 a标签
result = html.xpath('//ul//a')

此运行结果 与 '//li/a'相同。

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

#🌾:通过 xpath 选取所需的内容。通过//选取所有的ul标签,通过/a 选取所有ul下面直接子节点a标签。明显ul下面的直接子节点没有a,只有li
result = html.xpath('//ul/a')
🔊: / 和 // 区别
/:用于获取直接子节点。
//:用于获取子孙节点。

4. 选取父节点

通过连续的 / 或 // 可以查找子节点 或者 子孙节点,那么假如知道了子节点,怎么查找父节点呢?可以通过 .. 实现。

例如,首先选中href属性为 link4.html 的 a 节点。 然后获取其父节点,再获取父节点的class属性,相关代码如下:

from lxml import etree
#🌾:HTML文本
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>
'''
#🌾:通过 etree 将 html结果的文本字符串 转换为 待处理的 html文本对象
html = etree.HTML(text)
#🌾:通过 xpath 选取所需的内容。
# 1. 通过 // 选取所有 a 标签;
# 2. 通过 a[@href="link4.html"] 选取出a属性 href = 'link4.html' 的a标签
# 3. 通过 .. 选取出当前a标签的父标签
# 4. 通过 /@class 选取出父节点的class属性
result = html.xpath('//a[@href="link4.html"]/../@class')
#🌾:输出结果
print(result)  #['item-1']
!!!注意:这里读到‘/@class’可能有点疑惑,按照先前的语法规则,‘../’ 这里可能潜意识里,认为/@class应该是父节点的子节点中class属性,也就是标签a的class属性。这种想法是错了,如果有这种认为,估计是忘记了 “属性节点” 和 “内容节点” 都是父节点的子节点。“属性节点” 也是节点的类型之一。

检查一下结果发现,这正是我们获取的目标li节点的class属性。

也可以通过 parent:: 获取父节点, 代码如下:

result = html.xpath('//a[@href="link4.html"]/parent::*/@class')

5. 属性匹配

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

from lxml import etree
#🌾:HTML文本
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>
'''
#🌾:通过 etree 将 html结果的文本字符串 转换为 待处理的 html文本对象
html = etree.HTML(text)
#🌾:通过 xpath 选取所需的内容。
# 1. 通过 // 选取所有 li 标签;
# 2. 通过 [@class='item-0'] 选取 class 属性值为 ‘item-0’的 li 节点
result = html.xpath('//li[@class="item-0"]')
#🌾:输出结果
print(result)  #[<Element li at 0x104821840>, <Element li at 0x104821880>]

这里通过加入[@class='item-0'],限制了节点的class属性为 item-0。HTML文本中符合这个条件的 li 节点有两个。

6. 文本获取

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

from lxml import etree
#🌾:HTML文本
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>
'''
#🌾:通过 etree 将 html结果的文本字符串 转换为 待处理的 html文本对象
html = etree.HTML(text)
#🌾:通过 xpath 选取所需的内容。
# 1. 通过 // 选取所有 li 标签;
# 2. 通过 [@class='item-0'] 选取 class 属性值为 ‘item-0’的 li 节点
# 3. 通过 /a 获取 当前 li标签中内容 a标签
# 4. 通过 /text() 获取 a 标签下的 内容子标签
result = html.xpath('//li[@class="item-0"]/a/text()') #🌾:输出结果 print(result) #['first item', 'fifth item']

7. 属性获取

我们已经可以用 text 方法获取节点内部文本,那么节点属性该怎样获取呢?其实依然可以用@符号。例如,通过如下代码获取所有 li 节点下所有 a 节点的href属性:

from lxml import etree
#🌾:HTML文本
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>
'''
#🌾:通过 etree 将 html结果的文本字符串 转换为 待处理的 html文本对象
html = etree.HTML(text)
#🌾:通过 xpath 选取所需的内容。
# 1. 通过 // 选取所有 li 标签;
# 2. 通过 / 获取li标签的直接子节点 a 标签
# 3. 通过 /@href 获取 a 标签的 属性子节点值 
result = html.xpath('//li/a/@href')
#🌾:输出结果
print(result)  # ['link1.html', 'link2.html', 'link3.html', 'link4.html', 'link5.html']

这里通过 @href 获取节点的 href 属性。

!!!注意,此处和属性匹配的方法不同,属性匹配是用 中括号[] 加属性名和值来限定某个属性, 如 [@href = 'link1.html'], 此处的 @href 是指获取节点的某个属性,二者需要做好区分。

8. 属性多值匹配

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

from lxml import etree
#🌾:HTML文本
text = ''''
<li class = 'li li-first'><a href='link.html'>first item</a></li>
'''
#🌾:通过 etree 将 html结果的文本字符串 转换为 待处理的 html文本对象
html = etree.HTML(text)
#🌾:通过 xpath 选取所需的内容。
# 1. 通过 // 选取所有 li 标签;
# 2. 通过 [@class='li'] 选取出符合条件的 li 标签
result = html.xpath('//li[@class="li"]')
#🌾:输出结果 
print(result)  # []

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

这种情况需要用到contains方法,于是代码可以改写如下:

from lxml import etree
#🌾:HTML文本
text = ''''
<li class = 'li li-first'><a href='link.html'>first item</a></li>
'''
#🌾:通过 etree 将 html结果的文本字符串 转换为 待处理的 html文本对象
html = etree.HTML(text)
#🌾:通过 xpath 选取所需的内容。
# 1. 通过 // 选取所有 li 标签;
# 2. 通过 [contains(@class,"li")] 选取出符合条件的 li 标签
# 3. 通过 /a 选取出li标签直接子标签a
# 4. 通过 /text() 选出a标签的 子内容节点
result = html.xpath('//li[contains(@class,"li")]/a/text()')
#🌾:输出结果 
print(result)  # ['first item']

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

9. 多属性匹配

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

from lxml import etree
#🌾:HTML文本
text = ''''
<li class = 'li li-first' name = 'item'><a href='link.html'>first item</a></li>
'''
#🌾:通过 etree 将 html结果的文本字符串 转换为 待处理的 html文本对象
html = etree.HTML(text)
#🌾:通过 xpath 选取所需的内容。
# 1. 通过 // 选取所有 li 标签;
# 2. 通过 [contains(@class,"li") and @name = "item"] 选取出符合条件的 li 标签
# 3. 通过 /a 选取出li标签直接子标签a
# 4. 通过 /text() 选出a标签的 子内容节点
result = html.xpath('//li[contains(@class,"li") and @name = "item"]/a/text()')
#🌾:输出结果 
print(result)  # ['first item']

这里的li节点又增加了一个属性name。因此要确定li节点,需要同时考察class 和 name 属性,一个条件是class属性里面包含li字符串,另一个条件是name属性为item字符串,这二者同时得到满足,才是li节点。class 和 name 属性需要用 and 运算符相连,相连之后至于中括号内进行条件筛选。

这里的 and 其实是XPath 中的运算符。除了它,还有很多其他运算符,如or,mod 等, 在此总结为

10. 按序选择 

🤔:在选择节点时,某些属性可能同时匹配了多个节点,但我们只想要其中的某一个,如第二个或者最后一个,这时怎么办?

可以使用往中括号中传入索引的方法获取特定次序的节点,实例如下:

from lxml import etree
#🌾:HTML文本
text = ''''
    <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>
'''
#🌾:通过 etree 将 html结果的文本字符串 转换为 待处理的 html文本对象
html = etree.HTML(text)

#🌾:通过 xpath 选取所需的内容。选取了第一个li节点,往中括号中传入数字1即可实现。
#注意:这里和写代码不同,序号以1开头,而非0
result = html.xpath('//li[1]/a/text()')
print(result) # ['first item']

#🌾:选取了最后一个li节点,在中括号中调用last方法即可实现
result = html.xpath('//li[last()]/a/text()')
print(result) # ['fifth item']

#🌾:选取了位置小于3的li节点,也就是位置序号为1 和 2 的节点,得到结果就是前两个li节点。
result = html.xpath('//li[position()<3]/a/text()')
print(result) # ['first item', 'second item']

#🌾:选取了倒数第三个li节点,在中括号中调用last方法再减去2即可实现。因为last方法代表最后一个,在此基础上减2得到的就是倒数第三个。
result = html.xpath('//li[last()-2]/a/text()')
print(result) # ['third item']

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

💡:上面的按序选择,其实是使用了谓语语法,谓语用来查找某个特定的节点或者包含某个指定的值的节点。谓语被嵌在方括号中。

在下面的表格中,列出了带有谓语的一些路径表达式,以及表达式的结果:

11. 节点轴选择

XPath 提供了很多节点轴的选择,包括获取子元素、兄弟元素、父元素、祖先元素等,

实例如下:

from lxml import etree
#🌾:HTML文本
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>
'''
#🌾:通过 etree 将 html结果的文本字符串 转换为 待处理的 html文本对象
html = etree.HTML(text)

#🌾:通过 xpath 选取所需的内容。
# 第一次选择时,调用了ancestor轴,可以获取所有祖先节点。
# 其后需要跟两个冒号,然后是节点的选择器,这里我们直接使用*,表示匹配所有节点,因此相应返回结果是第一个li节点的所有祖先节点,包括html,body,div 和 ul。
result = html.xpath('//li[1]/ancestor::*') print(result) # [<Element html at 0x100a1de40>, <Element body at 0x100dd5a80>, <Element div at 0x100dd5ac0>, <Element ul at 0x100dd5b00>] #🌾:第二次选择时,又加了限定条件,这次是在冒号后面加了div,于是得到的结果就是只有div这个祖先节点了。 result = html.xpath('//li[1]/ancestor::div') print(result) # [<Element div at 0x100dd5ac0>] #🌾:第三次选择时,调用了attribute轴,可以获取所有属性值,其后跟的选择器还是 * ,代表获取节点的所有属性,返回值就是 li 节点的所有属性值。 result = html.xpath('//li[1]/attribute::*') print(result) # ['item-0'] #🌾:第四次选择时,调用了child轴,可以获取所有直接子节点。这里我们又加了限定条件,选取href属性为link1.html 的 a节点 result = html.xpath('//li[1]/child::a[@href="link1.html"]') print(result) # [<Element a at 0x100dd5a80>] #🌾:第五次选择时,调用了descendant轴,可以获取所有子孙节点。这里我们又加了限定条件-----获取span节点,所以返回结果只包含span节点,不包含a节点。 result = html.xpath('//li[1]/descendant::span') print(result) # #🌾:第六次选择时,调用了following轴,可以获取当前节点之后的所有节点。这里我们虽然使用的 * 匹配,但又加了索引选择,所以只获取了第二个后续节点。 result = html.xpath('//li[1]/following::*[2]') print(result) # [<Element a at 0x100dd5ac0>] #🌾:第七次选择时,调用了 following-sibling轴,可以获取当前节点之后的所有同级节点。这里我们使用 * 匹配, 所以获取了所有的后续同级节点。 result = html.xpath('//li[1]/following-sibling::*') print(result) # [<Element li at 0x100dd5940>, <Element li at 0x100dd5a40>, <Element li at 0x100dd5b40>, <Element li at 0x100dd5b80>]

posted on 2024-12-18 15:34  梁飞宇  阅读(497)  评论(0)    收藏  举报