跟着崔庆才学爬虫2:XPATH的使用

Xpath的全程是 XML Path Language,即xml路径语言,用来在XML文档中查找信息,它最初就是用来搜寻xml文档的,但同时适用于html文档的搜索。

所以在做爬虫时,我们完全可以使用Xpath实现对应的信息抽取,本节我们就介绍一下Xpath的基本用法。

1.Xpath的概览

首先,Xpath的选择功能十分强大,它提供了非常简洁明了的路径选择表达式,另外,它还提供了100多个内建函数,用于字符串,数值,时间的匹配以及节点,序列的处理等,几乎所有我们想要的定位节点,都可以用Xpath选择。

Xpath于1999年11月16日成为W3C标准,它被设计出来,供XSLT,XPointer以及其他XML解析软件使用

2.Xpath常用规则

具体语法规则可以参考下面网站

XPath 语法 | 菜鸟教程XPath 语法 XPath 使用路径表达式来选取 XML 文档中的节点或节点集。节点是通过沿着路径 (path) 或者步 (steps) 来选取的。 XML 实例文档 我们将在下面的例子中使用这个 XML 文档。 实例 [mycode3 type='xml'] Harry Potter 29.99 Learning XML 39.95 [/mycode3] 选取节点 ..icon-default.png?t=N7T8https://www.runoob.com/xpath/xpath-syntax.html这里列出了XPath的一个常用匹配规则,如下:

//title[@lang='eng']

 它代表了选择所有名称为title,同时属性lang的值为eng的节点。

后面通过python的lxml库,利用Xpath对HTML进行解析

3.准备工作

使用lxml库之前,首先确保其已经安装

安装代码:pip install lxml

详情参考 https://setup.scrape.center/lxml

安装完成

Looking in indexes: https://pypi.tuna.tsinghua.edu.cn/simple
Collecting lxml
  Downloading https://pypi.tuna.tsinghua.edu.cn/packages/31/58/e3b3dd6bb2ab7404f1f4992e2d0e6926ed40cef8ce1b3bbefd95877499e1/lxml-4.9.3-cp311-cp311-win_amd64.whl (3.8 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3.8/3.8 MB 4.3 MB/s eta 0:00:00
Installing collected packages: lxml
Successfully installed lxml-4.9.3

如果大家使用谷歌浏览器的话,可以安装一个xpath helper的插件

这个插件可以方便我们直接在网页上输入xpath代码,并返回我们想要的结果,是一个不错的实用插件,具体安装方法可以在网上搜索一下。我的插件之前装完不知道放哪了。

4.实例引入

下面我们通过实例感受一下xpath对网页进行解析的过程,相关代码如下:

from lxml import etree
text='''   
        <div data-v-63864230="" class="actor el-col el-col-4" style="padding-left: 7.5px; padding-right: 7.5px;">
          <div data-v-63864230="" class="el-card m-b is-hover-shadow"><!---->
            <div class="el-card__body">
              <img data-v-63864230=""
                   src="https://p0.meituan.net/movie/2dfc1d8df6b41d2055e637c1a4b04e0e13679.jpg@128w_170h_1e_1c"
                   class="image">
              <p data-v-63864230="" class="el-tooltip name text-center m-b-none m-t-xs item"
                 aria-describedby="el-tooltip-6009" tabindex="0">Dulcie Smart</p>
              <p data-v-63864230="" class="el-tooltip role text-center m-b-none m-t-xs item"
                 aria-describedby="el-tooltip-9582" tabindex="0">饰:Civil War News Poppet</p></div>
          </div>
        </div>
        '''
html=etree.HTML(text)
result = etree.tostring(html)
print(result.decode('utf-8'))

 这里我们随便找了一段网页代码,首先导入lxml库的etree模块,然后声明了一段HTML文本,接着调用HTML类进行初始化,这样就成功构造了一个Xpath解析对象,此处需要注意一点,HTML的文本如果没有对其或者闭合,etree模块会帮我们自动修正HTML文本的。

之后调用tostring方法即可输出修正后的HTML代码。但是结果时bytes类型,于是利用decode方法将其转化str类型。输出结果如下:

<html><body><div data-v-63864230="" class="actor el-col el-col-4" style="padding-left: 7.5px; padding-right: 7.5px;">
          <div data-v-63864230="" class="el-card m-b is-hover-shadow"><!---->
            <div class="el-card__body">
              <img data-v-63864230="" src="https://p0.meituan.net/movie/2dfc1d8df6b41d2055e637c1a4b04e0e13679.jpg@128w_170h_1e_1c" class="image"/>
              <p data-v-63864230="" class="el-tooltip name text-center m-b-none m-t-xs item" aria-describedby="el-tooltip-6009" tabindex="0">Dulcie Smart</p>
              <p data-v-63864230="" class="el-tooltip role text-center m-b-none m-t-xs item" aria-describedby="el-tooltip-9582" tabindex="0">&#39280;&#65306;Civil War News Poppet</p></div>
          </div>
        </div>
        </body></html>

可以看到的是,它不仅可以不全代码,还自动添加了body,html节点。

另外也可以不声明。直接读取文本进行解析,示例如下:

from lxml import etree
html=etree.parse('xpath的基本使用\\test.html',etree.HTMLParser())
result = etree.tostring(html)
print(result.decode('utf-8'))

其中文件中的内容就是上面例子中的HTML代码。这次输出的结果和上次不同,多了一个DOCTYPE声明,不过对解析没有任何影响,结果如下

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><div data-v-63864230="" class="actor el-col el-col-4" style="padding-left: 7.5px; padding-right: 7.5px;">&#13;
    <div data-v-63864230="" class="el-card m-b is-hover-shadow"><!---->&#13;
      <div class="el-card__body">&#13;
        <img data-v-63864230="" src="https://p0.meituan.net/movie/2dfc1d8df6b41d2055e637c1a4b04e0e13679.jpg@128w_170h_1e_1c" class="image"/>&#13;
        <p data-v-63864230="" class="el-tooltip name text-center m-b-none m-t-xs item" aria-describedby="el-tooltip-6009" tabindex="0">Dulcie Smart</p>&#13;
        <p data-v-63864230="" class="el-tooltip role text-center m-b-none m-t-xs item" aria-describedby="el-tooltip-9582" tabindex="0">&#39280;&#65306;Civil War News Poppet</p></div>&#13;
    </div>&#13;
  </div>&#13;
  </body></html>
5.所有节点

我们一般会用//开头的xpath规则,来选取所有符合要求的节点,这里还是以之前实例中HTML文本为例,选取其中的所有节点,实现代码如下:

from lxml import etree
html=etree.parse('xpath的基本使用\\test.html',etree.HTMLParser())
result = html.xpath('//*')
print(result)

运行结果如下
 

[<Element html at 0x2757cb68140>, <Element body at 0x2757d0ce240>, <Element div at 0x2757d0ce340>, <Element div at 0x2757d0ce5c0>, <Element div at 0x2757d0ce200>, <Element img at 0x2757d0ce4c0>, <Element p at 0x2757d0ce300>, <Element p at 0x2757d0ce640>]

这里使用*代表匹配所有节点,也就是获取了整个HTML文本中的所有节点,如果只想获取某一类型的节点例如文本中含有p节点,我们就可以改写代码:

from lxml import etree
html=etree.parse('xpath的基本使用\\test.html',etree.HTMLParser())
result = html.xpath('//p')
print(result)

 输出结果

[<Element p at 0x2505a3ee240>, <Element p at 0x2505a3ee340>]

可以看到提取结果也是一个列表,其中每个元素都是Element类型,要是想取出其中一个对象,可以直接用中括号加元素索引获取,如[0]

6.子节点

通过/或者//即可查找元素的子节点或子孙节点,假如现在想获取div节点下的所有p节点,则可以这样实现:

from lxml import etree
html=etree.parse('xpath的基本使用\\test.html',etree.HTMLParser())
result = html.xpath('//div/p')
print(result)

通过追加/p的方式,选择了所有div下的p节点,其中//div用于选择所有li节点,/a用于选择li节点的所有子节点a

