0%

【2022 年】Python3 爬虫教程 - parsel 的使用

前文我们了解了 lxml 使用 XPath 和 pyquery 使用 CSS Selector 来提取页面内容的方法,不论是 XPath 还是 CSS Selector,对于绝大多数的内容提取都足够了,大家可以选择适合自己的库来做内容提取。

不过这时候有人可能会问:我能不能二者穿插使用呀?有时候做内容提取的时候觉得 XPath 写起来比较方便,有时候觉得 CSS Selector 写起来比较方便,能不能二者结合起来使用呢?答案是可以的。

这里我们就介绍另一个解析库,叫做 parsel。

注意:如果你用过 Scrapy 框架(后文会介绍)的话,你会发现 parsel 的 API 和 Scrapy 选择器的 API 极其相似,这是因为 Scrapy 的选择器就是基于 parsel 做了二次封装,因此学会了这个库的用法,后文 Scrapy 选择器的用法就融会贯通了。

1. 介绍

parsel 这个库可以对 HTML 和 XML 进行解析,并支持使用 XPath 和 CSS Selector 对内容进行提取和修改,同时它还融合了正则表达式提取的功能。功能灵活而又强大,同时它也是 Python 最流行爬虫框架 Scrapy 的底层支持。

2. 准备工作

在本节开始之前,请确保已经安装好了 parsel 库,如尚未安装,可以使用 pip3 进行安装即可:

1
pip3 install parsel

更详细的安装说明可以参考:https://setup.scrape.center/parsel。

安装好之后,我们便可以开始本节的学习了。

3. 初始化

首先我们还是用上一节的示例 HTML,声明 html 变量如下:

1
2
3
4
5
6
7
8
9
10
11
html = '''
<div>
<ul>
<li class="item-0">first item</li>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>
<li class="item-1 active"><a href="link4.html">fourth item</a></li>
<li class="item-0"><a href="link5.html">fifth item</a></li>
</ul>
</div>
'''

接着,一般我们会用 parsel 的 Selector 这个类来声明一个 Selector 对象,写法如下:

1
2
from parsel import Selector
selector = Selector(text=html)

这里我们创建了一个 Selector 对象,传入了 text 参数,内容就是刚才声明的 HTML 字符串,赋值为 selector 变量。

有了 Selector 对象之后,我们可以使用 css 和 xpath 方法分别传入 CSS Selector 和 XPath 进行内容的提取,比如这里我们提取 class 包含 item-0 的节点,写法如下:

1
2
3
4
items = selector.css('.item-0')
print(len(items), type(items), items)
items2 = selector.xpath('//li[contains(@class, "item-0")]')
print(len(items2), type(items), items2)

我们先用 css 方法进行了节点提取,输出了提取结果的长度和内容,xpath 方法也是一样的写法,运行结果如下:

1
2
3 <class 'parsel.selector.SelectorList'> [<Selector xpath="descendant-or-self::*[@class and contains(concat(' ', normalize-space(@class), ' '), ' item-0 ')]" data='<li class="item-0">first item</li>'>, <Selector xpath="descendant-or-self::*[@class and contains(concat(' ', normalize-space(@class), ' '), ' item-0 ')]" data='<li class="item-0 active"><a href="li...'>, <Selector xpath="descendant-or-self::*[@class and contains(concat(' ', normalize-space(@class), ' '), ' item-0 ')]" data='<li class="item-0"><a href="link5.htm...'>]
3 <class 'parsel.selector.SelectorList'> [<Selector xpath='//li[contains(@class, "item-0")]' data='<li class="item-0">first item</li>'>, <Selector xpath='//li[contains(@class, "item-0")]' data='<li class="item-0 active"><a href="li...'>, <Selector xpath='//li[contains(@class, "item-0")]' data='<li class="item-0"><a href="link5.htm...'>]

可以看到两个结果都是 SelectorList 对象,它其实是一个可迭代对象。另外可以用 len 方法获取它的长度,都是 3,提取结果代表的节点其实也是一样的,都是第 1、3、5 个 li 节点,每个节点还是以 Selector 对象的形式返回了,其中每个 Selector 对象的 data 属性里面包含了提取节点的 HTML 代码。

不过这里可能大家有个疑问,第一次我们不是用 css 方法来提取的节点吗?为什么结果中的 Selector 对象还输出了 xpath 属性而不是 css 属性呢?这是因为 css 方法背后,我们传入的 CSS Selector 首先被转成了 XPath,XPath 才真正被用作节点提取。其中 CSS Selector 转换为 XPath 这个过程是在底层用 cssselect 这个库实现的,比如 .item-0 这个 CSS Selector 转换为 XPath 的结果就是 descendant-or-self::*[@class and contains(concat(' ', normalize-space(@class), ' '), ' item-0 ')],因此输出的 Selector 对象有了 xpath 属性了。不过这个大家不用担心,这个对提取结果是没有影响的,仅仅是换了一个表示方法而已。

4. 提取文本

好,既然刚才提取的结果是一个可迭代对象 SelectorList,那么要获取提取到的所有 li 节点的文本内容就要对结果进行遍历了,写法如下:

1
2
3
4
5
6
from parsel import Selector
selector = Selector(text=html)
items = selector.css('.item-0')
for item in items:
text = item.xpath('.//text()').get()
print(text)

