数据解析之Xpath

1. xpath基本语法

1.1.介绍

XPath 使用路径表达式来选取 XML 文档中的节点或节点集。XML,可能很多同学都不知道这是个什么东西,XML和HTML很相似,但是也有本质的区别。这里来简单介绍一下。

HTML 是超文本标记语言,HTML 使用标记标签来描述网页,它的文档中包含了 HTML 标签以及文本内容,HTML 文档也叫做 web 页面。

XML 是可扩展标记语言,用于创建网页和 web 应用程序。XML 是动态的,可用来传输数据。

区别

    • 语言类型不同;HTML 是超文本标记语言,而 XML 是可扩展标记语言。
    • 功能状态不同;HTML 用于显示数据,它是静态的;而 XML 用于传输数据,它是动态的。
    • 区分大小写;HTML 中不区分大小写,而 XML 需要区分大小写。
    • 标签数量;HTML 的标签数量有限,而 XML 标记是可扩展的。
    • 结束标记;HTML 中不需要结束标记,而 XML 中需要结束标记。
    • 预定义;HTML 中有预定义标记,而 XML 中是用户定义的标记。

这些概念大家了解一下即可,只需要知道Xpath不仅仅可以选取XML文档中的节点或节点集,也适用与HTML,目前的话已经很少见XML了,基本都是HTML。

1.2.语法

实例

<bookstore>
  <book>
    <title lang="eng">Harry Potter</title>
    <price>29.99</price>
  </book>
  <book>
    <title lang="eng">Learning XML</title>
    <price>39.95</price>
  </book>
</bookstore>

1.2.1.选取节点

XPath 使用路径表达式在 XML 文档中选取节点。节点是通过沿着路径或者 step 来选取的。 下面列出了最有用的路径表达式:

表达式

描述

nodename

选取此节点的所有子节点。

/

从根节点选取。

//

从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置。

.

选取当前节点。

..

选取当前节点的父节点。

@

选取属性。

在下面的表格中,我们已列出了一些路径表达式以及表达式的结果:

路径表达式

结果

bookstore

选取 bookstore 元素的所有子节点。

/bookstore

选取根元素 bookstore。注释:假如路径起始于正斜杠( / ),则此路径始终代表到某元素的绝对路径!

bookstore/book

选取属于 bookstore 的子元素的所有 book 元素。

//book

选取所有 book 子元素,而不管它们在文档中的位置。

bookstore//book

选择属于 bookstore 元素的后代的所有 book 元素,而不管它们位于 bookstore 之下的什么位置。

//@lang

选取名为 lang 的所有属性。

1.2.2.谓语

谓语用来查找某个特定的节点或者包含某个指定的值的节点。

谓语被嵌在方括号中。

在下面的表格中,我们列出了带有谓语的一些路径表达式,以及表达式的结果:

路径表达式

结果

/bookstore/book[1]

选取属于 bookstore 子元素的第一个 book 元素。

/bookstore/book[last()]

选取属于 bookstore 子元素的最后一个 book 元素。

/bookstore/book[last()-1]

选取属于 bookstore 子元素的倒数第二个 book 元素。

/bookstore/book[position()]

选取最前面的两个属于 bookstore 元素的子元素的 book 元素。

//title[@lang]

选取所有拥有名为 lang 的属性的 title 元素。

//title[@lang='eng']

选取所有 title 元素,且这些元素拥有值为 eng 的 lang 属性。

/bookstore/book[price>35.00]

选取 bookstore 元素的所有 book 元素,且其中的 price 元素的值须大于 35.00。

/bookstore/book[price>35.00]//title

选取 bookstore 元素中的 book 元素的所有 title 元素,且其中的 price 元素的值须大于 35.00。

需要注意xpath索引值是从1开始,而非从0开始。

1.2.3.选取未知节点

XPath 通配符可用来选取未知的 XML 元素。

通配符

描述

*

匹配任何元素节点。

@*

匹配任何属性节点。

node()

匹配任何类型的节点。

