跳到主要内容

Scrapy 默认的网页解析器 Xpath

Xpath 是 Scrapy 框架中默认的网页解析器,只有掌握了 Xpath 选择器,我们才能快速从网页元素中提取我们想要的数据。

1. xpath 选择器介绍

首先来看看 Xpath 的字面介绍:

XPath 即为 XML 路径语言(XML Path Language),它是一种用来确定XML文档中某部分位置的语言。 XPath 基于 XML 的树状结构,提供在数据结构树中找寻节点的能力。XQuery 和 XPointer 均构建于 XPath 表达式之上。

来看看 xpath 最常用的路径表达式规则:

表达式描述
nodename选择此元素的所有子节点
/从根节点开始选择
//从匹配选择的当前节点选择文档中的节点
.当前节点
当前节点的父节点
@选取属性

来看下面几个例子:

路径表达式含义
p选择所有 p 节点
//body选择所有的body元素节点/
//*[@class=“red-color”]/…选择所有class属性值为 “red-color” 节点的父节点

在 xpath 中可以使用通配符来提取相关节点元素:

路径表达式含义
//*找出所有节点
//*[@*]匹配任何有属性的节点
//*[@class=“red-color”]提取所有class属性值为 “red-color” 的节点

另外,在 xpath 中我们还可以使用运算符,来辅助选取节点:

路径表达式含义
//div//p
//p[1 + 1]/text()获取第二个p元素节点的文本值
//*[@value > 10]找出所有 value 值大于10的节点

其中 xpath 支持的表达式除了 +- *divmod 等基本运算符外,还有比较运算符,如 =!=>=<=>>andor等。

在 xpath 中有一个叫做轴的概念,表示相对于当前节点的节点集。下面是一些基本轴的定义:

轴名称含义
ancestor选取当前节点的所有先辈(父、祖父等)
ancestor-or-self选取当前节点的所有先辈(父、祖父等)以及当前节点本身
attribute选取当前节点的所有属性
child选取当前节点的所有子元素
descendant选取当前节点的所有后代元素(子、孙等)
descendant-or-self选取当前节点的所有后代元素(子、孙等)以及当前节点本身
following选取文档中当前节点的结束标签之后的所有节点
following-sibling选取文档中当前节点的结束标签之后的所有同级节点
parent选取当前节点的父节点
preceding选取文档中当前节点的开始标签之前的所有节点
preceding-sibling选取当前节点之前的所有同级节点
self选取当前节点

轴的用法是:轴名称::节点测试。来看下面几个例子:

路径表达式含义
//body/div[2]/following-sibling::*body节点下第二个div节点之后的所有同级节点
//body/p[1]/child::span[last()]/text()body节点下的第一个p节点下的最后一个span子节点的文本值
//body/p[1]/span/child::text()body节点下的第一个p节点下的所有span子节点的文本值
//body/p/attribute::*body节点下所有p节点的属性值

最后,在 xpath 中还有一些辅助我们更好搜索节点的函数:

函数含义
starts-with()获取某个字符串开头的节点
contains()包含某个字符串的节点,可以是属性包含、文本包含等等
text()获取节点的文本值

上述辅助函数的实例如下:

路径表达式含义
//p[contains(@class, “red”)]获取class属性值包含"red"的所有p节点值
‘//span[contains(text(), “蓝色”)]/text()’获取文本值包含"蓝色"的所有span节点的文本
‘//span[starts-with(text(), “蓝”)]/text()’获取文本值以"蓝"开头的所有span节点的文本

此外,当然还有许多方面没有讲到,后续会在实战中进行说明。上面的基础部分一定要熟记和灵活运用,足以应付常见的页面数据提取。下面就进入实战环节,使用 Python 来实操 xpath 路径表达式。

2. xpath 解析实战

lxml 是 Python 中的一个解析库,支持 HTML 和 XML 的解析,支持 XPath 解析方式,而且解析效率非常高。本节将安装该模块解析 html 文本并提取相应的数据。