这里我们遍历了 items 变量,赋值为 item,那么这里 item 又变成了一个 Selector 对象,那么此时我们又可以调用其 css 或 xpath 方法进行内容提取了,比如这里我们就用 .//text() 这个 XPath 写法提取了当前节点的所有内容,此时如果不再调用其他方法,其返回结果应该依然为 Selector 构成的可迭代对象 SelectorList。SelectorList 有一个 get 方法,get 方法可以将 SelectorList 包含的 Selector 对象中的内容提取出来。

运行结果如下:

1
2
3
first item
third item
fifth item

这里 get 方法的作用是从 SelectorList 里面提取第一个 Selector 对象,然后输出其中的结果。

我们再看一个实例:

1
2
result = selector.xpath('//li[contains(@class, "item-0")]//text()').get()
print(result)

输出结果如下:

1
first item

其实这里我们使用 //li[contains(@class, "item-0")]//text() 选取了所有 class 包含 item-0 的 li 节点的文本内容。应该来说,返回结果 SelectorList 应该对应三个 li 对象,而这里 get 方法仅仅返回了第一个 li 对象的文本内容,因为其实它会只提取第一个 Selector 对象的结果。

那有没有能提取所有 Selector 的对应内容的方法呢?有,那就是 getall 方法。

所以如果要提取所有对应的 li 节点的文本内容的话,写法可以改写为如下内容:

1
2
result = selector.xpath('//li[contains(@class, "item-0")]//text()').getall()
print(result)

输出结果如下:

1
['first item', 'third item', 'fifth item']

这时候,我们就能得到列表类型结果了,和 Selector 对象是一一对应的。

因此,如果要提取 SelectorList 里面对应的结果,可以使用 get 或 getall 方法,前者会获取第一个 Selector 对象里面的内容,后者会依次获取每个 Selector 对象对应的结果。

另外上述案例中,xpath 方法改写成 css 方法,可以这么实现:

1
2
result = selector.css('.item-0 *::text').getall()
print(result)

这里* 用来提取所有子节点(包括纯文本节点),提取文本需要再加上::text,最终的运行结果是一样的。

到这里我们就简单了解了文本提取的方法。

5. 提取属性

刚才我们演示了 HTML 中文本的提取,直接在 XPath 中加入 //text() 即可,那提取属性怎么做呢?类似的方式,也直接在 XPath 或者 CSS Selector 中表示出来就好了。

比如我们提取第三个 li 节点内部的 a 节点的 href 属性,写法如下:

1
2
3
4
5
6
from parsel import Selector
selector = Selector(text=html)
result = selector.css('.item-0.active a::attr(href)').get()
print(result)
result = selector.xpath('//li[contains(@class, "item-0") and contains(@class, "active")]/a/@href').get()
print(result)

这里我们实现了两种写法,分别用 css 和 xpath 方法实现。我们根据同时包含 item-0 和 active 这两个 class 为依据来选取第三个 li 节点,然后进一步选取了里面的 a 节点,对于 CSS Selector,选取属性需要加 ::attr() 并传入对应的属性名称来选取,对于 XPath,直接用 /@ 再加属性名称即可选取。最后统一用 get 方法提取结果即可。

运行结果如下:

1
2
link3.html
link3.html

可以看到两种方法都正确提取到了对应的 href 属性。

6. 正则提取

除了常用的 css 和 xpath 方法,Selector 对象还提供了正则表达式提取方法,我们用一个实例来了解下:

1
2
3
4
from parsel import Selector
selector = Selector(text=html)
result = selector.css('.item-0').re('link.*')
print(result)

这里我们先用 css 方法提取了所有 class 包含 item-0 的节点,然后使用 re 方法,传入了 link.*,用来匹配包含 link 的所有结果。

运行结果如下:

1
['link3.html"><span class="bold">third item</span></a></li>', 'link5.html">fifth item</a></li>']

可以看到,re 方法在这里遍历了所有提取到的 Selector 对象,然后根据传入的正则表达式查找出符合规则的节点源码并以列表的形式返回。

当然如果在调用 css 方法时已经提取了进一步的结果,比如提取了节点文本值,那么 re 方法就只会针对节点文本值进行提取:

1
2
3
4
from parsel import Selector
selector = Selector(text=html)
result = selector.css('.item-0 *::text').re('.*item')
print(result)

运行结果如下:

1
['first item', 'third item', 'fifth item']

另外我们也可以利用 re_first 方法来提取第一个符合规则的结果:

1
2
3
4
from parsel import Selector
selector = Selector(text=html)
result = selector.css('.item-0').re_first('<span class="bold">(.*?)</span>')
print(result)

这里调用了 re_first 方法,这里提取的是被 span 标签包含的文本值,提取结果用小括号括起来表示一个提取分组,最后输出的结果就是小括号部分对应的结果,运行结果如下:

1
third item

通过这几个例子我们知道了正则匹配的一些使用方法,re 对应多个结果,re_first 对应单个结果,可以在不同情况下选择对应的方法进行提取。

7. 总结

parsel 是一个融合了 XPath、CSS Selector 和正则表达式的提取库,功能强大又灵活,建议好好学习一下,同时也可以为后文学习 Scrapy 框架打下基础,有关 parsel 更多的用法可以参考其官方文档:https://parsel.readthedocs.io/。

本节代码:https://github.com/Python3WebSpider/ParselTest。