Python XPath的使用

Python XPath的使用


0x00 前言

  几个月前学习了一下XPath,好久没用发现已经忘得一干二净了,今天打算参考《Python3 网络爬虫开发实战》复习一下XPath的基本内容,做点笔记放在博客上方便以后查阅

0x01 XPath介绍

  XPath,全称XML Path Language,即XML路径语言,它是一门在XML文档中查找信息的语言。它最初是用来搜寻XML文档的,但是它同样适用于HTML文档的搜索。所以1在做爬虫时,我们完全可以使用XPath来做相应的信息抽取。

0x02准备工作

  在使用XPath之前我们得先安装lxml库

pip install lxml

0x03 实例引入

from lxml import etree

text = '''
<div class="movie-item-info">
        <p class="name"><a href="/films/1218273" title="误杀" data-act="boarditem-click" data-val="{movieId:1218273}">误杀</a></p>
        <p class="star">
                主演:肖央,谭卓,陈冲
<p class="releasetime">上映时间:2019-12-13    </div>
'''
html = etree.HTML(text)
result = etree.tostring(html)
print(result.decode('utf-8'))

  我们首先导入lxml库得etree模块,然后声明了一段HTML文本(来自猫眼电影),调用HTML类进行初始化,这样就成功构造了一个XPath解析对象。etree模块还可以自动修正HTML文本。这里我们调用tostring()方法即可输出修正后的HTML代码,但是结果是bytes类型,我们利用了decode()方法将其转化成str类型,结果如下:

<html><body><div class="movie-item-info">
        <p class="name"><a href="/films/1218273" title="&#35823;&#26432;" data-act="boarditem-click" data-val="{movieId:1218273}">&#35823;&#26432;</a></p> 
        <p class="star">
                &#20027;&#28436;&#65306;&#32918;&#22830;,&#35885;&#21331;,&#38472;&#20914;
</p><p class="releasetime">&#19978;&#26144;&#26102;&#38388;&#65306;2019-12-13    </p></div>
</body></html>

  可以看到,经过处理之后节点标签被不齐,还自动添加了body、html节点。
  另外,XPath也可以直接读取文本进行解析,示例如下:

from lxml import etree

html = etree.parse('F:\\code\\myProject\\博客\\test.html', etree.HTMLParser())
result = etree.tostring(html)
print(result.decode('utf-8'))

其中读取目录里的文件内容为:

<div class="movie-item-info">
        <p class="name"><a href="/films/1218273" title="误杀" data-act="boarditem-click" data-val="{movieId:1218273}">误杀</a></p>
        <p class="star">
                主演:肖央,谭卓,陈冲
<p class="releasetime">上映时间:2019-12-13    </div>
<div class="movie-item-info">
        <p class="name"><a href="/films/1190122" title="叶问4:完结篇" data-act="boarditem-click" data-val="{movieId:1190122}">叶问4:完结篇</a></p>
        <p class="star">
                主演:甄子丹,吴樾,吴建豪
        </p>
<p class="releasetime">上映时间:2019-12-20</p>    </div>

  这里解析中文出现了html编码,我们现在只是了解下XPath解析基础知识,不用纠结于中文被编码,我们稍后会解决这个问题。运行结果如下:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><div class="movie-item-info">&#13;
        <p class="name"><a href="/films/1218273" title="&#232;&#175;&#175;&#230;&#157;&#128;" data-act="boarditem-click" data-val="{movieId:1218273}">&#232;&#175;&#175;&#230;&#157;&#128;</a></p>&#13;
        <p class="star">&#13;
                &#228;&#184;&#187;&#230;&#188;&#148;&#239;&#188;&#154;&#232;&#130;&#150;&#229;&#164;&#174;,&#232;&#176;&#173;&#229;&#141;&#147;,&#233;&#153;&#136;&#229;&#134;&#178;&#13;
