XPath的使用

1.使用实例

通过Python的lxml库,利用XPath进行HTML的解析。

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)
#调用HTML类进行初始化,构造一个XPath解析对象

result = etree.tostring(html)
print(result.decode('utf-8'))
#调用tostring()方法即可输出修正后的HTML代码,但是结果是bytes类型
#用decode()方法将其转换为str类型

或者读取文本文件进行解析:

from lxml import etree

html = etree.parse('./test.html',etree.HTMLParser())
#读取文件解析,test.html与前面的HTML代码相同

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>

结果补全了li节点标签,自动添加了body、html节点。

(直接读取文件输出多了一个DOCTYPE声明,对解析结果无影响)

 

2.所有节点

我们一般会用 // 开头的XPath规则来选取所有符合要求的结果。

使用 * 代表匹配所有节点。

以前面的test.html文件来演示。

代码:

from lxml import etree

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

运行结果:输出

[<Element html at 0x20d4c1280c8>, <Element body at 0x20d4c290308>, <Element p at 0x20d4c290348>, <Element ul at 0x20d4c290388>, <Element li at 0x20d4c290408>, <Element a at 0x20d4c290508>, <Element li at 0x20d4c290648>, <Element a at 0x20d4c2906c8>, <Element li at 0x20d4c290708>, <Element a at 0x20d4c290608>, <Element li at 0x20d4c290748>, <Element a at 0x20d4c290788>, <Element li at 0x20d4c2907c8>, <Element a at 0x20d4c290808>]

如果要选取所有的li节点,只需要使用//加上li节点名称即可。

代码:

from lxml import etree

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

运行结果:输出

[<Element li at 0x1ff6652a3c8>, <Element li at 0x1ff6652a408>, <Element li at 0x1ff6652a448>, <Element li at 0x1ff6652a4c8>, <Element li at 0x1ff6652a6c8>]

提取出来的结果都是列表形式,每个元素都是Element对象。

 

3.子节点

通过 // 或 / 即可查找元素的子节点或子孙节点。

如果想选择li节点的所有直接a子节点。/a用于选择li节点的所有直接子节点a。

from lxml import etree

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

运行结果:输出

[<Element a at 0x1f494b5a3c8>, <Element a at 0x1f494b5a408>, <Element a at 0x1f494b5a448>, <Element a at 0x1f494b5a4c8>, <Element a at 0x1f494b5a6c8>]

 

如果要获取所有的子孙节点,就可以使用 // 。

代码:

from lxml import etree

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

运行结果:输出

[<Element a at 0x2081ef6a408>, <Element a at 0x2081ef6a448>, <Element a at 0x2081ef6a488>, <Element a at 0x2081ef6a508>, <Element a at 0x2081ef6a708>]

 

但是如果使用//ul/a就不能获得任何结果,因为ul节点下没有直接的a子节点,只有li节点。

代码:

from lxml import etree

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

运行结果:输出

[]

/ :用于获取直接子节点

// :用于获取子孙节点

 

4.父节点

如果知道了子节点,可以通过 .. 来查找父节点。

代码:

from lxml import etree

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

运行结果:输出

['item-1']

获取到了目标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)

运行结果同上。

 

5.属性匹配

可以使用@符号进行属性过滤。比如要选择class为item-0的li节点。

代码:

from lxml import etree

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

运行结果:输出

[<Element li at 0x1dd0b76a4c8>, <Element li at 0x1dd0b76a508>]

 

6.文本获取

使用XPatn中的text()方法获取文本中的节点。

尝试获取li节点中的文本。

代码:

from lxml import etree

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

print(result)

 

运行结果:输出

['\r\n     ']

并没有获取到任何文本,只获取到了换行符,这是为什么呢?因为 XPath 中 text 方法前面是 /,而此处 / 的含义是选取直接子节点,

很明显 li 的直接子节点都是 a 节点,文本都是在 a 节点内部的,所以这里匹配到的结果就是被修正的 li 节点内部的换行符,因为自动修正的 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']

 

