Selector的使用
我们之前介绍了利用 Beautiful Soup、pyquery 以及正则表达式来提取网页数据的方法,确实非常方便。不过 Scrapy 提供了自已的数据提取方法,即内置的 Selector。
在 3.4 节我们已经初步了解了 parsel 库的基本用法,Scrapy 中的 Selector 是就是基于 parsel 库来构建的,而同时 parsel 又依赖于 lxml,Selector 对 parsel 进行了封装,使其能更好地与 Scrapy 结合使用。
Selector 支持 XPath 选择器、CSS 选择器以及正则表达式:功能全面,解析速度和准确度非常高。
本节我们就来详细介绍下 Selector 的用法。
直接使用
Selector 其实并不一定非要在 Scrapy 中使用,它也是一个可以独立使用的模块。我们可以直接利用 Selector 这个类来构建一个选择器对象,然后调用它的相关方法(如 xpath,css 等)来提取数据。
例如,针对一段 HTML 代码,我们可以用如下方式构建 Selector 对象来提取数据:
from scrapy import Selector
body = '<html><head><title>Hello World</title></head><body></body></html>'
selector = Selector(text=body)
title = selector.xpath('//title/text()').extract_first()
print(title)
运行结果如下:
Hello World
这里没有在 Scrapy 框架中运行,而是把 Scrapy 中的 Selector 单独拿出来使用了,构建的时候传入 text 参数,就生成了一个 Selector 选择器对象,然后就可以像 Scrapy 中的解析方式一样,调用 xpath,css 等方法来提取数据了。
在这里我们查找的是源代码中 title 内的文本,在 XPath 选择器最后加 text 方法就可以实现文本的提取了。
以上内容就是 Selector 的直接使用方式。同 Beautiful Soup 等库类似,Selector 也是强大的网页解析库。如果方便的话,我们t也可以在其他项目中直接使用 Selector 来提取数据。
接下来,我们用实例来详细讲解 Selector 的用法。
Scrapy Shell
由于 Selector 主要是与 Scrapy 结合使用,如 Scrapy 的回调函数中的参数 response 直接调用 xpath 或者 css 方法来提取数据,所以在这里我们借助 Scrapy shell 来模拟 Scrapy 请求的过程,讲解相关的提取方法。
我们用官方文档的一个样例页面来做演示: https://doc.scrapy.org/en/latest/static/selectors-sample1.html 。
开启 Scrapy shell,在命令行输入如下命令:
scrapy shell https://doc.scrapy.org/en/latest/_static/selectors-sample1.html
我们就进入 Scrapy shell 模式了。这个过程其实是 Scrapy 发起了一次请求,请求的 URL 就是刚才命令行下输人的 URL,把一些可操作的变量传递给我们,如 request、response 等,如下 15-7 所示。
2024-01-30 16:55:04 [scrapy.extensions.telnet] INFO: Telnet console listening on 127.0.0.1:6023
2024-01-30 16:55:04 [rotating_proxies.middlewares] INFO: Proxies(good: 0, dead: 0, unchecked: 1, reanimated: 0, mean backoff time: 0s)
2024-01-30 16:55:04 [scrapy.core.engine] INFO: Spider opened
2024-01-30 16:55:05 [rotating_proxies.expire] DEBUG: Proxy <http://127.0.0.1:1080> is GOOD
2024-01-30 16:55:05 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://doc.scrapy.org/robots.txt> (referer: None)
2024-01-30 16:55:05 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://doc.scrapy.org/en/latest/_static/selectors-sample1.html> (referer: None)
[s] Available Scrapy objects:
[s] scrapy scrapy module (contains scrapy.Request, scrapy.Selector, etc)
[s] crawler <scrapy.crawler.Crawler object at 0x000001B20C3876D0>
[s] item {}
[s] request <GET https://doc.scrapy.org/en/latest/_static/selectors-sample1.html>
[s] response <200 https://doc.scrapy.org/en/latest/_static/selectors-sample1.html>
[s] settings <scrapy.settings.Settings object at 0x000001B20C3877F0>
[s] spider <DefaultSpider 'default' at 0x1b20ce93970>
[s] Useful shortcuts:
[s] fetch(url[, redirect=True]) Fetch URL and update local objects (by default, redirects are followed)
[s] fetch(req) Fetch a scrapy.Request and update local objects
[s] shelp() Shell help (print this help)
[s] view(response) View response in a browser
>>>
我们可以在命令行模式下输入命令,调用对象的一些操作方法,按下回车之后实时显示结果。这与 Python 的命令行交互模式类似。
接下来演示的实例都将页面的源码作为分析目标,页面源码如下所示:
<html>
<head>
<base href='http://example.com/'/>
<title>Example website</title>
</head>
<body>
<div id='images'>
<a href='image1.html'>Name:My image 1<br/><img src='image1_thumb.jpg'/></a>
<a href='image2.html'>Name:My image 2<br/><img src='image2_thumb.jpg'/></a>
<a href='image3.html'>Name:My image 3<br/><img src='image3_thumb.jpg'/></a>
<a href='image4.html'>Name:My image 4<br/><img src='image4_thumb.jpg'/></a>
<a href='image5.html'>Name:My image 5<br/><img src='image5_thumb.jpg'/></a>
</div>
</body>
</html>
XPath 选择器
进入 Scrapy Shell 后,我们主要通过操作 response 变量进行解析。因为我们解析的是 HTML 代码,Selector 将自动使用 HTML 语法来分析。
response 有一个属性 selector,我们调用 response.selector 返回的内容就相当于用 response 的 text 构造了一个 Selector 对象。通过这个 Selector 对象,我们可以调用如 xpath,css 等解析方法向方法传入 XPath或 CSS 选择器参数就可以实现信息的提取。
我们用一个实例感受一下,代码如下所示:
>>> result = response.selector.xpath('//a')
>>> result
[<Selector query='//a' data='<a href="image1.html">Name: My image ...'>,
<Selector query='//a' data='<a href="image2.html">Name: My image ...'>,
<Selector query='//a' data='<a href="image3.html">Name: My image ...'>,
<Selector query='//a' data='<a href="image4.html">Name: My image ...'>,
<Selector query='//a' data='<a href="image5.html">Name: My image ...'>]
>>> type(result)
<class 'scrapy.selector.unified.SelectorList'>
>>>
打印结果的形式是 Selector 组成的列表,其实它是 SelectorList 类型,SelectorList 和 Selector 都可以迷续调用 xpath 和 css 等方法来进一步提取数据。
在上面的例子中,我们提取了 a 节点。接下来,我们尝试继续调用 xpath 方法来提取 a 节点内包含的 img 节点,代码如下所示:
>>> result.xpath('./img')
[<Selector query='./img' data='<img src="image1_thumb.jpg" alt="imag...'>,
<Selector query='./img' data='<img src="image2_thumb.jpg" alt="imag...'>,
<Selector query='./img' data='<im
g src="image3_thumb.jpg" alt="imag...'>,
<Selector query='./img' data='<img src="image4_thumb.jpg" alt="imag...'>,
<Selector query='./img' data='<img src="image5_thumb.jpg" alt="imag...'>]
我们获得了 a 节点里面的所有 img 节点,结果为 5。
值得注意的是,选择器的最前方加 .
(一个点)代表提取元素内部的数据,如果没有加点,则代表从根节点开始提取。此处我们用了 ./img
的提取方式,代表从 a 节点里进行提取。如果此处我们用 //img,则还是从 html 节点里进行提取。
我们刚才使用 response.selector.xpath 方法对数据进行了提取。Scrapy 提供了两个实用的快捷方法,response.xpath 和 response.css,二者的功能完全等同于 response.selector.xpath 和 response.selector.css。
方便起见,后面我们统一直接调用 response 的 xpath 和 css 方法进行选择。
现在我们得到的是 SelectorList 类型的变量,该变量是由 Selector 对象组成的列表。可以用索引单独取出其中某个 Selector 元素:代码如下所示:
>>> result[0]
<Selector query='//a' data='<a href="image1.html">Name: My image ...'>
我们可以像操作列表一样操作这个 selectorList。
但是现在获取的内容是 Selector 或者 SelectorList 类型,并不是真正的文本内容。具体的内容怎么提取呢?
比如我们现在想提取 a 节点元素,就可以利用 extract 方法,代码如下所示:
>>> result.extract()
['<a href="image1.html">Name: My image 1 <br><img src="image1_thumb.jpg" alt="image1"></a>', '<a href="image2.html">Name: My image 2 <br><img src="image2_thumb.jpg" alt="image2"></a>
', '<a href="image3.html">Name: My image 3 <br><img src="image3_thumb.jpg" alt="image3"></a>', '<a href="image4.html">Name: My image 4 <br><img src="image4_thumb.jpg" alt="image4"></a>', '<a href="image5.html">Name: My image 5 <br><img src="image5_thumb.jpg" alt="image5"></a>']
这里使用了 extract 方法,我们可以把真实需要的内容获取下来。
我们还可以改写 XPath 表达式,来选取节点的内部文本和属性,代码如下所示:
>>> response.xpath('//a/text()').extract()
['Name: My image 1 ', 'Name: My image 2 ', 'Name: My image 3 ', 'Name: My image 4 ', 'Name: My image 5 ']
>>> response.xpath('//a/@href').extract()
['image1.html', 'image2.html', 'image3.html', 'image4.html', 'image5.html']
我们只需要再加一层/text() 就可以获取节点的内部文本,或者加一层/@href 就可以获取节点的 href 属性。其中,@ 符号后面内容就是要获取的属性名称。
现在,我们可以用一个规则获取所有符合要求的节点,返回的类型是列表类型。
但是这里有一个问题:如果符合要求的节点只有一个,那么返回的结果会是什么呢?我们再用一个实例来感受一下,代码如下所示:
>>> response.xpath('//a[@href="image1.html"]/text()').extract()
['Name: My image 1 ']
但是,这个写法很明显是有风险的。一旦 XPath 有问题,extract 后的结果可能是一个空列表。如果我们再用索引来获取,就可能导致数组越界。
所以,另外一个方法可以专门提取单个元素,它叫作 extract_first。我们可以改写上面的例子,相关代码如下:
>>> response.xpath('//a[@href="image1.html"]/text()').extract_first()
'Name: My image 1 '
这样,我们直接利用 extract_first 方法将匹配的第一个结果提取出来,同时也不用担心数组越界的问题了。
另外,我们也可以为 extract_first 方法设置一个默认值,这样当 XPath 规则提取不到内容时,就会直接使用默认值。例如将 XPath 改成一个不存在的规则,重新执行代码,代码如下所示:
>>> response.xpath('//a[@href="image1"]/text()').extract_first()
>>> response.xpath('//a[@href="image1"]/text()').extract_first('Default Image')
'Default Image'
这里,如果 XPath 匹配不到任何元素,调用 extract_first 会返回空,也不会报错。
在第二行代码中,我们还传递了一个参数当作默认值,如 Default Image。这样,如果 XPath 匹配不到结果,返回值会使用这个参数来代替,可以看到输出正是如此。
到现在为止,我们了解了 Scrapy 中的 XPath 的相关用法,包括嵌套查询、提取内容,提取单个内容、获取文本和属性等。
CSS 选择器
接下来,我们看看 CSS 选择器的用法。
Scrapy 的选择器同时还对接了 CSS 选择器,使用 response.css 方法就可以使用 CSS 选择器来选择对应的元素了。
例如在上文我们选取了所有的 a 节点,那么 CSS 选择器同样可以做到,相关代码如下:
>>> response.css('a')
[<Selector query='descendant-or-self::a' data='<a href="image1.html">Name: My image ...'>,
<Selector query='descendant-or-self::a' data='<a href="image2.html">Name: My image ...'>,
<Selector query='descendant-or-self::a' data='<a href="image3.html">Name: My image ...'>,
<Selector query='descendant-or-self::a' data='<a href="image4.html">Name: My image ...'>,
<Selector query='descendant-or-self::a' data='<a href="image5.html">Name: My image ...'>]
同样,调用 extract 方法就可以提取节点,代码如下所示:
>>> response.css('a').extract()
['<a href="image1.html">Name: My image 1 <br><img src="image1_thumb.jpg" alt="image1"></a>', '<a href="image2.html">Name: My image 2 <br><img src="image2_thumb.jpg" alt="image2"></a>
', '<a href="image3.html">Name: My image 3 <br><img src="image3_thumb.jpg" alt="image3"></a>', '<a href="image4.html">Name: My image 4 <br><img src="image4_thumb.jpg" alt="image4"></a>', '<a href="image5.html">Name: My image 5 <br><img src="image5_thumb.jpg" alt="image5"></a>']
可以看到,用法和 XPath 选择是完全一样的。
另外,我们也可以进行属性选择和嵌套选择,代码如下所示:
>>> response.css('a[href="image1.html"]').extract()
['<a href="image1.html">Name: My image 1 <br><img src="image1_thumb.jpg" alt="image1"></a>']
>>> response.css('a[href="image1.html"] img').extract()
['<img src="image1_thumb.jpg" alt="image1">']
这里用 [href="image.html"]限定了 href 属性,可以看到匹配结果就只有一个了。另外如果想查找 a 节点内的 img 节点,只需要再加一个空格和 img。选择器的写法和标准 CSS 选择器写法如出一辙。
我们也可以使用 extract_first 方法提取列表的第一个元素,比如:
>>> response.css('a[href="image1.html"] img').extract_first()
'<img src="image1_thumb.jpg" alt="image1">'
接下来的两个用法不太一样。节点的内部文本和属性的获取是这样实现的:
>>> response.css('a[href="image1.html"]::text').extract_first()
'Name: My image 1 '
>>> response.css('a[href="image1.html"] img::attr(src)').extract_first()
'image1_thumb.jpg'
获取文本和属性需要用 ::text 和 ::attr 的写法,而其他库如 Beautiful Soup 或 pyquery 都有单独的方法。
另外,CSS 选择器和 XPath 选择器一样,能够嵌套选择。我们可以先用 XPath 选择器选中所有 a 节点,再利用 CSS 选择器选中 img 节点,然后用 XPath 选择器获取属性。我们用一个实例来感受一下,代码如下所示:
>>> response.xpath('//a').css('img').xpath('@src').extract()
['image1_thumb.jpg', 'image2_thumb.jpg', 'image3_thumb.jpg', 'image4_thumb.jpg', 'image5_thumb.jpg']
我们成功获取了所有 img 节点的 src 属性。
因此,我们可以随意使用 xpath 和 css 方法,二者自由组合实现嵌套查询,它们是完全兼容的。
正则匹配
Scrapy 的选择器还支持正则匹配。比如在示例的 a 节点中,文本类似于 Name:My image 1,现在我们只想把 Name:后面的内容提取出来,就可以借助 re 方法,代码实现如下:
>>> response.xpath('//a/text()').re('Name:\s(.*)')
['My image 1 ', 'My image 2 ', 'My image 3 ', 'My image 4 ', 'My image 5 ']
我们给 re 方法传了一个正则表送式:其中(.*)就是要匹配的内容、输出的结果就是正则表达式匹配的分组,结果会依次输出。
如果同时存在两个分组,那么结果依然会被按序输出,代码如下所示:
>>> response.xpath('//a/text()').re('(.*?):\s(.*)')
['Name', 'My image 1 ', 'Name', 'My image 2 ', 'Name', 'My image 3 ', 'Name', 'My image 4 ', 'Name', 'My image 5 ']
类似 extract_first 方法,re_first 方法可以选取列表的第一个元素,用法如下:
>>> response.xpath('//a/text()').re_first('(.*?):\s(.*)')
'Name'
>>> response.xpath('//a/text()').re_first('Name:\s(.*)')
'My image 1 '
不论正则匹配了几个分组,结果都会等于列表的第一个元素。
值得注意的是,response 对象不能直接调用 re 和 re_first 方法。如果想要对全文进行正则匹配,可以先调用 xpath 方法再正则匹配,代码如下所示:
>>> response.re('Name:\s(.*)')
Traceback (most recent call last):
File "<console>", line 1, in <module>
AttributeError: 'HtmlResponse' object has no attribute 're'
>>> response.xpath('.').re('Name:\s(.*)<br>')
['My image 1 ', 'My image 2 ', 'My image 3 ', 'My image 4 ', 'My image 5 ']
>>> response.xpath('.').re_first('Name:\s(.*)<br>')
'My image 1 '
通过上面的例子我们可以看到,直接调用 re 方法会提示没有 re 属性。但是这里首先调用了 xpath('.') 选中全文,然后调用了 re 和 re_first 方法,就可以进行正则匹配了。