运行结果如下:

[<Element p at 0x20e6d59a380>, <Element p at 0x20e6d59a480>]

 有人会说这两次的返回值是一样的,我直接//p节点也可以获取这两个节点,其实是不一样的

我们在原文档的代码再加上两个p节点

<html><body><div data-v-63864230="" class="actor el-col el-col-4" style="padding-left: 7.5px; padding-right: 7.5px;">
    <div data-v-63864230="" class="el-card m-b is-hover-shadow"><!---->
      <div class="el-card__body">
        <img data-v-63864230="" src="https://p0.meituan.net/movie/2dfc1d8df6b41d2055e637c1a4b04e0e13679.jpg@128w_170h_1e_1c" class="image"/>
        <p data-v-63864230="" class="el-tooltip name text-center m-b-none m-t-xs item" aria-describedby="el-tooltip-6009" tabindex="0">Dulcie Smart</p>
        <p data-v-63864230="" class="el-tooltip role text-center m-b-none m-t-xs item" aria-describedby="el-tooltip-9582" tabindex="0">&#39280;&#65306;Civil War News Poppet</p></div>
    </div>
  </div>
  <p>hello</p>
  <p>world</p>
  </body></html>

 再次运行发现,依然只有两个p节点被获取了,这是因为div下的p节点只有两个,如果//p获取的话会获取所有的p节点,所以对比较精细的数据进行提取的时候,要注意提取格式,不要出现获取不必要的数据,加重运行负担。

上面的/用于选取节点的直接子节点,如果要获取节点的所有子孙节点,可以使用//,例如要获取body节点下的所有img节点,就可以写成//body//img

运行结果都是一样的,但是如果写成//body/img就无法获取任何结果,因为/用于获取直接子节点,而body下没有直接的img子节点,只有div和p节点,所有无法获取任何匹配结果。输出结果为[]

因此这里要注意/和//的区别,前者用于获取直接子节点,后者用于获取子孙节点。

父节点

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

首先选择a节点中href属性为detail/1,然后获取其父节点,在获取父节点的class属性,相关代码如下

html代码

<div data-v-7f856186="" class="p-h el-col el-col-24 el-col-xs-9 el-col-sm-13 el-col-md-16">
    <a data-v-7f856186="" href="/detail/1" class="name">
      <h2 data-v-7f856186="" class="m-b-sm">霸王别姬 - Farewell My Concubine</h2>
    </a>
    <div data-v-7f856186="" class="categories">
      
      <button data-v-7f856186="" type="button"
              class="el-button category el-button--primary el-button--mini">
        <span>剧情</span>
      </button>
      
      <button data-v-7f856186="" type="button"
              class="el-button category el-button--primary el-button--mini">
        <span>爱情</span>
      </button>
      
    </div>

 python代码

from lxml import etree
html=etree.parse('xpath的基本使用\\test2.html',etree.HTMLParser())
result = html.xpath('//a[@href="/detail/1"]/../@class')
print(result)

运行结果

['p-h el-col el-col-24 el-col-xs-9 el-col-sm-13 el-col-md-16']

正好就是父节点的class属性。

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

from lxml import etree
html=etree.parse('xpath的基本使用\\test2.html',etree.HTMLParser())
result = html.xpath('//a[@href="/detail/1"]/parent::*/@class')
print(result)

运行结果是一样的

属性匹配

在选取节点的时候,还可以使用@符号实现属性过滤,例如,选取class属性为name的a节点,可以这样实现

from lxml import etree
html=etree.parse('xpath的基本使用\\test2.html',etree.HTMLParser())
result = html.xpath('//a[@class="name"]')
print(result)

运行结果:[<Element a at 0x1369dc9e740>]

 这里符合xpath的表达式的只有一个结果,那么就会返回一个,如果多个表达式符合,那么就都会进行返回,如果返回的结果不是想要的,那么xpath还需要添加别的方法进行验证。

文本获取

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

from lxml import etree
html=etree.parse('xpath的基本使用\\test2.html',etree.HTMLParser())
result = html.xpath('//a[@class="name"]/text()')
print(result)