另一种是使用 //。

代码:

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内部的文本,即换行符。

 

7.属性获取

@可以获取节点属性。

如果我们想获取所有li节点下所有的a节点的href属性。

代码:

from lxml import etree

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

运行结果:输出

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

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

 

8.属性多值匹配

某些节点的属性有多个值。

代码:

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()函数。

代码:

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()')
#contains()方法,第一个参数传入属性名称,第二个参数传入属性值

print(result)

运行结果:输出

['first item']

 

9.多属性匹配

根据多个属性确定一个节点,就要同时匹配多个属性。

代码:

from lxml import etree

text = '''
<li class="li li-first" name="item"><a href="link.html">first item</a></li>
'''
#这里li节点又增加了一个name属性,需要同时根据class和name来选择

html = etree.HTML(text)
result = html.xpath('//li[contains(@class, "li") and @name="item"]/a/text()')
#使用and操作符相连接

print(result)

运行结果:输出

['first item']

and是XPath中的运算符,此外还有很多。

 

10.按序选择

有时匹配了多个节点,却只想要其中几个节点。

可以利用中括号传入索引的方法获取特定次序节点。

代码:

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()')
#选取了第一个li节点,注意是以1开头的,而不是0

print(result)
result = html.xpath('//li[last()]/a/text()')
#选取最后一个li节点,中括号中传入last()即可

print(result)
result = html.xpath('//li[position()<3]/a/text()')
#选取了位置小于3的节点,也就是序号为1和2的节点

print(result)
result = html.xpath('//li[last()-2]/a/text()')
#选取了倒数第三个li节点,传入last()-2即可,因为last()是最后一个,last()-2就是倒数第三个

print(result)

运行结果:输出

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

使用了last()、position()等函数,在XPath中还有许多其他种类的函数。

 

11.节点轴选择

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::*')
#调用ancestor轴,可以获取所有的先祖节点。
#其后要跟两个冒号,其次是节点的选择器,使用 * 表示匹配所有节点。

print(result)
result = html.xpath('//li[1]/ancestor::div')
#*改为div,只得到div这一个先祖节点。

print(result)
result = html.xpath('//li[1]/attribute::*')
#调用attribute轴,可以获取所有属性值。
#使用*表示获取节点的所有属性。

print(result)
result = html.xpath('//li[1]/child::a[@href="link1.html"]')
#调用child轴,可以获取所有直接子节点。
#其后加了限定条件,选择href属性为link1.html的a节点。

print(result)
result = html.xpath('//li[1]/descendant::span')
#调用descendant轴,可以获取所有子孙节点。
#其后加了限定条件,获取span节点,所以返回的结果只包含span节点而不包含a节点。

print(result)
result = html.xpath('//li[1]/following::*[2]')
#调用following轴,可以获取当前节点之后的所有节点。
#虽然使用的是 * 匹配,但又加了索引选择,所以只获取了第二个后续节点。

print(result)
result = html.xpath('//li[1]/following-sibling::*')
#调用了following-sibling轴,可以获取当前节点之后的所有同级节点。
#使用 * 匹配,所以获取了所有后续同级节点。

print(result)

运行结果:输出

[<Element html at 0x2879a32aa48>, <Element body at 0x2879a469788>, <Element div at 0x2879a469848>, <Element ul at 0x2879a469888>]
[<Element div at 0x2879a469848>]
['item-0']
[<Element a at 0x2879a469788>]
[<Element span at 0x2879a469848>]
[<Element a at 0x2879a469888>]
[<Element li at 0x2879a469948>, <Element li at 0x2879a469b48>, <Element li at 0x2879a4697c8>, <Element li at 0x2879a4698c8>]

 

参考用书《python3网络爬虫开发实战》

posted @ 2020-12-20 15:33  Hao_ran  阅读(415)  评论(0)    收藏  举报