[store@server2 ~]$ sudo pip3 install lxml
WARNING: Running pip install with root privileges is generally not a good idea. Try `pip3 install --user` instead.
Collecting lxml
Downloading http://mirrors.cloud.aliyuncs.com/pypi/packages/55/6f/c87dffdd88a54dd26a3a9fef1d14b6384a9933c455c54ce3ca7d64a84c88/lxml-4.5.1-cp36-cp36m-manylinux1_x86_64.whl (5.5MB)
100% |████████████████████████████████| 5.5MB 82.9MB/s
Installing collected packages: lxml
Successfully installed lxml-4.5.1

我们先准备好素材,也就是要解析的 HTML 文档。为了更有代入感,我直接使用慕课网 wiki 页面的数据进行操作,获取数据的方式如下图所示:

图片描述

获取慕课网 wiki 页面的 HTML 数据 最后保存到一个 test.html 文本,然后我们要准备一段 Python 代码:

from lxml import etree

tree = etree.parse('test.html', etree.HTMLParser(encoding='utf8'))

def print\_result(exp, results):
print('xpath表达式为:{},其匹配结果为:'.format(exp))
for res in results:
print(res.strip())
print('')

def test\_xpath\_expression(exp):
results = tree.xpath(exp)
print_result(exp, results)

将这个 Python 文件命名为 test_xpath.py 和 test.html 放在同一级目录下:

[store@server2 ~]$ ls
shen test.html test_xpath.py

接下来我们就可以进行激动人心的测试了,来完成一个简单的实验:

图片描述

慕课网 wiki 页面数据获取 第一个实验的目标就是拿到 javascript 分类下的教程的三个数据:标题、总节数以及访问次数。通过 F12 查看相关的 HTML 结构,我们可以通过如下的 Xpath表达式获取相应的数据:

Python 3.6.8 (default, Apr  2 2020, 13:34:55) 
[GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from test_xpath import test_xpath_expression
>>> exp1 = '//h2[@class="language-title"]/text()'
>>> test_xpath_expression(exp1)
xpath表达式为://h2[@class="language-title"]/text(),其匹配结果为:
JavaScript
HTML & CSS
服务器
开发工具
其他后端语言
基础应用
框架应用
基础应用
Python Web 开发
MySQL

接下来看一看元素的结构:

图片描述

javascript 专栏的节点结构 可以看到 javascript 专栏标题是 h2 节点,这个节点同级下有一个 div,它下面的四个 div 节点正是那四个专栏。我们首先匹配下这四个专栏元素:

>>> exp1 = '//h2[contains(text(), "JavaScript")]/following-sibling::div/div[@class="course-card"]'
>>> test_xpath_expression(exp1)
xpath表达式为://h2[contains(text(), "JavaScript")]/following-sibling::div/div[@class="course-card"],其匹配结果为:
<Element div at 0x7f7015bf8808>
<Element div at 0x7f700c656788>
<Element div at 0x7f700c6567c8>
<Element div at 0x7f700c656808>

那么我们来进一步分析每个 div 内部如何得到教程标题、总节数以及访问次数这些数据:

图片描述

获取教程数据 可以看到,在前面找到 div 节点的基础上在往下两层,找到 class 属性值为 text 的 div 节点,所有的数据都在这个节点中:

  • 标题:上面找到的 div 节点下的第一个 a 节点的文本值;
  • 教程总节数:上面找到的 div 节点下的第一个 p 节点下第一个 span 元素的文本值;
  • 总访问次数:上面找到的 div 节点下的第一个 p 节点下第二个 span 元素的文本值;

这样我们就能进行写出提取相应数据的 Xpath 路径表达式了,测试如下:

>>> exp1 = '//h2[contains(text(), "JavaScript")]/following-sibling::div/div[@class="course-card"]/child::div/div[@class="text"]/a[1]/text()'
>>> test_xpath_expression(exp1)
xpath表达式为://h2[contains(text(), "JavaScript")]/following-sibling::div/div[@class="course-card"]/child::div/div[@class="text"]/a[1]/text(),其匹配结果为:
Javascript 入门教程
TypeScript 入门教程
Vue 入门教程
Ajax 入门教程

>>> exp2 = '//h2[contains(text(), "JavaScript")]/following-sibling::div/div[@class="course-card"]/child::div/div[@class="text"]/p/span[1]/text()'
>>> test_xpath_expression(exp2)
xpath表达式为://h2[contains(text(), "JavaScript")]/following-sibling::div/div[@class="course-card"]/child::div/div[@class="text"]/p/span[1]/text(),其匹配结果为:
56小节
38小节
39小节
9小节

>>> exp3 = '//h2[contains(text(), "JavaScript")]/following-sibling::div/div[@class="course-card"]/child::div/div[@class="text"]/p/span[2]/text()'
>>> test_xpath_expression(exp3)
xpath表达式为://h2[contains(text(), "JavaScript")]/following-sibling::div/div[@class="course-card"]/child::div/div[@class="text"]/p/span[2]/text(),其匹配结果为:
9832
3547
3628
1800


接下来我们整理下 Python 代码,将整个 wiki 页面上的教程都解析出来,并将数据整理成 json 格式。预期最后的结果应该是这样的:

{
'前端开发': {
'JavaScript': [
{'title': 'JavaScript入门教程', 'total\_chapters': 56, 'total\_visited': 9001},
{...},
{...},
{...}
],
'HTML & CSS': [ ... ]
}
'服务端相关': {

},
...
}

这样的难度再次增加,其核心的获取数据的过程和上面一致。后面获取其他数据的结果过程不作分析,大家有兴趣仔细研究下代码,然后动手实操。话不多说,上代码:

# 代码文件:test\_xpath2.py

from lxml import etree
def get\_direction\_data(direction_tree):
"""
获取一个方向下的课程数据
:return:
"""
direction_data = {}
cards = direction_tree.xpath('.//div[@class="language-card"]')
for card in cards:
title = card.xpath('.//h2[@class="language-title"]/text()')[0]
course_list = card.xpath('.//div[@class="course-card"]')
courses = []
for course in course_list:
course_title = course.xpath('.//div[@class="text"]/a[1]/text()')[0]
course_total_chaps = course.xpath('.//div[@class="text"]/p/span[1]/text()')[0]
course_total_visit_count = course.xpath('.//div[@class="text"]/p/span[2]/text()')[0]
courses.append({
'course\_title': course_title.strip(),
'course\_total\_chaps': course_total_chaps.strip(),
'course\_total\_visit\_count': int(course_total_visit_count.strip())
})
direction_data[title] = courses
return direction_data


def get\_all\_data():
"""
解析慕课网wiki数据
:return:
"""
result = {}
html = etree.parse('test.html', etree.HTMLParser(encoding='utf8'))
directions = html.xpath('//div[@class="direction-con"]')
for direction in directions:
# 提取方向key,注意一定要有点号,表示从当前元素开始提取
direction_name = direction.xpath('./div[@class="title-con"][1]/text()')
if direction_name:
result[direction_name[0]] = get_direction_data(direction)
return result

运行的结果如下:

[store@server2 ~]$ python3
Python 3.6.8 (default, Apr 2 2020, 13:34:55)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from test_xpath2 import get_all_dat
>>> get_all_data()
{'前端开发': {'JavaScript': [{'course\_title': 'Javascript 入门教程', 'course\_total\_chaps': '56小节', 'course\_total\_visit\_count': 9832}, {'course\_title': 'TypeScript 入门教程', 'course\_total\_chaps': '38小节', 'course\_total\_visit\_count': 3547}, {'course\_title': 'Vue 入门教程', 'course\_total\_chaps': '39小节', 'course\_total\_visit\_count': 3628}, {'course\_title': 'Ajax 入门教程', 'course\_total\_chaps': '9小节', 'course\_total\_visit\_count': 1800}], 'HTML & CSS': [{'course\_title': 'CSS3 入门教程', 'course\_total\_chaps': '32小节', 'course\_total\_visit\_count': 1512}, {'course\_title': 'Less 入门教程', 'course\_total\_chaps': '22小节', 'course\_total\_visit\_count': 364}, {'course\_title': '雪碧图入门教程', 'course\_total\_chaps': '24小节', 'course\_total\_visit\_count': 915}]}, '服务端相关': {'服务器': [{'course\_title': 'Nginx 入门教程', 'course\_total\_chaps': '24小节', 'course\_total\_visit\_count': 4500}, {'course\_title': 'HTTP 入门教程', 'course\_total\_chaps': '16小节', 'course\_total\_visit\_count': 456}, {'course\_title': 'Docker 入门教程', 'course\_total\_chaps': '25小节', 'course\_total\_visit\_count': 1067}, {'course\_title': 'Shell 入门教程', 'course\_total\_chaps': '17小节', 'course\_total\_visit\_count': 2060}, {'course\_title': 'Linux 入门教程', 'course\_total\_chaps': '25小节', 'course\_total\_visit\_count': 1430}], '开发工具': [{'course\_title': 'Gradle 入门教程', 'course\_total\_chaps': '12小节', 'course\_total\_visit\_count': 1121}, {'course\_title': 'Vim 入门教程', 'course\_total\_chaps': '14小节', 'course\_total\_visit\_count': 1491}, {'course\_title': 'RESTful 规范教程', 'course\_total\_chaps': '13小节', 'course\_total\_visit\_count': 1316}, {'course\_title': 'Markdown 入门教程', 'course\_total\_chaps': '31小节', 'course\_total\_visit\_count': 733}, {'course\_title': 'Maven 入门教程', 'course\_total\_chaps': '17小节', 'course\_total\_visit\_count': 155}, {'course\_title': 'GitHub 入门教程', 'course\_total\_chaps': '9小节', 'course\_total\_visit\_count': 261}], '其他后端语言': [{'course\_title': 'C 语言入门教程', 'course\_total\_chaps': '45小节', 'course\_total\_visit\_count': 1933}, {'course\_title': 'Go 入门教程', 'course\_total\_chaps': '36小节', 'course\_total\_visit\_count': 691}, {'course\_title': 'Ruby 入门教程', 'course\_total\_chaps': '26小节', 'course\_total\_visit\_count': 410}]}, 'Java': {'基础应用': [{'course\_title': 'Java 入门教程', 'course\_total\_chaps': '39小节', 'course\_total\_visit\_count': 5229}, {'course\_title': 'Android 入门教程', 'course\_total\_chaps': '29小节', 'course\_total\_visit\_count': 553}, {'course\_title': '算法入门教程', 'course\_total\_chaps': '11小节', 'course\_total\_visit\_count': 628}], '框架应用': [{'course\_title': 'Spring Boot 入门教程', 'course\_total\_chaps': '25小节', 'course\_total\_visit\_count': 4861}, {'course\_title': 'Spring 入门教程', 'course\_total\_chaps': '21小节', 'course\_total\_visit\_count': 850}, {'course\_title': 'Hibernate 入门教程', 'course\_total\_chaps': '23小节', 'course\_total\_visit\_count': 619}, {'course\_title': 'MyBatis 入门教程', 'course\_total\_chaps': '23小节', 'course\_total\_visit\_count': 895}]}, 'Python': {'基础应用': [{'course\_title': 'Python 入门语法教程', 'course\_total\_chaps': '24小节', 'course\_total\_visit\_count': 3617}, {'course\_title': 'Python 原生爬虫教程', 'course\_total\_chaps': '19小节', 'course\_total\_visit\_count': 2001}, {'course\_title': 'Python 进阶应用教程', 'course\_total\_chaps': '29小节', 'course\_total\_visit\_count': 726}], 'Python Web 开发': [{'course\_title': 'Django 入门教程', 'course\_total\_chaps': '33小节', 'course\_total\_visit\_count': 668}, {'course\_title': 'NumPy 入门教程', 'course\_total\_chaps': '21小节', 'course\_total\_visit\_count': 152}]}, '数据库': {'MySQL': [{'course\_title': 'MySQL 入门教程', 'course\_total\_chaps': '32小节', 'course\_total\_visit\_count': 3638}, {'course\_title': 'SQL 入门教程', 'course\_total\_chaps': '47小节', 'course\_total\_visit\_count': 2406}]}}

是不是实现了预期效果?爬取网页,解析数据的过程和这个类似。掌握好今天的内容,你就已经掌握了爬虫的一个核心步骤。

3. 小结

本小节中,我们重点介绍了 Xpath 选择器的一些基本知识,包括通用的路径表达式规则、运输符、轴的概念以及 Xpath 选择器中常用的辅助函数。接下来我们用一段 Html 文本结合 Python 代码进行了实战演示,帮助我们更好的理解 xpath 选择器,本节课程就到这里,希望大家有所收获。

图片描述