</p><p class="releasetime">&#228;&#184;&#138;&#230;&#152;&#160;&#230;&#151;&#182;&#233;&#151;&#180;&#239;&#188;&#154;2019-12-13    </p></div>&#13;
<div class="movie-item-info">&#13;
        <p class="name"><a href="/films/1190122" title="&#229;&#143;&#182;&#233;&#151;&#174;4&#239;&#188;&#154;&#229;&#174;&#140;&#231;&#187;&#147;&#231;&#175;&#135;" data-act="boarditem-click" data-val="{movieId:1190122}">&#229;&#143;&#182;&#233;&#151;&#174;4&#239;&#188;&#154;&#229;&#174;&#140;&#231;&#187;&#147;&#231;&#175;&#135;</a></p>&#13;
        <p class="star">&#13;
                &#228;&#184;&#187;&#230;&#188;&#148;&#239;&#188;&#154;&#231;&#148;&#132;&#229;&#173;&#144;&#228;&#184;&#185;,&#229;&#144;&#180;&#230;&#168;&#190;,&#229;&#144;&#180;&#229;&#187;&#186;&#232;&#177;&#170;&#13;
        </p>&#13;
<p class="releasetime">&#228;&#184;&#138;&#230;&#152;&#160;&#230;&#151;&#182;&#233;&#151;&#180;&#239;&#188;&#154;2019-12-20</p>    </div></body></html>    

0x04 获取节点

  • 获取所有节点
      我们一般会用//开头的XPath规则来选取所有符合要求的节点。可以这样实现:
from lxml import etree

text = '''
<div class="movie-item-info">
        <p class="name"><a href="/films/1218273" title="误杀" data-act="boarditem-click" data-val="{movieId:1218273}">误杀</a></p>
        <p class="star">
                主演:肖央,谭卓,陈冲
<p class="releasetime">上映时间:2019-12-13    </div>
<div class="movie-item-info">
        <p class="name"><a href="/films/1190122" title="叶问4:完结篇" data-act="boarditem-click" data-val="{movieId:1190122}">叶问4:完结篇</a></p>
        <p class="star">
                主演:甄子丹,吴樾,吴建豪
        </p>
<p class="releasetime">上映时间:2019-12-20</p>    </div>
'''
html = etree.HTML(text)
result = html.xpath('//*')  # 获取所有节点
print(result)

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

  • 获取指定节点
      如果想获取所有p节点,示例如下:
from lxml import etree

text = '''
<div class="movie-item-info">
        <p class="name"><a href="/films/1218273" title="误杀" data-act="boarditem-click" data-val="{movieId:1218273}">误杀</a></p>
        <p class="star">
                主演:肖央,谭卓,陈冲
<p class="releasetime">上映时间:2019-12-13    </div>
<div class="movie-item-info">
        <p class="name"><a href="/films/1190122" title="叶问4:完结篇" data-act="boarditem-click" data-val="{movieId:1190122}">叶问4:完结篇</a></p>
        <p class="star">
                主演:甄子丹,吴樾,吴建豪
        </p>
<p class="releasetime">上映时间:2019-12-20</p>    </div>
'''
html = etree.HTML(text)
result = html.xpath('//p')      # 获取所有p节点
print(result)
print(result[0])

  这里要选取所有p节点,可以使用//,然后直接加上节点名称即可,调用时直接使用xpath()方法即可。运行结果:

[<Element p at 0x46ac428>, <Element p at 0x46ac4e8>, <Element p at 0x46ac528>, <Element p at 0x46ac548>, <Element p at 0x46ac568>, <Element p at 0x46ac5a8>]
<Element p at 0x46ac428>

  这里可以看到提取结果是一个列表形式,其中每个元素都是一个Element对象。如果要取出其中的一个对象,可以直接用中括号加索引,如[0]

  • 获取子节点
      我们可以通过/或//即可查找出元素的子节点或子孙节点。假如现在想选择div节点的所有p子节点,可以这样实现:
from lxml import etree

text = '''
<div class="movie-item-info">
        <p class="name"><a href="/films/1218273" title="误杀" data-act="boarditem-click" data-val="{movieId:1218273}">误杀</a></p>
        <p class="star">
                主演:肖央,谭卓,陈冲
<p class="releasetime">上映时间:2019-12-13    </div>
<div class="movie-item-info">
        <p class="name"><a href="/films/1190122" title="叶问4:完结篇" data-act="boarditem-click" data-val="{movieId:1190122}">叶问4:完结篇</a></p>
        <p class="star">
                主演:甄子丹,吴樾,吴建豪
        </p>
<p class="releasetime">上映时间:2019-12-20</p>    </div>
'''
html = etree.HTML(text)
result = html.xpath('//div/p')  # 获取div的子节点p
print(result)
print(result[0])