在下面的表格中,我们列出了一些路径表达式,以及这些表达式的结果:

路径表达式

结果

/bookstore/*

选取 bookstore 元素的所有子元素。

//*

选取文档中的所有元素。

//title[@*]

选取所有带有属性的 title 元素。

1.2.4.选取若干路径

通过在路径表达式中使用"|"运算符,您可以选取若干个路径。

在下面的表格中,我们列出了一些路径表达式,以及这些表达式的结果:

路径表达式

结果

//book/title | //book/price

选取 book 元素的所有 title 和 price 元素。

//title | //price

选取文档中的所有 title 和 price 元素。

/bookstore/book/title | //price

选取属于 bookstore 元素的 book 元素的所有 title 元素,以及文档中所有的 price 元素。

2. jsonpath语法与xpath语法对比

2.1.语法对比

XPath

JsonPath

说明

/

$

文档根元素

.

@

当前元素

/

.或[]

匹配下级元素

..

N/A

匹配上级元素,JsonPath不支持此操作符

//

..

递归匹配所有子元素

*

*

通配符,匹配下级元素

@

N/A

匹配属性,JsonPath不支持此操作符

[]

[]

下标运算符,根据索引获取元素,XPath索引从1开始,JsonPath索引从0开始

`

`

[,]

N/A

[start:end:step]

数据切片操作,XPath不支持

[]

?()

过滤表达式

N/A

()

脚本表达式,使用底层脚本引擎,XPath不支持

()

N/A

分组,JsonPath不支持

Xpath用于解析XML/HTML,jsonpath用于解析json数据,正则用于解析字符串。

2.2.实例操作对比

在之前学习jsonpath的时候有这么一个例子:

json格式实例

{
  "store": {
    "book": [{
      "category": "reference",
      "author": "Nigel Rees",
      "title": "Sayings of the Century",
      "price": 8.95
    }, {
      "category": "fiction",
      "author": "Evelyn Waugh",
      "title": "Sword of Honour",
      "price": 12.99
    }, {
      "category": "fiction",
      "author": "Herman Melville",
      "title": "Moby Dick",
      "isbn": "0-553-21311-3",
      "price": 8.99
    }, {
      "category": "fiction",
      "author": "J. R. R. Tolkien",
      "title": "The Lord of the Rings",
      "isbn": "0-395-19395-8",
      "price": 22.99
    }
    ],
    "bicycle": {
      "color": "red",
      "price": 19.95
    }
  }

获取同样节点jsonpath规则与xpath规则有什么不同呢?下面给大家列出来了。不过这里一定需要注意xpath是无法解析json格式数据的,这里只是给大家举例获取节点模拟一下。

接下来我们看一下如何对这个文档进行解析:

XPath

JsonPath

Result

/store/book/author

$.store.book[*].author

所有book的author节点

//author

$..author

所有author节点

/store/*

$.store.*

store下的所有节点,book数组和bicycle节点

/store//price

$.store..price

store下的所有price节点

//book[3]

$..book[2]

匹配第3个book节点

//book[last()]

$..book[(@.length-1)],或 $..book[-1:]

匹配倒数第1个book节点

//book[position()<3]

$..book[0,1],或 $..book[:2]

匹配前两个book节点

//book[isbn]

$..book[?(@.isbn)]

过滤含isbn字段的节点

//book[price<10]

$..book[?(@.price<10)]

过滤price<10的节点

//*

$..*

递归匹配所有子节点

 参考文档:XPath 介绍 | Apifox 帮助文档

3. python程序使用xpath

3.1. xpath 依赖库使用

3.1.1. 依赖库安装

想要在python当中使用 xpath 语法进行数据解析需要安装 xpath 依赖库。

普通安装:pip install lxml

镜像源安装: pip install lxml -i http://pypi.douban.com/simple/

安装好之后就可以使用该库写xpath规则解析数据了。

3.1.2. lxml 库应用

导入模块

from lxml import etree

 虽然是 xpath 解析库,但是不论是从库名还是方法名来看都是和 xpath 没有关系的,所以也是需要大家打破传统的思想,认为使用什么技术那么所依赖的库名肯定就是该技术名称,其实是不对的,之后大家会遇到很多名称和库名不对应的依赖库的。
库导入成功后,使用第一小节中的实例代码给大家演示一下该模块的使用方法。

etree基本使用

from lxml import etree


line = """
<bookstore>
  <book>
    <title lang="eng">Harry Potter</title>
    <price>29.99</price>
  </book>
  <book>
    <title lang="eng">Learning XML</title>
    <price>39.95</price>
  </book>
</bookstore>
"""
tree = etree.XML(line)  # 因为line数据是XML结构,所以是.XML(), 如果数据是HTML结构就是.HTML()
print(tree)

可以看到打印结果是一个 Element 对象,只要数据类型是 Element 对象就可以使用.xpath()方法。比如现在我们要获取第一个 book 标签下的 price 节点, xpath 规则为/bookstore/book[1]/price, 那么在程序中就可以写成:

获取第一个book下的price节点

from lxml import etree


line = """
<bookstore>
  <book>
    <title lang="eng">Harry Potter</title>
    <price>29.99</price>
  </book>
  <book>
    <title lang="eng">Learning XML</title>
    <price>39.95</price>
  </book>
</bookstore>
"""
tree = etree.XML(line)  # 因为line数据是XML结构,所以是.XML(), 如果数据是HTML结构就是.HTML()
# print(tree)
first_price = tree.xpath('/bookstore/book[1]/price')
print(first_price)

就获取到了 price 节点,如果想要该节点下的文本值,就只需要在 xpath 规则后面跟上/text()即可。

如果是要获取第一个book节点下面的title节点的lang属性值,就可以写成:

返回的是一个数据结构是列表,要取具体文本值的话就只需要使用列表取值操作去取值即可。其他的解析需求各位自行按照第一小节去尝试获取,这里我就不再一一进行演示了,自己一定要都去试试,别我不演示你就不去试了,那我会了不代表你也会了,能力都是敲出来的。接下来就上案例。

3.2. 案例实战

本次案例选择的是自如租房,获取房屋数据(标题)。抓包的流程就不再赘述,通过关键词检索发现数据就在z0/这个数据包当中,所以需要先获取这个z0/数据包。

请求方式为get,无需请求参数,那就看看请求头里面有没有特殊的参数。

都是普通参数,那就带上这个User-Agent:用户代理发起请求。

发起请求获取响应保存本地html

import requests


class MySpider(object):
    def __init__(self):
        self.url = "https://www.ziroom.com/z/z0/"
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'
        }

    def Urequest(self):  # 封装的请求函数
        return requests.get(url=self.url, headers=self.headers)

    def getMainHtml(self):
        response = self.Urequest().content.decode()
        with open('自如.html', 'w', encoding='utf8') as f:
            f.write(response)


if __name__ == '__main__':
    s = MySpider()
    s.getMainHtml()

运行后就可以看到本地文件当中多出了一个自如.html文件,打开里面是有数据的,那再通过关键词搜索看看数据在不在里面,也就是看看有没有获取到正确的响应数据。

现在可以确定数据就在这个响应当中,就可以开始着手解析数据了。这里直接就给大家讲遇到这种数据的解析思路了。可以看到页面当中是有很多房屋信息的,那就要先找到整个房屋数据标签是什么,这时候可以在抓包工具中的 Elements 里面去查看一下。

这个是所有数据的标签,下面的一个又一个div标签就是每一条数据。

所以就可以先写到这个第一个房屋信息标签的 xpath 规则,快捷方法是直接右键标签选择copy xpath

但这种方法存在一个问题,就是这里copy的xpath规则是按照 Elements 结构的规则,而非真实响应数据的规则,也就是说当响应数据与 Elements 结构不同时,直接copy的xpath规则可能会导致获取不到数据,所以此方法虽好,但不注意就会掉进一些坑里面去。作为一个骨灰级爬虫玩家,我选择手写 xpath 规则。

先来看一下,一般先找整个数据标签,也就是:

在咱们的本地文件中搜一下这个class值Z_list-box

发现该值是唯一的,那么就可以直接通过这个特征进行定位。

定位总数据标签

import requests
from lxml import etree

class MySpider(object):
    def __init__(self):
        self.url = "https://www.ziroom.com/z/z0/"
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'
        }

    def Urequest(self):  # 封装的请求函数
        return requests.get(url=self.url, headers=self.headers)

    def getMainHtml(self):
        response = self.Urequest().content.decode()
        # with open('自如.html', 'w', encoding='utf8') as f:
        #     f.write(response)
        tree = etree.HTML(response)  # 现在解析的是HTML结构所以是.HTML()
        div_list = tree.xpath('//div[@class="Z_list-box"]')


if __name__ == '__main__':
    s = MySpider()
    s.getMainHtml()

tree.xpath('//div[@class="Z_list-box"]')这句的意思是跨节点定位一个div标签,什么条件的div标签呢,class值为Z_list-box的div标签,所以就能够拿到该div节点。

现在总的数据标签拿到了,但是具体数据是在下面的子div标签里面的,所以还需要跨一个div节点获取到所有的div节点,也就是tree.xpath('//div[@class="Z_list-box"]/div'),没有指定是什么div,所以就会把子代div都获取到,如果是/div[1],那么就是第一条数据节点,不指定就是所有的子代div节点。

现在就获取到了很多的div节点了,且这些节点都是被列表包裹的,可以使用len(div_list)查看数量。现在单条房屋信息数据标签列表拿到了,就该去循环这个列表继续去精准xpath取值了。

先来获取房屋标题。这个时候为了更好的分析结构,我们可以单独拿一个数据标签出来放到一个新的html文件中进行观察。

通过结构展开后,收起不需要的标签,整个结构就非常清晰了。

可以看到房屋标题是 h5 标签下面的 a 标签的文本值,搜一下 h5 标签是不是唯一的,一般这种标题型标签都是比较特殊的。

是唯一的,那么如果你大胆一点就直接可以//h5/a/text()获取,如果想要保守一点就/div[@class="info-box"]/h5/a/text()

获取房屋标题

import requests
from lxml import etree

class MySpider(object):
    def __init__(self):
        self.url = "https://www.ziroom.com/z/z0/"
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'
        }

    def Urequest(self):  # 封装的请求函数
        return requests.get(url=self.url, headers=self.headers)

    def getMainHtml(self):
        response = self.Urequest().content.decode()
        # with open('自如.html', 'w', encoding='utf8') as f:
        #     f.write(response)
        tree = etree.HTML(response)  # 现在解析的是HTML结构所以是.HTML()
        div_list = tree.xpath('//div[@class="Z_list-box"]/div')
        for div in div_list:
            title = div.xpath('.//h5/a/text()')  # . 是代表当前节点
            print(title)


if __name__ == '__main__':
    s = MySpider()
    s.getMainHtml()

这里说一下为什么循环的div也能继续.xpath(),在上文中我们打印div_list的时候,大家能够看到列表当中包裹的是一个有一个的div Element对象,所以还能够继续写xpath规则,.就是代表当前节点,当前节点在哪呢,如果是第一次循环,那么当前节点就在

这个位置,以此类推。

查看输出结果,发现第五个数据是空的,来看看页面中第五个是什么东西。

第五个是广告信息,所以没有信息是正常的,也可以去本地看看,本地的第五个div结构是和其他div结构不一样的,先不管他,后面保存数据的时候再来处理这个东西。标题拿到之后还需要拿房屋详细信息,也就是

这个规则就更简单了,只需要定位class属性值为desc的div标签,然后再获取下面div节点的文本值即可。

两个div的文本值都是获取到了,当然如果你觉得这两个文本值不是同一个类型,你也可以单独进行获取,第二个文本值有很多空行,可以先将这两个元素拼接起来后再去除这些换行以及空格,拼接列表元素的话可以使用''.join()方法,前面引号当中是要以什么字符拼接列表元素,不写就是以空拼接,方法内写的就是列表对象。

拼接上之后就可以用replace()方法进行替换换行、制表符和空格了。

现在就已经拼接上了,老样子,第五个广告先不管。这个案例的价格的话是用图片的形式展示的,所以就先不获取价格了,再获取一下房屋图片的url吧。

这里出现了两个后缀为.jpg的url,那到底是哪一个呢,去到页面中看一下。

页面当中的图片url是src属性值,但是页面当中的src属性值和本地src属性值不同,通过对比,页面中的src值和本地的data-original属性值相同,这也是本地响应数据和Element结构不同的一个小点,直接从页面copy xpath 的话你很容易就写成了/@src,但实际上响应数据的/@src值与页面端完全不一样。

所以这里xpath规则可以这么写.//img/@data-original

第五个广告的图片又不一样,所以可以确定,第五个元素是我们不需要的,那在循环前直接把第五个元素删掉就好了。然后返回值都是列表,那就对列表进行下标取值就行了。图片url缺失协议头,所以还需要再前面拼接https:

image.png

import requests
from lxml import etree

class MySpider(object):
    def __init__(self):
        self.url = "https://www.ziroom.com/z/z0/"
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'
        }

    def Urequest(self):  # 封装的请求函数
        return requests.get(url=self.url, headers=self.headers)

    def getMainHtml(self):
        response = self.Urequest().content.decode()
        # with open('自如.html', 'w', encoding='utf8') as f:
        #     f.write(response)
        tree = etree.HTML(response)  # 现在解析的是HTML结构所以是.HTML()
        div_list = tree.xpath('//div[@class="Z_list-box"]/div')
        div_list.pop(4)  # 删除广告div节点
        for div in div_list:
            title = div.xpath('.//h5/a/text()')[0]  # . 是代表当前节点
            infoMessage = div.xpath('.//div[@class="desc"]/div/text()')
            info = "-".join(infoMessage).replace(' ', '').replace('\n', '').replace('\t', '')
            pic_url = 'https:' + div.xpath('.//img/@data-original')[0]
            print(title, info, pic_url)


if __name__ == '__main__':
    s = MySpider()
    s.getMainHtml()

image.png


数据成功拿到,保存为json文件就不需要再过多赘述了吧,之前都演示过了,我这里就直接给完整代码了,无非就是类的写法会有点区别。

完整代码

import json

import requests
from lxml import etree

class MySpider(object):
    def __init__(self):
        self.url = "https://www.ziroom.com/z/z0/"
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'
        }
        self.dic = {  # 定义类全局字典用于数据保存
            'list': []
        }

    def Urequest(self):  # 封装的请求函数
        return requests.get(url=self.url, headers=self.headers)

    def getMainHtml(self):
        response = self.Urequest().content.decode()
        # with open('自如.html', 'w', encoding='utf8') as f:
        #     f.write(response)
        tree = etree.HTML(response)  # 现在解析的是HTML结构所以是.HTML()
        div_list = tree.xpath('//div[@class="Z_list-box"]/div')
        div_list.pop(4)  # 删除广告div节点
        for div in div_list:
            title = div.xpath('.//h5/a/text()')[0]  # . 是代表当前节点
            infoMessage = div.xpath('.//div[@class="desc"]/div/text()')
            info = "-".join(infoMessage).replace(' ', '').replace('\n', '').replace('\t', '')
            pic_url = 'https:' + div.xpath('.//img/@data-original')[0]
            # print(title, info, pic_url)
            dic = {
                'title': title,
                'info': info,
                'picUrl': pic_url
            }
            self.dic["list"].append(dic)

    def saveJson(self):
        with open('自如.json', 'w', encoding='utf8') as f:
            json.dump(self.dic, f, ensure_ascii=False, indent=4)


if __name__ == '__main__':
    s = MySpider()
    s.getMainHtml()
    # 前面数据获取完成之后再调用保存函数
    s.saveJson()

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱学习的小su

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值