-
XPath概览
-
XPath的常用规则
-
准备工作
-
实例引入
-
所有节点
-
子节点
-
父节点
-
属性匹配
-
文本获取
-
属性匹配
-
属性多值匹配
-
多属性匹配
-
按序选择
-
节点轴选择
这里是我上手的用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.html
的a
节点,然后再获取其父节点,然后再获取其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-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 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']
因为Xpath
中text()
前面是/
,而此处的/
含义是选取直接子节点,很明显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
属性,**注意:**这里的属性获取和属性匹配不同
- 属性匹配时中括号加属性名和值来限定某个属性:
@href="link1.html"
- 属性获取时获取节点的某个属性
@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']
- 选取了第一个
li
节点,中括号传入数字**(注意:这里的数字与代码中不同,计数从1开始,不是0)**,所以输出的是第一个li
节点的a
节点的文本。 - 选取了最后一个节点,再中括号内传入
last()
即可。 - 选择了位置小于3的
li
节点,也就是位置为1和2的节点,得到的结果就是前两个节点。 - 选择了倒数第三个
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>]
- 调用
ancestor
轴,可以获取所有祖先。其后跟两个冒号,然后是节点选择器,这里使用*
表示匹配所有节点,因此返回的结果是第一个li节点的所有祖先节点,包括html,body,div,ui; - 选择第一个
li
节点的祖先节点中的div祖先; - 调用了
attribute
轴,可以获取所有属性值,后面的选择器依然是*
,代表获取这个节点的所有属性,返回值就是li
节点的所有属性值。 - 调用了
child
轴,可以获取所有直接子节点,加上限定条件,选择href
属性为link1.html
的a节点。 - 选择了
descendant
轴,可以所有子孙节点,加限定条件获取span
节点,返回的结果只包含span节点而不包含a
节点。 - 调用了
following
轴,可以获取当前节点之后的所有节点,限定条件是*
,但又加了索引,u偶一只能获取到第二个后续节点。 - 调用了
following-sibling
轴,可以获取当前节点之后的所有同级节点,用的是*
进行匹配,所以获取了所有后续同级节点。
以上是XPath
轴的简单用法,更多轴用法可以参考:XPath轴用法