XPath解析库的使用

  1. XPath概览

  2. XPath的常用规则

  3. 准备工作

  4. 实例引入

  5. 所有节点

  6. 子节点

  7. 父节点

  8. 属性匹配

  9. 文本获取

  10. 属性匹配

  11. 属性多值匹配

  12. 多属性匹配

  13. 按序选择

  14. 节点轴选择

这里是我上手的用python爬取的知乎热榜的文章,随缘更新完善,欢迎知道批评:爬取知乎热榜问题

XPath概览

XPath选择功能强大,提供了非常简洁明了的路径选择表达式,另外还提供了超过一百个的内建函数,几乎可以定位所有想要的节点。

常用规则

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

示例如下:

//title[@lang='eng']

这就是一个XPath规则,代表选择所有名称为title,同时属性lang的值为eng 的节点。
后面会通过python的lxml库,利用XPath进行HTML解析。

准备工作

进行lxml库的安装

实例引入

通过实例引入来感受XPath来对网页进行解析的过程。

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="link1.html">third item</a></li>
        <li class = "item-1" ><a href="link4.html">forth item</a></li>
        <li class = "item-0" ><a href="link5.html">fifth item</a></li>
    </ul>
</div>
'''
html = etree.HTML(text)
result = etree.tostring(html)

这里先导入了lxml库的etree模块,然后声明了一段HTML文档,调用HTML类进行初始化,这样就成功构造了一个XPath解析对象。
调用tostring()方法即可输出修正后的HTML代码,但是修正过后的结果是bytes类型的,这里就需要利用decode()方法来将其转换为str类型。

html = etree.HTML(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="link1.html">third item</a></li>
        <li class="item-1"><a href="link4.html">forth item</a></li>
        <li class="item-0"><a href="link5.html">fifth item</a></li>
    </ul>
</div>
</body></html>

可以在写text的时候,少写一点后节点,上述可以不全body,html 等节点。

这里建议将text的内容另外保存在一个文件之中,方便保存写过的代码,当然练手就不需要考虑了,这里我是将text的内容另存为了一个text.html文档里面,后面的程序都是直接读取这个文档。

当然,也可以直接读取文本文件进行解析:

from lxml import etree
html = etree.parse('./tesst.html',etree.HTMLParse())
result = etree.tostring(html)
print(result.decode('utf-8'))

最后输出的结果会略有不同,会有了一个DOCTYPE的申明提醒,但是对解析结果无任何影响。

所有节点

一般会选取 // 开头的XPath规则来选取选优符合要求的节点,以之前的HTML文本为例,要选取所有的节点,可以这样实现:

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

输出的结果:

[<Element html at 0x1f7d8c3d108>, 
<Element body at 0x1f7d8c1bfc8>, 
<Element p at 0x1f7d8c3d148>, 
<Element div at 0x1f7d8c3d188>,
<Element ul at 0x1f7d8c3d1c8>, 
<Element li at 0x1f7d8c3d248>, 
<Element a at 0x1f7d8c3d288>, 
<Element li at 0x1f7d8c3d2c8>, 
<Element a at 0x1f7d8c3d308>, 
<Element li at 0x1f7d8c3d208>, 
<Element a at 0x1f7d8c3d348>, 
<Element li at 0x1f7d8c3d388>, 
<Element a at 0x1f7d8c3d3c8>, 
<Element li at 0x1f7d8c3d408>, 
<Element a at 0x1f7d8c3d448>]

为了方便阅读,我手动对输出结果的格式进行了调整。
这里使用*代表匹配所有节点,也就是整个HTML文档的所有节点都会被获取,返回的形式是一个列表,每个元素都是Element类型,其后跟了节点名称,如html,body,div,ul,li等,所有节点包含在其中。
当然也可以指定节点名称。例如想要获取所有的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 0x2525775d108>, <Element li at 0x2525775d148>, <Element li at 0x2525775d188>, <Element li at 0x2525775d1c8>, <Element li at 0x2525775d208>]
<Element li at 0x2525775d108>

这里可以看到的结果是一个列表形式,每个元素都是Element对象,需要提取第几个对象,可以直接用中括号加索引,(python计数从0开始)

子节点

通过///即可查找元素的子节点或者是子孙节点,加入现在想选择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节点的所有直接子节点a,即可得到所有li节点的所有直接a子节点。
运行结果如下:

[<Element a at 0x21ceea0d108>, <Element a at 0x21ceea0d148>, <Element a at 0x21ceea0d188>, <Element a at 0x21ceea0d1c8>, <Element a at 0x21ceea0d208>]

此处使用的/用户选取直接子节点,如果想要获取所有子孙节点,可以使用//。例如想要获取ul节下的所有子孙a节点,如下:

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

结果如下:

[<Element a at 0x21713c0d108>, <Element a at 0x21713c0d148>, <Element a at 0x21713c0d188>, <Element a at 0x21713c0d1c8>, <Element a at 0x21713c0d208>]

运行结果相同,但是这里如果使用//ul/a,就无法获取结果了,因为ul的直接子节点没有a节点。输出结果就是一个空列表。

[]

父节点

可以用..来查找父节点。比如,现在选中href属性为link4.htmla节点,然后再获取其父节点,然后再获取其class属性,相关代码如下:

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)

结果是相同的。注意,在使用第二种方法获取父节点的时候,后面有个*,没有*会报错:

Traceback (most recent call last):
  File "D:/python/spider/toutiao.py", line 16, in <module>
    result = html.xpath('//a[@href="link4.html"]/parent::/@class')
  File "src\lxml\etree.pyx", line 2295, in lxml.etree._ElementTree.xpath
  File "src\lxml\xpath.pxi", line 357, in lxml.etree.XPathDocumentEvaluator.__call__
  File "src\lxml\xpath.pxi", line 225, in lxml.etree._XPathEvaluatorBase._handle_result
lxml.etree.XPathEvalError: Invalid expression

属性匹配

在选取的时候,我们还可以用@符号来进行属性过滤。比如这里如果要选取class为uitem-0li节点,如下实现:

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

结果如下:

[<Element li at 0x233ca86d108>, <Element li at 0x233ca86d148>]

文本获取

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

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

这里可以手动去前文验证一下是否是符合预期的输出。这里是先去了li节点,然后又取到了子节点a,再取其文本,符合预期。
再看用另一种方式(即使用//)选取的结果。

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

输出结果相同。
注意:
如果说,你的源html的节点是不全的,例如缺少后节点</li>,</a>之类的,这里的输出结果会多一个:

['\n']

因为Xpathtext()前面是/,而此处的/含义是选取直接子节点,很明显li的直接子节点都是a节点,文本都在a节点内部,所以这里会得到的是换行符,因为自动修正的li节点的尾标签换行了。

**总结:**如果想要获取子孙节点内部的所有文本,可以指及用//text()的方式,这样可以保证获取最全面的文本信息,但可能会夹杂一些换行符等特殊字符。如果想获取某些特定子孙节点下的所有文本,可以县选区到特定的子孙节点,然后再调用text()来获取期内不得文本,保证获取的结果的整洁。

属性获取

text()可以获取节点内部的文本,用@符号就可以获取节点属性。例如:获取所有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', 'link1.html', 'link4.html', 'link5.html']

这里通过@href来获取节点的href属性,**注意:**这里的属性获取和属性匹配不同

  1. 属性匹配时中括号加属性名和值来限定某个属性:@href="link1.html"
  2. 属性获取时获取节点的某个属性@href

属性多值匹配

有时候有些属性可能会有多个值,例如:

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

这里的html的li节点下的class有两个属性,但是还想要之前的方法进行匹配,就匹配不到结果。结果如下:

[]

强行进行多属性匹配也是行不通的:

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

结果也是空列表。
这里就需要用到contains()函数了,代码可以改写如下:

from lxml import etree
html = etree.HTML(text)
result = html.xpath('//li[contains(@class, "li")]/a/text()')
print(result)

输出结果:

['first item']

这样通过contains()方法, 第一个参数传入属性名称,第二个参数传入属性值,只要此属性包含所传入的属性值,就可以完成匹配。

多属性匹配

还有一种情况会经常碰到,就是多属性确定一个节点,这是就需要同时匹配多个属性,此时可以使用运算符and来连接。示例如下:

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

这里的li节点又增加了一个属性name,要确定这个节点,需要同时根据class和name属性来进行选择:一个条件是class属性里面包含li字符串,另一个是name属性为item字符串,两者需要同时满足,用and连接符连接。
返回的结果:

['first item']

这里的and其实是XPath中的运算符,还有其他的运算符:and or mod等。

按序选择

有时候匹配到多个节点,如果是只想要某个节点,如第二个或者最后一个,我们就可以利用中括号传入索引的方法获取特定次序的节点,以一开始的text.html文件的html,实例如下:

from lxml import etree
html = etree.parse('./test.html',etree.HTMLParser())
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)

输出结果为:

['first item']
['fifth item']
['first item', 'second item']
['third item']
  1. 选取了第一个li节点,中括号传入数字**(注意:这里的数字与代码中不同,计数从1开始,不是0)**,所以输出的是第一个li节点的a节点的文本。
  2. 选取了最后一个节点,再中括号内传入last()即可。
  3. 选择了位置小于3的li节点,也就是位置为1和2的节点,得到的结果就是前两个节点。
  4. 选择了倒数第三个li节点,中括号内传入last()-2即可。
    这节使用了last()position()了两个函数,XPath包含一百多个内建函数,详情请参考XPath内建函数

节点轴的选择

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

text = '''
<li class = "li=first" name="item" ><a href="link1.html">first item</a></li>
'''
from lxml import etree
html = etree.parse('./test.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 0x1b75a45d048>, <Element body at 0x1b75a45d108>, <Element div at 0x1b75a45d148>, <Element ul at 0x1b75a45d188>]
[<Element div at 0x1b75a45d148>]
['item-0']
[<Element a at 0x1b75a45d188>]
[]
[<Element a at 0x1b75a45d108>]
[<Element li at 0x1b75a45d148>, <Element li at 0x1b75a45d1c8>, <Element li at 0x1b75a45d208>, <Element li at 0x1b75a45d248>]
  1. 调用ancestor轴,可以获取所有祖先。其后跟两个冒号,然后是节点选择器,这里使用*表示匹配所有节点,因此返回的结果是第一个li节点的所有祖先节点,包括html,body,div,ui;
  2. 选择第一个li节点的祖先节点中的div祖先;
  3. 调用了attribute轴,可以获取所有属性值,后面的选择器依然是*,代表获取这个节点的所有属性,返回值就是li节点的所有属性值。
  4. 调用了child轴,可以获取所有直接子节点,加上限定条件,选择href属性为link1.html的a节点。
  5. 选择了descendant轴,可以所有子孙节点,加限定条件获取span节点,返回的结果只包含span节点而不包含a节点。
  6. 调用了following轴,可以获取当前节点之后的所有节点,限定条件是*,但又加了索引,u偶一只能获取到第二个后续节点。
  7. 调用了following-sibling轴,可以获取当前节点之后的所有同级节点,用的是*进行匹配,所以获取了所有后续同级节点。

以上是XPath轴的简单用法,更多轴用法可以参考:XPath轴用法

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值