parsel的使用

前几节我们了解了 LXML 解析器,使用 XPath、pyquery 库,CSS 选择器来提取页面内容的方法。不论 XPath 还是 CSS 选择器,都已经能够满足绝大多数的内容提取,大家可以选择适合自己的库。

这是可能有人会问:我能不能二者穿插使用呀?有时候觉得 XPath 写起来比较方便,有时候觉得 CSS 选择器写起来比较方便,二者能结合吗?答案是可以的。

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

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

介绍

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

准备工作

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

pip3 install parsel

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

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

初始化

我们还是用上一节的 HTML 文本,指明 html 变量如下:

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>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 对象,写法如下:

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

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

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

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 方法也是一样的写法,运行结果如下:

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

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

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

提取文本

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

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 对象,此时又可以调用其 cssxpath 方法进行内容提取了,这里我们是用 .//text() 这个 XPath 写法提取了当前节点的所有内容,此时如果不再调用其他方法:那么返回结果应该依然为 selector 构成的可选代对象 SelectorList。SelectorList 中有一个 get 方法,可以将 SelectorList 包含的 Selector 对象中的内容提取出来。

运行结果如下:

first item
third item
fifth item

get 方法的作用是从 SelectorList 里面提取第一个 Selector 对象,然后输出其中的结果。我们再看一个实例:

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

输出结果如下:

first item

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

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

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

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

输出结果如下:

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

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

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

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

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

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

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

提取属性

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

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

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)

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

运行结果如下:

link3.html
link3.html

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

正则提取

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

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

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

运行结果如下:

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

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

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

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

运行结果如下:

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

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

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

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

third item

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

总结

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