【Python爬虫(10)】解锁XPath:Python爬虫的精准导航仪(京东、淘宝实例)
【Python爬虫】专栏简介:本专栏是 Python 爬虫领域的集大成之作,共 100 章节。从 Python 基础语法、爬虫入门知识讲起,深入探讨反爬虫、多线程、分布式等进阶技术。以大量实例为支撑,覆盖网页、图片、音频等各类数据爬取,还涉及数据处理与分析。无论是新手小白还是进阶开发者,都能从中汲取知识,助力掌握爬虫核心技能,开拓技术视野。
目录
- 一、XPath 语法规则详解
- 1.1 节点选择
- 1.2 路径表达式
- 1.3 谓语(Predicates)
- 1.4 通配符与选取若干路径
- 二、在 Python 中使用 lxml 库结合 XPath 解析网页
- 2.1 lxml 库简介与安装
- 2.2 lxml 库的基本使用
- 2.3 XPath 与 lxml 结合实例
- 三、实例:利用 XPath 爬取电商网站商品详情
- 3.1 确定目标电商网站与商品
- 3.2 分析网页结构
- 3.3 编写爬虫代码
- 3.4 运行与优化
一、XPath 语法规则详解
XPath(XML Path Language)是一种用于在 XML 文档中定位和选择元素的查询语言,在爬虫领域,它常用于从 HTML 或 XML 页面中提取数据,通过特定的语法规则,能够精准地定位到页面中的各种元素,为数据提取提供了极大的便利。接下来,我们将详细介绍 XPath 的语法规则。
1.1 节点选择
在 XPath 中,节点是构成 XML 或 HTML 文档的基本单元,包括元素节点、属性节点、文本节点等。通过不同的方式,可以选择特定的节点。
- 通过元素名称选择:直接使用元素名称,可以选取该元素的所有子节点。例如,在一个 HTML 文档中,如果要选择所有的<div>元素,可以使用div,这将返回文档中所有的<div>元素节点。
- 通过路径选择:使用路径表达式来指定节点的位置。路径表达式由一系列的步骤组成,每个步骤之间用斜杠(/)分隔。例如,/html/body/div表示从根节点开始,依次选择<html>元素、<body>元素下的所有<div>元素,这种方式能够精确地定位到特定层级结构下的元素。
- 通过属性选择:利用@符号来选择具有特定属性的元素。例如,//div[@class=‘container’]表示选择所有具有class属性且属性值为container的<div>元素,这在根据元素的属性特征来筛选元素时非常有用。
- 通过位置选择:使用方括号[]并在其中指定位置索引来选择特定位置的元素。需要注意的是,XPath 中的索引从 1 开始。例如,/html/body/div[1]表示选择<html>元素下<body>元素的第一个<div>子元素;/html/body/div[last()]表示选择最后一个<div>子元素 ,通过这种方式可以灵活地获取指定位置的元素。
- 使用逻辑运算符:可以使用and、or等逻辑运算符来组合多个条件,实现更复杂的节点选择。例如,//div[@class=‘container’ and @id=‘main’]表示选择同时具有class属性值为container且id属性值为main的<div>元素;//div[@class=‘container’ or @class=‘sidebar’]表示选择class属性值为container或者sidebar的<div>元素,满足多种筛选需求。
- 通过文本内容选择:使用text()函数来选择包含特定文本内容的元素。例如,//div[text()=‘Hello, World!’]表示选择文本内容为Hello, World!的<div>元素;//div[contains(text(), ‘Hello’)]表示选择文本内容包含Hello的<div>元素,方便根据文本信息定位元素。
1.2 路径表达式
路径表达式是 XPath 中用于定位节点的关键语法,它描述了如何在文档的节点树中导航到目标节点。
- 绝对路径:以斜杠(/)开头,表示从根节点开始的完整路径。例如,/html/body/div/p表示从根节点<html>开始,依次经过<body>、<div>,最终选择<p>元素,这种路径明确指定了元素在文档中的层级位置,定位精确,但如果文档结构发生变化,可能会导致路径失效。
- 相对路径:不以斜杠(/)开头,表示相对于当前节点的路径。例如,div/p表示选择当前节点下的<div>元素中的<p>元素;…/表示选择当前节点的父节点,相对路径更加灵活,能够适应文档结构的一些变化。
- 路径表达式中的符号:
-
- 斜杠(/):用于分隔路径中的各个步骤,表示直接子节点关系。例如,/html/head/title表示选择<html>元素的直接子元素<head>,再选择<head>的直接子元素<title>。
-
- 双斜杠(//):表示在文档中选择任意位置的节点,无论其在文档层次结构中的深度如何。例如,//div表示选择文档中所有的<div>元素,而不考虑它们的嵌套层次,这在快速定位特定类型元素时非常方便。
-
- 点(.):表示当前节点。例如,./p表示选择当前节点下的<p>元素。
-
- 双点(…):表示当前节点的父节点。例如,如果当前节点是<p>元素,…/则表示选择其父节点。
-
- @:用于选择属性。例如,//div[@id]表示选择所有具有id属性的<div>元素;//@class表示选择所有的class属性节点。
1.3 谓语(Predicates)
谓语是 XPath 中用于进一步筛选节点的重要机制,它被嵌入在方括号[]中,通过指定条件来筛选出符合要求的节点。
- 选取特定位置的节点:如前面提到的,使用索引来选取特定位置的节点。例如,/bookstore/book[2]表示选择<bookstore>元素下的第二个<book>元素;/bookstore/book[last()-1]表示选择<bookstore>元素下倒数第二个<book>元素,通过这种方式可以精确获取特定顺序的元素。
- 选取具有特定属性值的节点:通过在方括号中指定属性和属性值来筛选节点。例如,//book[@category=‘fiction’]表示选择所有category属性值为fiction的<book>元素;//img[@src=‘image.jpg’]表示选择src属性值为image.jpg的<img>元素,根据属性特征筛选目标元素。
- 选取具有特定文本内容的节点:结合text()函数,使用谓语来选取具有特定文本内容的节点。例如,//li[text()=‘Item 1’]表示选择文本内容为Item 1的<li>元素;//p[contains(text(), ‘重要内容’)]表示选择文本内容包含重要内容的<p>元素,方便根据文本信息筛选节点。
1.4 通配符与选取若干路径
在 XPath 中,通配符和选取若干路径的语法为数据提取提供了更高的灵活性。
-
通配符:
-
- 星号(*):匹配任何元素节点。例如,/bookstore/*表示选择元素的所有子元素,无论这些子元素是什么类型。
-
- @:匹配任何属性节点。例如,//div[@]表示选择所有带有属性的<div>元素,而不关心具体的属性名称。
-
- node():匹配任何类型的节点,包括元素节点、文本节点、属性节点等。例如,//node()表示选择文档中的所有节点。
-
选取若干路径:使用 “|” 运算符可以选取若干个路径。例如,//book/title | //book/price表示选取<book>元素的所有<title>和<price>元素;//title | //description表示选取文档中的所有<title>和<description>元素,通过这种方式可以一次性获取多个不同路径下的元素。
二、在 Python 中使用 lxml 库结合 XPath 解析网页
了解了 XPath 的语法规则后,接下来我们看看如何在 Python 中使用 lxml 库结合 XPath 来解析网页。lxml 库是 Python 中一个功能强大的解析库,它提供了高效的 XML 和 HTML 解析功能,并且对 XPath 有很好的支持,使得我们能够方便地从网页中提取所需的数据。
2.1 lxml 库简介与安装
lxml 是一个基于 C 语言的 libxml2 和 libxslt 库的 Python 库,它具有高性能、功能丰富的特点。在处理 XML 和 HTML 文档时,lxml 库提供了简单而直观的 API,能够快速地解析和提取数据。
安装 lxml 库非常简单,如果你已经安装了 pip(Python 的包管理工具),可以在命令行中使用以下命令进行安装:
pip install lxml
安装过程中,pip 会自动下载并安装 lxml 库及其依赖项。安装完成后,你就可以在 Python 项目中使用 lxml 库了。
2.2 lxml 库的基本使用
在使用 lxml 库之前,需要先导入相关的模块。通常,我们使用以下方式导入 lxml.etree 模块:
from lxml import etree
- 解析本地文件:如果要解析本地的 HTML 或 XML 文件,可以使用etree.parse()方法。例如,假设我们有一个名为example.html的本地 HTML 文件,代码如下:
# 解析本地HTML文件
tree = etree.parse('example.html')
# 获取根节点
root = tree.getroot()
在上述代码中,etree.parse(‘example.html’)会读取并解析example.html文件,返回一个ElementTree对象,我们可以通过getroot()方法获取该对象的根节点。
- 解析网页文件:当我们需要解析从网络获取的 HTML 内容时,可以使用etree.HTML()方法。例如,通过requests库获取网页内容后,使用etree.HTML()进行解析:
import requests
from lxml import etree
# 发送HTTP请求获取网页内容
url = 'https://example.com'
response = requests.get(url)
html_content = response.text
# 解析网页内容
html_tree = etree.HTML(html_content)
在这个例子中,首先使用requests.get(url)获取指定 URL 的网页内容,然后将返回的文本内容传递给etree.HTML()方法,该方法会将 HTML 内容解析为一个Element对象,后续就可以使用 XPath 表达式对其进行操作。
2.3 XPath 与 lxml 结合实例
下面通过一个具体的例子来展示如何使用 XPath 表达式在 lxml 解析的网页中提取数据。假设我们要从一个电商网站的商品列表页面中提取商品的标题、价格和链接信息。
以京东商城的手机商品列表页面(https://search.jd.com/Search?keyword=手机)为例,代码如下:
import requests
from lxml import etree
# 发送HTTP请求获取网页内容
url = 'https://search.jd.com/Search?keyword=手机'
headers = {
'User - Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
response = requests.get(url, headers=headers)
html_content = response.text
# 解析网页内容
html_tree = etree.HTML(html_content)
# 使用XPath表达式提取商品信息
# 提取商品标题
titles = html_tree.xpath('//div[@class="gl-i-wrap"]//div[@class="p-name p-name-type-2"]//em/text()')
# 提取商品价格
prices = html_tree.xpath('//div[@class="gl-i-wrap"]//div[@class="p-price"]//i/text()')
# 提取商品链接
links = html_tree.xpath('//div[@class="gl-i-wrap"]//div[@class="p-name p-name-type-2"]//a/@href')
# 打印提取到的信息
for title, price, link in zip(titles, prices, links):
print(f'商品标题: {title.strip()}')
print(f'商品价格: {price}')
print(f'商品链接: https:{link}')
print('-' * 50)
在上述代码中:
- 首先使用requests.get()方法发送 HTTP 请求获取网页内容,并设置了User - Agent头信息来模拟浏览器访问,以避免被网站识别为爬虫而拒绝访问。
- 然后使用etree.HTML()方法将获取到的网页内容解析为Element对象。
- 接着使用 XPath 表达式来提取商品的标题、价格和链接信息。例如,//div[@class=“gl-i-wrap”]//div[@class=“p-name p-name-type-2”]//em/text()表示选择所有具有gl-i-wrap类的div元素下,具有p-name p-name-type-2类的div元素下的em元素的文本内容,即商品标题;//div[@class=“gl-i-wrap”]//div[@class=“p-price”]//i/text()用于提取商品价格;//div[@class=“gl-i-wrap”]//div[@class=“p-name p-name-type-2”]//a/@href用于提取商品链接。
- 最后,通过zip()函数将提取到的标题、价格和链接信息一一对应,并打印输出。
三、实例:利用 XPath 爬取电商网站商品详情
3.1 确定目标电商网站与商品
我们以淘宝为例,确定要爬取的商品为 “运动鞋”。淘宝作为国内知名的电商平台,商品种类丰富,页面结构较为典型,非常适合作为爬虫学习的实践对象。通过爬取淘宝上的运动鞋商品信息,我们可以深入了解如何在复杂的电商页面中运用 XPath 提取数据。
3.2 分析网页结构
在开始编写爬虫代码之前,需要仔细分析淘宝搜索 “运动鞋” 后的结果页面的 HTML 结构。打开浏览器,访问淘宝搜索页面(https://s.taobao.com/search?q=运动鞋),使用浏览器的开发者工具(通常可以通过右键点击页面,选择 “检查” 或 “审查元素” 打开)来查看页面的 HTML 代码。
- 商品列表区域:在开发者工具中,可以发现商品信息都包含在特定的
元素中,例如具有class属性为"item J_MouserOnverReq "的<div>元素,这个div元素可以看作是每个商品的容器,里面包含了商品的各种详细信息。
- 商品名称:商品名称通常在<div>元素内部的<a>标签下的<img>标签的alt属性中,例如//div[@class="item J_MouserOnverReq "]/div/div/div/a/img/@alt,通过这个 XPath 表达式可以定位到商品名称。
- 商品价格:价格信息在<div>元素内部的<div>标签下,且该<div>标签具有class属性为"price g_price g_price-highlight",具体的价格数值在<strong>标签内,对应的 XPath 表达式可以是//div[@class="item J_MouserOnverReq "]/div[2]/div/div/strong/text()。
- 商品销量:销量信息在<div>元素内部的<div>标签下,该<div>标签具有class属性为"deal-cnt",使用 XPath 表达式//div[@class="item J_MouserOnverReq "]/div[2]/div/div[@class=“deal-cnt”]/text()可以提取到销量数据。
- 店铺名称:店铺名称在<div>元素内部的<div>标签下,该<div>标签具有class属性为"shop",进一步在<a>标签下的<span>标签中可以找到店铺名称,对应的 XPath 表达式为//div[@class="item J_MouserOnverReq "]/div[2]/div[3]/div/a/span[2]/text()。
- 商品链接:商品链接在<div>元素内部的<div>标签下,该<div>标签具有class属性为"pic",在<a>标签的href属性中存储着商品链接,XPath 表达式为//div[@class="item J_MouserOnverReq "]/div/div/div/a/@href。通过分析这些元素的位置和属性,我们能够准确地构建 XPath 表达式来提取所需的商品详情信息。
3.3 编写爬虫代码
import requests
from lxml import etree
import csv
def crawl_taobao_shoes():
# 发送请求获取网页内容
url = 'https://s.taobao.com/search?q=运动鞋'
headers = {
'User - Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
try:
response = requests.get(url, headers=headers)
response.raise_for_status() # 检查请求是否成功
except requests.exceptions.RequestException as e:
print(f"请求发生异常: {e}")
return
# 使用lxml库结合XPath提取商品信息
html_tree = etree.HTML(response.text)
items = html_tree.xpath('//div[@class="item J_MouserOnverReq "]')
data = []
for item in items:
# 提取商品名称
title = item.xpath('div/div/div/a/img/@alt')[0].strip() if item.xpath('div/div/div/a/img/@alt') else ''
# 提取商品价格
price = item.xpath('div[2]/div/div/strong/text()')[0].strip() if item.xpath('div[2]/div/div/strong/text()') else ''
# 提取商品销量
sales = item.xpath('div[2]/div/div[@class="deal-cnt"]/text()')[0].strip() if item.xpath(
'div[2]/div/div[@class="deal-cnt"]/text()') else ''
# 提取店铺名称
shop_name = item.xpath('div[2]/div[3]/div/a/span[2]/text()')[0].strip() if item.xpath(
'div[2]/div[3]/div/a/span[2]/text()') else ''
# 提取商品链接
link = 'https:' + item.xpath('div/div/div/a/@href')[0].strip() if item.xpath('div/div/div/a/@href') else ''
data.append([title, price, sales, shop_name, link])
# 保存提取数据到CSV文件
with open('taobao_shoes.csv', 'w', newline='', encoding='utf - 8') as csvfile:
writer = csv.writer(csvfile)
writer.writerow(['商品名称', '商品价格', '商品销量', '店铺名称', '商品链接'])
for row in data:
writer.writerow(row)
if __name__ == "__main__":
crawl_taobao_shoes()
在上述代码中:
- 首先,定义了要爬取的 URL 和请求头headers,通过设置User - Agent头信息,模拟浏览器发送请求,以避免被淘宝的反爬虫机制检测到。
- 使用requests.get()方法发送 HTTP GET 请求,获取网页内容。如果请求过程中发生异常,例如网络问题、请求被拒绝等,会捕获requests.exceptions.RequestException异常,并打印异常信息。
- 利用etree.HTML()方法将获取到的网页内容解析为Element对象,以便后续使用 XPath 表达式进行数据提取。
- 通过 XPath 表达式从解析后的 HTML 树中提取商品名称、价格、销量、店铺名称和商品链接等信息。在提取过程中,使用了条件判断来处理可能出现的提取失败情况,例如当某个商品的某个信息不存在时,将其设置为空字符串。
- 将提取到的数据存储在一个列表data中,然后使用 Python 的csv模块将数据写入到名为taobao_shoes.csv的 CSV 文件中。在写入文件时,首先写入表头,然后逐行写入数据。
3.4 运行与优化
运行上述爬虫代码,可能会遇到一些问题,需要进行相应的处理和优化:
- 异常处理:
-
- 网络请求失败:尽管在代码中已经使用了try - except块来捕获requests.exceptions.RequestException异常,但还可以进一步细化异常处理。例如,可以根据不同的异常类型进行不同的处理,对于网络超时异常requests.exceptions.Timeout,可以设置重试机制,增加请求成功的概率;对于连接错误异常requests.exceptions.ConnectionError,可以提示用户检查网络连接。
-
- 数据提取失败:在提取数据时,可能会因为网页结构的变化或者其他原因导致数据提取失败。可以在代码中增加更多的容错处理,例如在提取数据前,先检查对应的 XPath 表达式是否能找到节点,如果找不到,可以记录日志并跳过该商品的处理,或者尝试使用其他的 XPath 表达式进行提取。
- 优化爬虫:
控制请求频率:淘宝等电商网站通常会有反爬虫机制,为了避免被封禁 IP,需要控制请求频率。可以在每次请求之间添加适当的时间间隔,例如使用time.sleep()函数,设置一个合理的睡眠时间,模拟人类正常的浏览行为。例如,在发送请求获取网页内容的代码部分添加如下代码:
import time
# 发送请求获取网页内容
url = 'https://s.taobao.com/search?q=运动鞋'
headers = {
'User - Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
for _ in range(3): # 尝试3次
try:
response = requests.get(url, headers=headers)
response.raise_for_status() # 检查请求是否成功
break
except requests.exceptions.Timeout:
print("请求超时,重试中...")
time.sleep(5) # 等待5秒后重试
except requests.exceptions.RequestException as e:
print(f"请求发生异常: {e}")
break
else:
print("多次请求均失败,终止程序")
exit()
在上述代码中,使用了一个for循环来尝试发送请求 3 次,每次请求失败如果是超时异常,就等待 5 秒后重试,如果 3 次都失败,则打印提示信息并终止程序。
- 提高代码效率:如果需要爬取多页数据,可以使用多线程或异步编程来提高爬取效率。例如,使用asyncio库结合aiohttp库进行异步请求,减少请求的等待时间。同时,可以对提取到的数据进行预处理,例如去除数据中的空格、特殊字符等,提高数据的质量,方便后续的数据分析。