Xpath 是什么?
- xpath 是一种 xml 的路径查询语言,是一种非常强大的定位方式。能在 xml 树状结构中寻找节点。用于在 xml 文档中通过元素和属性进行导航。(html 是 xml 的一种实现方式。)
( HTML 的结构就是树形结构,HTML 是根节点,所有的其他元素节点都是从根节点发出的。其他的元素都是这棵树上的节点Node,每个节点还可能有属性和文本。而路径就是指某个节点到另一个节点的路线。)
节点之间的关系:
- 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 的绝对路径和相对路径代码:
但是拷贝出来的代码缺乏灵活性,也不一定全是对的,大部分情况下,都需要自己定义 xpath 语句。
相对路径的定位语法:
- 基本定位语法:
- 元素属性定位:
通过 @ 符号指定需要使用的属性。
- 层级属性结合定位: 通过某些元素无法精确定位时,可以查找其父级及其祖先节点,找到有确定的祖先节点后,通过层级依次向下定位。
代码实例:
<!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)
- 使用部分匹配函数:
ends-with:选取属性或者文本以某些字符结尾,比如://div[ends-with(@id, 'require')] 选取 id 属性以 require 结尾的 div 元素
验证 Xpath 定位语法:
方式一:
在开发工具的 Elements 中按 Ctrl + F,在搜索框中输入 Xpath
方式二:
使用 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 网络请求的步骤:
- 导入网络请求模块库(数据解析库)
- 发请求获取响应
- 将响应对象转成 xml 对象
- 通过 xml 对象的 xpath 语法定位数据(页面分析)
- 将获取的数据进一步处理即可
解题思路:
-
页面与思路分析:
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)