运行结果如下:

[<Element p at 0x49ad508>, <Element p at 0x49ad5c8>, <Element p at 0x49ad608>, <Element p at 0x49ad628>, <Element p at 0x49ad648>, <Element p at 0x49ad688>]
<Element p at 0x49ad508>


  • 获取子孙节点
      上面我们使用/来获取了子节点,想要获取子孙节点我们可以用//。例如要获取div节点下的所有子孙节点,可以这样实现:
from lxml import etree

text = '''
<div class="movie-item-info">
        <p class="name"><a href="/films/1218273" title="误杀" data-act="boarditem-click" data-val="{movieId:1218273}">误杀</a></p>
        <p class="star">
                主演:肖央,谭卓,陈冲
<p class="releasetime">上映时间:2019-12-13    </div>
<div class="movie-item-info">
        <p class="name"><a href="/films/1190122" title="叶问4:完结篇" data-act="boarditem-click" data-val="{movieId:1190122}">叶问4:完结篇</a></p>
        <p class="star">
                主演:甄子丹,吴樾,吴建豪
        </p>
<p class="releasetime">上映时间:2019-12-20</p>    </div>
'''
html = etree.HTML(text)
result = html.xpath('//div//a')     # 获取div下的子孙节点,就是a
print(result)
print(result[0])

运行结果如下:

[<Element a at 0x44dc468>, <Element a at 0x44dc4a8>]
<Element a at 0x44dc468>

  我们要注意/和//的区别,其中/是用于获取直接子节点,//用于获取子孙节点。

  • 获取父节点
      我们已经知道了获取子节点和子孙节点,获取父节点可以用..来实现。比如我们想获取href属性为/films/1218273的a节点的父节点的class属性,也就是p的class属性,可以这样来实现:
from lxml import etree

text = '''
<div class="movie-item-info">
        <p class="name"><a href="/films/1218273" title="误杀" data-act="boarditem-click" data-val="{movieId:1218273}">误杀</a></p>
        <p class="star">
                主演:肖央,谭卓,陈冲
<p class="releasetime">上映时间:2019-12-13    </div>
<div class="movie-item-info">
        <p class="name"><a href="/films/1190122" title="叶问4:完结篇" data-act="boarditem-click" data-val="{movieId:1190122}">叶问4:完结篇</a></p>
        <p class="star">
                主演:甄子丹,吴樾,吴建豪
        </p>
<p class="releasetime">上映时间:2019-12-20</p>    </div>
'''
html = etree.HTML(text)
result = html.xpath('//a[@href="/films/1218273"]/../@class')
print(result)

运行结果如下:

['name']

0x05 属性匹配

  上面我们获取父节点的属性时已经用到了属性的知识,在xpath中我们可以用@符号来进行属性过滤。比如这里要选取class为star的p节点,可以这样实现:

from lxml import etree

text = '''
<div class="movie-item-info">
        <p class="name"><a href="/films/1218273" title="误杀" data-act="boarditem-click" data-val="{movieId:1218273}">误杀</a></p>
        <p class="star">
                主演:肖央,谭卓,陈冲
<p class="releasetime">上映时间:2019-12-13    </div>
<div class="movie-item-info">
        <p class="name"><a href="/films/1190122" title="叶问4:完结篇" data-act="boarditem-click" data-val="{movieId:1190122}">叶问4:完结篇</a></p>
        <p class="star">
                主演:甄子丹,吴樾,吴建豪
        </p>
<p class="releasetime">上映时间:2019-12-20</p>    </div>
'''
html = etree.HTML(text)
result = html.xpath('//p[@class="star"]')   # 属性匹配
print(result)

  这里我们通过加入[@class="star"],限制了节点的class属性为star,而在案例的HTML文本中符合条件的p节点有两个,所以结果应该返回两个匹配到的元素。结果如下:

[<Element p at 0x4c0c3a8>, <Element p at 0x4c0c468>]


0x06文本获取

  我们可以利用XPath中的test()方法来获取文本内容,接下来尝试获取属性为class的p节点的文本,也就是我们的电影名称

from lxml import etree

text = '''
<div class="movie-item-info">
        <p class="name"><a href="/films/1218273" title="误杀" data-act="boarditem-click" data-val="{movieId:1218273}">误杀</a></p>
        <p class="star">
                主演:肖央,谭卓,陈冲
<p class="releasetime">上映时间:2019-12-13    </div>
<div class="movie-item-info">
        <p class="name"><a href="/films/1190122" title="叶问4:完结篇" data-act="boarditem-click" data-val="{movieId:1190122}">叶问4:完结篇</a></p>
        <p class="star">
                主演:甄子丹,吴樾,吴建豪
        </p>
<p class="releasetime">上映时间:2019-12-20</p>    </div>
'''
html = etree.HTML(text)
result = html.xpath('//p[@class="name"]/text()')
print(result)

运行结果如下:

[]

  我们可以看到我们并没有获取到任何文本,书上给的解释是:XPath中text()前面是/,而此处/的含义是选取直接子节点,p的直接子节点都是a节点,文本都是在a节点内部的。我们错误的使用了/使得我们没有匹配到想要获取的内容
  如果想要获取p节点的内部文本,就有两种获取方式,一种是先获取a节点在获取文本,另一种就是使用//。接下来,我们来看一下二者的区别:
首先,选取到a节点在获取文本,代码如下:

from lxml import etree

text = '''
<div class="movie-item-info">
        <p class="name"><a href="/films/1218273" title="误杀" data-act="boarditem-click" data-val="{movieId:1218273}">误杀</a></p>
        <p class="star">
                主演:肖央,谭卓,陈冲
<p class="releasetime">上映时间:2019-12-13    </div>
<div class="movie-item-info">
        <p class="name"><a href="/films/1190122" title="叶问4:完结篇" data-act="boarditem-click" data-val="{movieId:1190122}">叶问4:完结篇</a></p>
        <p class="star">
                主演:甄子丹,吴樾,吴建豪
        </p>
<p class="releasetime">上映时间:2019-12-20</p>    </div>
'''
html = etree.HTML(text)
result = html.xpath('//p[@class="name"]/a/text()')
print(result)

运行结果如下:

['误杀', '叶问4:完结篇']

  可以看到这里的返回值是两个,内容都是属性为name的p节点的文本。这里我们是逐层选取的,先选取了p节点,有利用/选取了其直接子节点a,然后再选取文本,得到的结果恰好是我们预期的两个结果
  再来看下用另外一种方式(即使用//)选取的结果,代码如下:

from lxml import etree

text = '''
<div class="movie-item-info">
        <p class="name"><a href="/films/1218273" title="误杀" data-act="boarditem-click" data-val="{movieId:1218273}">误杀</a></p>
        <p class="star">
                主演:肖央,谭卓,陈冲
<p class="releasetime">上映时间:2019-12-13    </div>
<div class="movie-item-info">
        <p class="name"><a href="/films/1190122" title="叶问4:完结篇" data-act="boarditem-click" data-val="{movieId:1190122}">叶问4:完结篇</a></p>
        <p class="star">
                主演:甄子丹,吴樾,吴建豪
        </p>
<p class="releasetime">上映时间:2019-12-20</p>    </div>
'''
html = etree.HTML(text)
result = html.xpath('//p[@class="name"]//text()')   # 使用//获取p节点下的文本
print(result)

运行结果如下:

['误杀', '叶问4:完结篇']

  我们可以看到我们还是得到了预期的结果,但是如果我们想获取属性为star的p节点下的文本,代码如下:

from lxml import etree

text = '''
<div class="movie-item-info">
        <p class="name"><a href="/films/1218273" title="误杀" data-act="boarditem-click" data-val="{movieId:1218273}">误杀</a></p>
        <p class="star">
                主演:肖央,谭卓,陈冲
<p class="releasetime">上映时间:2019-12-13    </div>
<div class="movie-item-info">
        <p class="name"><a href="/films/1190122" title="叶问4:完结篇" data-act="boarditem-click" data-val="{movieId:1190122}">叶问4:完结篇</a></p>
        <p class="star">
                主演:甄子丹,吴樾,吴建豪
        </p>
<p class="releasetime">上映时间:2019-12-20</p>    </div>
'''
html = etree.HTML(text)
result = html.xpath('//p[@class="star"]//text()')
print(result)

运行结果:

['\n                主演:肖央,谭卓,陈冲\n', '\n                主演:甄子丹,吴樾,吴建豪\n        ']

  我们发现虽然获取到了我们想要的内容,但是里面还有一些换行符。所以说,如果想要获取子孙节点内部的所有文本,可以直接使用//加text()方式,这样可以保证获取到最全面的文本信息,但是可能会夹杂一些换行符等特殊字符。如果想要获取某些特定子孙节点下的所有文本,可以先获取到特定的子孙节点,然后再调用text()方法获取其内部文本,这样可以保证我们的结果是整洁的

0x07 属性获取

  我们知道用text()可以获取节点内部文本,那么节点属性该怎样获取呢?其实还是用@符号就可以。例如,我们想获取所有p节点下所有a节点的href属性,代码如下:

from lxml import etree

text = '''
<div class="movie-item-info">
        <p class="name"><a href="/films/1218273" title="误杀" data-act="boarditem-click" data-val="{movieId:1218273}">误杀</a></p>
        <p class="star">
                主演:肖央,谭卓,陈冲
<p class="releasetime">上映时间:2019-12-13    </div>
<div class="movie-item-info">
        <p class="name"><a href="/films/1190122" title="叶问4:完结篇" data-act="boarditem-click" data-val="{movieId:1190122}">叶问4:完结篇</a></p>
        <p class="star">
                主演:甄子丹,吴樾,吴建豪
        </p>
<p class="releasetime">上映时间:2019-12-20</p>    </div>
'''
html = etree.HTML(text)
result = html.xpath('//p/a/@href')
print(result)

  这里我们通过@href即可获取节点的href属性。注意,此处和属性匹配的方法不同,属性匹配是中括号加属性名和值来限定某个属性,如[@href="/films/1218273"],而此处的@href指的是获取节点的某个属性,二者需要做好区分。
运行结果如下:

['/films/1218273', '/films/1190122']

  我们成功获取了所有p节点下的a节点的href属性,他们以列表的形式返回

0x08 属性多值匹配

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

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()')
print(result)

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

['first item']


0x09 多属性匹配

  另外,我们可能遇到一种情况,那就是根据多个属性确定一个节点,这时就需要同时匹配多个属性。此时可以使用运算符and来连接,示例如下:

from lxml import etree

text = '''
<li class="li li-first" name="item"><a href="link.html">first item</a></li>
'''
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']


0x10 按序选择

  有时候我们在选择的时候某些属性可能同时匹配了多个节点,但是又想要其中的某个节点,这是我们可以利用中括号传入索引的方法来获取特定次序的节点,示例如下:

from lxml import etree

text = '''
<div>
<ul>
<li class="item-0"><a href="link1.html">first item</a></li>
<li class="item-0"><a href="link2.html">second item</a></li>
<li class="item-1"><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)
result_1 = html.xpath('//li[1]/a/text()')
print(result_1)
result_2 = html.xpath('//li[last()]/a/text()')
print(result_2)
result_3 = html.xpath('//li[position()<3]/a/text()')
print(result_3)
result_4 = html.xpath('//li[last()-2]/a/text()')
print(result_4)

  第一次选择时我们选取第一个li节点,中括号传入数字1即可。第二次选择时我们选取最后一个li节点,中括号中传入last()即可。第三次选择时,我们选取了位置小于3的li节点,也就是位置序号为1和2的li节点。第四次选择时,我们选取了倒数第三个li节点
运行结果如下:

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

结语

  XPath选择器的功能十分强大,使用起来简单方便,大大的提升了我们爬虫的效率。

上一篇:python操作xpath 0227


下一篇:Python爬虫实践 —— 7.秘密网鬼故事大全故事爬取(lxml xpath+requests)