运行结果:['\r\n      ', '\r\n    ']

 奇怪的是,没有获取任何文本,只获取了换行符,这是为什么呢?因为xpath中的text方法前面是/,而/的含义是选取直接子节点,很明显a的子节点是h2,文本都在h2节点内部,所以这里匹配的结果就是被修正a节点内部的换行符,因为自动修正的a节点尾部标签换行了。

因此,如果想要获取a节点的文本,就有两种方式,一种是先选取a节点在获取文本,另一种是使用//

我们写代码举例

from lxml import etree
html=etree.parse('xpath的基本使用\\test2.html',etree.HTMLParser())
result = html.xpath('//a[@class="name"]//text()')
print(result)

因为结果含有中文,输出结果会有乱码['\r\n      ', 'é\x9c¸ç\x8e\x8bå\x88«å§¬ - Farewell My Concubine', '\r\n    ']

 

from lxml import etree
html=etree.parse('xpath的基本使用\\test2.html',etree.HTMLParser())
result = html.xpath('//a[@class="name"]/h2/text()')
print(result)

由此,想要获取子孙节点内部的所有文本可以直接使用//加text方法的方式,这样能够保证获取最全面的文本信息,但是可能会夹杂一些换行符,如果只想获取某些特定子孙节点下的所有文本,则可以先选取特定的子孙节点,在调用text方法获取其内部的文本,这样就可以保证获取的结果是整洁的。

属性获取

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

from lxml import etree
html=etree.parse('xpath的基本使用\\test2.html',etree.HTMLParser())
result = html.xpath('//a/@href')
print(result)

运行结果:['/detail/1']

 这里只有一个a节点并且有href值,如果存在多个,则会返回多个,并以列表形式返回

属性多值匹配

有些时候,某个节点的某个属性可能有多个值,或者属性值存在多个节点,这个时候要匹配节点就要进行多值匹配

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

这里的li节点有两个属性,如果用上述的方法获取节点,是无法获取到内容的

运行结果:

[]

这种情况就要用到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)

运行结果:['first item']

上面使用了contains方法,给其第一个参数传入属性名称,第二个参数传入属性值,只要传入的属性包含传入的属性值,就可以完成匹配

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

多属性匹配

我们还可能遇到一种情况,就是根据多个属性确定一个节点,这是需要同时匹配多个节点,运算符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,因此要确定li节点,需要同时考察class和name属性,一个条件是class属性里面包含li字符串,另一个条件是name属性的item字符串,这两者同时满足才能获取,class和name属性需要用and运算符相连,相连之后置于中括号内进行条件筛选,运行结果如下

['first item']

按序选择

在选择节点时,某个属性同时匹配了多个节点,但是我们只要其中一个,第一个,第二个或者最后一个,这是应该怎么办?

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

from lxml import etree
text= '''
<div>
    <ul>
        <li class="item-0"><a href="link1.html">first</a></li>
        <li class="item-1"><a href="link3.html">second</a></li>
        <li class="item-inactive"><a href="link4.html">third</a></li>
        <li class="item-3"><a href="link5.html">fourth</a></li>
        <li class="item-4"><a href="link2.html">fifth</a></li>
    </ul>
</div>
'''
html=etree.HTML(text)
result=html.xpath('//li[1]/a/text()')
result2=html.xpath('//li[last()]/a/text()')
result3=html.xpath('//li[position()<3]/a/text()')
result4=html.xpath('//li[last()-2]/a/text()')
print(result)
print(result2)
print(result3)
print(result4)

上述代码中,第一次选择选取第一个li节点,在中括号中传入数字一即可实现,注意的是和代码不同传入的是数字1,而非0

第二次选择时选择最后一个li节点,在中括号中调用last方法即可实现。

第三次选择是,选择位置小于3的li节点,也就是位置序号为1,2的节点,得到的结果就是前两个节点。

第四次选择时,选取了倒数第三个li节点,在中括号中调用last方法在减去2即可实现,因为last方法时最后一个,在此基础上减2就得到了倒数三个

运行结果如下

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

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值