【Python】数据解析之 Xpath 解析

526 阅读10分钟

Xpath 是什么?

  • xpath 是一种 xml 的路径查询语言,是一种非常强大的定位方式。能在 xml 树状结构中寻找节点。用于在 xml 文档中通过元素和属性进行导航。(html 是 xml 的一种实现方式。)

image.png ( HTML 的结构就是树形结构,HTML 是根节点,所有的其他元素节点都是从根节点发出的。其他的元素都是这棵树上的节点Node,每个节点还可能有属性和文本。而路径就是指某个节点到另一个节点的路线。

节点之间的关系: image.png

  • xpath 是一种标记语法的文本格式,xpath 可以方便的定位 xml 中的元素和其中的属性值。lxml 是 python 中的一个第三方模块,包含了将 html 文本转成 xml 对象,和对象执行 xpath 功能。

Xpath 的安装:

pip install lxml

如果 Mac 本安装失败怎么处理?

对于使用 Mac 的小伙伴,如果遇到安装失败,通常是因为电脑没有安装 Xcode 软件(因为编译 lxml 时,会用到里面的一些命令),那么安装 Xcode 有两种方式:

方法一: 在终端输入命令:

xcode-select --install

方法二:

在 pycharm 中写好下方语句:如果 lxml 未安装或安装失败,lxml下方会有红色波浪形,然后选中 lxml,根据提示进行后续操作即可。

from lxml import etree

安装成功后,pycharm 中的语句下方红色波浪形会消失。

xpath的解析原理:

实例化一个 etree 类型的对象,且将页面源码数据加载到该对象中,需要调用该对象的 xpath 方法结合着不同形式的 xpath 表达式进行标签定位和数据提取。

etree 对象的实例化:

  • etree.parse(fileName) # 本地的 html 文件
  • etree.HTML(page_text) #声明一段 html 文本,调用 html 类进行初始化,构造了一个 Xpath 解析对象
  • Xpath 方法返回的永远是一个列表

Xpath 中的绝对路径与相对路径:

绝对路径:

以 / 开头,从 html 根节点开始算,一层一层找下去,直到找到需要的节点为止。

优点:
  • 路径一定是正确的
缺点:
  • 不够灵活
  • 路径可能会很长很长

相对路径:

以 // 开头,可以任意节点开始;通常我们会选取一个可以唯一定位到元素的开始写,增加查找的准确性。

优点:
  • 比较灵活
缺点:
  • 不保证百分百的正确

通过开发者工具,可以拷贝到 Xpath 的绝对路径和相对路径代码:

image.png

但是拷贝出来的代码缺乏灵活性,也不一定全是对的,大部分情况下,都需要自己定义 xpath 语句。

相对路径的定位语法:

  • 基本定位语法:

image.png

  • 元素属性定位:

通过 @ 符号指定需要使用的属性。

image.png

  • 层级属性结合定位: 通过某些元素无法精确定位时,可以查找其父级及其祖先节点,找到有确定的祖先节点后,通过层级依次向下定位。

代码实例: image.png

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="search" id="form" method="post">
  <span class="bg">
    <span class="soutu">搜索</span>
  </span>
  <span class="soutu">
    <input type="text" name="key" id="su">
  </span>
  <div></div>
</form>
</body>
</html>
# 1、根据层级向下找,从 form 找到绿色的span:
//form[@id='form']/span/span

# 2、查找某元素内部的所有元素,选取 form 元素内部的所有 span:
//form[@id='form']//span  # 第二个双斜杠,表示选取内部所有的 span,不管层级关系
  • 使用谓语定位:

谓语是 Xpath 中用于描述元素位置。主要有数字下标、最后一个子元素 last()、元素下标函数 position()。

注意点:Xpath 中的下标从1开始。

# 1、使用下标的方式,从 form 找到 input:
//form[@id='form']/span[2]/input

# 2、查找最后一个子元素,选取form下的最后一个span:
//form[@id='form']/span[last()]

# 3、查找倒数第几个子元素,选取 form 下的倒数第二个 span:
//form[@id='form']/span[last()-1]

# 4、使用 position() 函数,选取 form 下第二个 span:
//form[@id='form']/span[position()=2]

# 5、使用 position() 函数,选取下标大于2的span:
//form[@id='form']/span[position()>2]
  • 使用逻辑运算符定位:

如果元素的某个属性无法精确定位到这个元素,还可以使用逻辑运算符 and 连接多个属性进行定位,以百度输入框为例:

# 使用and,查找 name 属性为 wd 并且 class 属性为 s_ipt 的任意元素
//*[@name='wd' and @class='s_ipt']

# 使用or:查找 name 属性为 wd 或者 class 属性为 s_ipt 的任意元素,取其中之一满足即可。
//*[@name='wd' or @class='s_ipt']

# 使用|同时查找多个路径,取或:
//form[@id='form']//span | //form[@id='form']//input
  • 使用文本定位: 在爬取网站使用 Xpath 提取数据的时候,最常使用的是 Xpath 的text()方法,该方法可以提取当前元素的信息,但是某些元素下包含很多嵌套元素,也想一并的提取出来,则需使用 string(.)方法,但是该方法使用的时候跟text()不太一样,下面来通过实例看一下:
import requests  # 用于发包
from lxml import etree  # etree第三方模块,将html转成xml对象

url = 'https://www.biedoul.com/article/180839'

headers = {
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36",
}

response = requests.get(url, headers=headers)
html = etree.HTML(response.text)  # 将html转成xml对象

# //text,获取当前这个定位下的所有文本数据,但是看到的数据中带有转义符,就不是很完美
data = html.xpath('//div[@class="cc2"]//text()')  # 根据路径获取数据,返回的数据永远是个列表
print(data)

# 根据路径定位,通过下标取列表中的第一个值,再通过.xpath('string(.)')去掉无用又干扰的内容。
data1 = html.xpath('//div[@class="cc2"]')[0].xpath('string(.)')  #
print(data1)
  • 使用部分匹配函数:

image.png

ends-with:选取属性或者文本以某些字符结尾,比如://div[ends-with(@id, 'require')] 选取 id 属性以 require 结尾的 div 元素

验证 Xpath 定位语法:

方式一:

在开发工具的 Elements 中按 Ctrl + F,在搜索框中输入 Xpath

image.png

方式二:

使用 Xpath Helper 定位工具验证:(非万能工具)

  • chrome 插件:Xpath Helper
  • firefox 插件:Xpath checker

lxml 的基本使用:

from lxml import etree

webData = """
    <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="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>
             </ul>
         </div>
"""

# 将 html 转成 xml 文件
element = etree.HTML(webData)
print(element)

# 获取li标签下面的a标签的href
links = element.xpath('//ul/li/a/@href')
print(links)

# 获取li标签下面的a标签的文本数据
result = element.xpath('//ul/li/a/text()')
print(result)

小结:

数据解析之 Xpath 网络请求的步骤:

  1. 导入网络请求模块库(数据解析库)
  2. 发请求获取响应
  3. 将响应对象转成 xml 对象
  4. 通过 xml 对象的 xpath 语法定位数据(页面分析)
  5. 将获取的数据进一步处理即可

解题思路:

  1. 页面与思路分析:

    a. 确定数据接口(不考虑加密情况)【如果有分页,需考虑url的规律】

    b. 观察请求头信息,是否有加密(目前阶段都未加密)

    c. 确定编码格式(以防拿到的数据乱码)

    d. 确定目标数据(文档/题目会有提供)

    e. xpath 语法构建(有点难度,需手写)

    注意点:确定需要的数据是 element 中的数据还是网页源代码的数据 两者非常相似,但 element 中的标签属性可能在网页源代码中没有,所以element只能是辅助,实际上以网页源代码为准。

    f. 保存数据

2. 代码实现:

根据上述步骤,使用代码的形式一一实现即可。

代码实例1:

请求过于频繁时,容易被封掉(已被封,拿不到数据了-_-)

import requests
from lxml import etree

"""
目标:熟悉 xpath 解析方式
需求:爬取电影的名称、评分、引言、详情页的url、翻页爬取1-10页的数据

解题思路:
优先导入库 request、lxml(etree)

一、页面分析:
1、找到url的翻页规律--抓包
page1:https://movie.douban.com/top250?filter=
page2:https://movie.douban.com/top250?start=25&filter=
page3:https://movie.douban.com/top250?start=50&filter=

二、页面解析:
    1、获取整个网页源代码
    2、将网页源代码转成一个xml对象(为什么要转:方便后面进行xpath定位解析数据)
    3、通过xpath语法语法对我们的目标数据进行爬取
    4、将爬取数据保存只列表中

三、代码实现
"""


# 定义一个函数用于获取网页源代码
def getSource(pagelink):  # 参数pagelink接收url
    # 请求头
    headers = {
        "User-Agent": "xxx",
    }
    # 获取源代码
    response = requests.get(url=pagelink, headers=headers)  # 返回<Response [200]>,类型:<class 'requests.models.Response'>
    response.encoding = 'utf-8'
    html = response.text  # http响应内容的 字符串str 形式,请求 url 对应的页面内容并解码
    # print(html)  # 获取到前10页的html代码
    # html = response.content  # http响应内容的 二进制(bytes) 形式
    # print(html)
    return html  # 拿到数据后,return回到函数调用处,要将获取的数据,传过去进行解析


# 定义一个函数用于解析我们的目标数据
def everyItem(html):  # html接收后,然后开始解析

    # 将数据转成xml对象
    element = etree.HTML(html)

    # 解析我们的目标数据
    divData = element.xpath("//li//div[@class='info']")  # 返回的是个列表,通过循环处理25条数据中的每一条数据

    # 定义一个列表,用于将每一页的数据存放到列表中
    itemList = []

    # 通过循环处理25条数据中的每一条数据
    for item in divData:

        # 定义一个字典,用于保存每一条数据
        itemDict = {}

        # 获取主标题
        mainTitle = item.xpath("./div[@class='hd']/a/span[@class='title']/text()")
        doMainTitle = ''.join(mainTitle).replace('\xa0', '')   # .join():方便处理杂乱数据
        # print(doMainTitle)

        # 获取副标题,列表中没有replace方法,通过索引,把值取出来,并去掉杂乱数据,只有字符串才有replace、join等方法
        subheadingTitle = item.xpath("./div[@class='hd']/a/span[@class='other']/text()")[0].replace('\xa0', '')
        # print(subheadingTitle)

        title = ''.join(doMainTitle + subheadingTitle).replace(' ', '')
        # print(title)

        # 评分
        # score = item.xpath("./div[@class='bd']/div[@class='star']/span[2]//text()")[0]
        score = item.xpath("div[@class='bd']/div[@class='star']/span[2]/text()")[0]

        # 详情页的 url
        detailsPageUrl = item.xpath("./div[@class='hd']/a/@href")[0]

        # 引言,可能会有空数据,可以采用非空处理、异常处理
        introduction = item.xpath("./div[@class='bd']/p[@class='quote']/span/text()")
        # print(introduction)

        # 数据的非空处理
        if introduction:  # True
            introduction = introduction[0]  # 条件为真(如果有数据),取第一个值
        else:
            introduction = ''  # 条件为假(如果没有数据),打印为空

        #  将数据存放到字典中,形成键值对
        itemDict['title'] = title
        itemDict['score'] = score
        itemDict['detailsPageUrl'] = detailsPageUrl
        itemDict['introduction'] = introduction

        # 将字典中的数据,追加到列表中
        itemList.append(itemDict)
    # print(itemList)
    return itemList


# # 定义一个函数用于保存数据
def writeData(movieList):
    print(movieList)  # 还没学文件的存储,暂时打印
    pass


# 定义一个主函数,用于调用各个函数
if __name__ == '__main__':

    # 定义一个大的列表,用于存放10页的数据
    movieList = []

    for i in range(1, 11):
        url = f"https://movie.douban.com/top250?start={(i - 1) * 25}&filter="
        html = getSource(url)  # 调用方法,传入url参数
        itemList = everyItem(html)  # 传到方法everyItem()中,对数据进行解析,拿到一页的数据
        movieList += itemList  # 将每次拿到的每页的数据,添加到大盒子中
    # print(movieList)  # 打印到一个列表中,10页数据
    writeData(movieList)  # 简单的保存数据

代码实例2:

import requests
from lxml import etree

"""
链家网前五页数据
1、名称,2、位置,3、单价,4、总价
https://cs.lianjia.com/ershoufang/
"""


def getSource(pagelink):
    headers = {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36",
    }
    response = requests.get(url=pagelink, headers=headers)  # 获取网页前5页的 html 代码
    response.encoding = 'utf-8'

    html = response.text  # 获取解码的字符串文本数据
    # print(f'----这是第{i}页的数据:', html)
    return html


def itemDiv(html):
    # 对 html 数据进行解析,转成 xml 文件
    element = etree.HTML(html)

    # 根据 xpath 定位,获取每页中,右侧每个大盒子的数据
    dataList = element.xpath("//ul[@class='sellListContent']/li/div[@class='info clear']")  # 数据盒子的根目录
    itemList = []
    for item in dataList:
        itemDict = {}
        itemName = item.xpath("./div[@class='title']/a//text()")
        # print(f"名称:{itemName}")
        itemAddress = item.xpath("./div[@class='flood']/div[@class='positionInfo']/a/text()")
        # print(f"位置信息:{itemAddress}")
        itemUnitPrice = item.xpath("./div[@class='priceInfo']/div[@class='unitPrice']/span//text()")
        # print(f"单价为:{itemUnitPrice}")
        itemPrice = item.xpath("./div[@class='priceInfo']/div[@class='totalPrice totalPrice2']/span//text()")
        # print(f"总价为{itemPrice}万元")
        # print(f'房屋:{itemName[0].strip()},坐落于:{"".join(itemAddress)},单价是{itemUnitPrice[0]},总价为{itemPrice[0]}万元。')
        itemDict['itemName'] = itemName
        itemDict['itemAddress'] = itemAddress
        itemDict['itemUnitPrice'] = itemUnitPrice
        itemDict['itemPrice'] = itemPrice

        itemList.append(itemDict)
    return itemList


if __name__ == '__main__':
    hourseList = []
    for i in range(1, 6):
        url = f"https://cs.lianjia.com/ershoufang/pg{i}/"
        html = getSource(url)
        itemData = itemDiv(html)
        hourseList += itemData
    print(hourseList)