Python 爬虫框架

Python Scrapy 爬虫教程之选择器 Selectors

2019-10-12  本文已影响0人  别摸我蒙哥

Selectors

在抓取一个web页面的时候,大多数任务在于从HTML源中提取数据。有很多可用的的库支持这些操作,比如:

Scrapy 有自己的提取数据的机制。它们称之为 selectors(选择器),因为从 HTML 文档中筛选特定内容,可以使用XPathCSS表达式。

XPath是一个筛选 XML 文档节点的语言,也能用于筛选 HTML 文档。
CSS 是一个应用格式到 HTML 文档的语言,它定义选择器与特定 HTML 元素格式之间的关系。

注意:
Scrapy 选择器是 `parsel` 库的轻量级再封装;封装的目的是为了与 Scrapy 响应对象提供更好的整合。
`parsel` 是一个独立的网页抓取库,可以不依赖于 Scrapy。它使用 `lxml` 库作为底层引擎,并且在 lxml 顶层接口再实现了一个简单API。这意味着 Scrapy 选择器的速度与解析准确性与 lxml 十分接近。

一、使用选择器

scrapy shell https://docs.scrapy.org/en/latest/_static/selectors-sample1.html 为例

1.1 构建选择器

使用 .selector 暴露 Response 对象的 Selector 实例:

In [4]: response.selector.xpath('//a/text()').getall()
Out[4]:
['Name: My image 1 ',
 'Name: My image 2 ',
 'Name: My image 3 ',
 'Name: My image 4 ',
 'Name: My image 5 ']

更简洁的方式查询,Scrapy 提供了两种简写:response.xpath()response.css()

In [5]: response.css('a::text').getall()
In [6]: response.xpath('//a/text()').getall()

Scrapy selectors 是 Selector 类的实例,通过传输 TextResponse 对象或作为 unicode 的补全来构成。
通常不需要手动构造 Scrapy selectors,原因如下:response 对象可以用于 Spider 回调,所以大部分场景下会偏向于使用 response.css()response.xpath() 作为简写。通过使用 response.selectorresponse.xpath() response.css() 可以确保响应体只被解析一次。

todo:这里的 Selector 类 和 Spider 与 response 之间的回调 问题,以后在研究源码的时候着重看看怎么处理的

但是在必要条件下,需要直接使用 Selector ,比如要从如下的内容中构造对象:

>>> from scrapy.selector import Selector
>>> body = '<html><body><span>good</span></body></html>'
>>> Selector(text=body).xpath('//span/text()').get()
'good'

Selector 会自动基于输入类型选择最好的解析规则(XML 或 HTML)。

1.2 选择器

接下来先介绍如何使用Scrapy shell(它提供了可交互的测试)
使用方法scrapy shell https://docs.scrapy.org/en/latest/_static/selectors-sample1.html

在 shell 加载完成后,可以通过 response 直接获取响应,通过 response.selector 属性获取选择器

由于该网页返回的是 HTML 内容,所以选择器会自动使用 HTML 解析模式。

为title标签内的文本构造 XPath 访问方式

In [7]: response.xpath('//title/text()')
Out[7]: [<Selector xpath='//title/text()' data='Example website'>]

可以看到返回的是一个选择器对象列表,如果想要提取文本内容,需要使用选择器的 .get() 或者 .getall() 方法,如下所示:

In [8]: response.xpath('//title/text()').get()
Out[8]: 'Example website'

.get() 永远只返回一个结果;

如果选择器有多个匹配,那么只返回匹配的第一个内容;
如果只有没有匹配,将返回 None

.getall() 返回一个结果列表

注意 CSS 选择器可以使用 CSS3 的伪元素(pseudoelements)选择文本内容和属性节点。

In [9]: response.css('title::text')
Out[9]: [<Selector xpath='descendant-or-self::title/text()' data='Example website'>]

In [10]: response.css('title::text').get()
Out[10]: 'Example website'

可以得知,.xpath().css() 方法都会返回一个 SelectorList 实例对象,该实例对象是选择器的列表。

这个接口还可以用于快速选择嵌套的数据:

In [11]: response.css('img').xpath('@src').getall()
Out[11]:
['image1_thumb.jpg',
 'image2_thumb.jpg',
 'image3_thumb.jpg',
 'image4_thumb.jpg',
 'image5_thumb.jpg']

自定义返回结果

如果在获取元素时,没有发现对应的元素,那么将返回 None;但是也可以自定义返回结果:

In [12]: response.css('ab_post').get('no_element')
Out[12]: 'no_element'

css 获取属性值 .attrib

在上面的例子中,获取 src 属性使用了 ‘@src’ XPath,在 CSS 中通过 .attrib 也可以查询该属性。

In [13]: [img.attrib['src'] for img in response.css('img')]
Out[13]:
['image1_thumb.jpg',
 'image2_thumb.jpg',
 'image3_thumb.jpg',
 'image4_thumb.jpg',
 'image5_thumb.jpg']

.attrib 作为简写同样也可以直接获取 SelectorList, 它返回第一个被匹配的元素:

In [14]: response.css('img').attrib['src']
Out[14]: 'image1_thumb.jpg'

这种做法在只需要一个结果时非常有用,比如当根据id或页面上唯一的元素选择时:

In [18]: response.css('base').attrib['href']
Out[18]: 'http://example.com/'

综合用法

获取 base 标签的 href 属性内容

In [18]: response.css('base').attrib['href']
Out[18]: 'http://example.com/'

In [19]: response.xpath('//base/@href').get()
Out[19]: 'http://example.com/'

In [20]: response.css('base::attr(href)').get()
Out[20]: 'http://example.com/'

In [21]: response.css('base').attrib['href']
Out[21]: 'http://example.com/'

获取标签的 href 属性内容(限定内容包含 “image”)

In [24]: response.xpath('//a[contains(@href, "image")]/@href').getall()
Out[24]: ['image1.html', 'image2.html', 'image3.html', 'image4.html', 'image5.html']

In [25]: response.css('a[href*=image]::attr(href)').getall()
Out[25]: ['image1.html', 'image2.html', 'image3.html', 'image4.html', 'image5.html']

获取标签 href 内容包含“image”的子标签 src 属性值

In [26]: response.xpath('//a[contains(@href, "image")]/img/@src').getall()
Out[26]:
['image1_thumb.jpg',
 'image2_thumb.jpg',
 'image3_thumb.jpg',
 'image4_thumb.jpg',
 'image5_thumb.jpg']

In [27]: response.css('a[href*=image] img::attr(src)').getall()
Out[27]:
['image1_thumb.jpg',
 'image2_thumb.jpg',
 'image3_thumb.jpg',
 'image4_thumb.jpg',
 'image5_thumb.jpg']

1.3 扩展CSS选择器

根据 W3C 标准,CSS 选择器不能提供选择文本节点或属性值的功能。
但是在网页中抓取内容是非常必要的,Scrapy 为此实现了许多非标准的伪元素。

实例

1.4 嵌套选择器

筛选模式( .xapth().css() )都返回同一类型选择器的列表,所以你可以对这些筛选列表进行调用,下面是例子:

In [28]: links = response.xpath('//a[contains(@href, "image")]')

In [29]: links.getall()
Out[29]:
['<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>']

In [30]: for index, link in enumerate(links):
    ...:     args = (index, link.xpath('@href').get(), link.xpath('img/@src').get())
    ...:     print('Link numver %d points to url %r and image %r' % args)
    ...:
Link numver 0 points to url 'image1.html' and image 'image1_thumb.jpg'
Link numver 1 points to url 'image2.html' and image 'image2_thumb.jpg'
Link numver 2 points to url 'image3.html' and image 'image3_thumb.jpg'
Link numver 3 points to url 'image4.html' and image 'image4_thumb.jpg'
Link numver 4 points to url 'image5.html' and image 'image5_thumb.jpg'

1.5 选择元素属性

有多种方式获取属性的值。

XPath 选择属性

首先,可以使用 XPath 语法:

In [31]: response.xpath('//a/@href').getall()
Out[31]: ['image1.html', 'image2.html', 'image3.html', 'image4.html', 'image5.html']

XPath 语法有多种优势:在标准的 XPath 格式中,@attributes 可以作为 XPath 表达式的一部分,等等,可以通过属性值来筛选内容。

CSS 选择属性

Scrapy 也提供了 CSS 选择器的扩展(::attr(...)),它允许这样获得属性值:

In [32]: response.css('a::attr(href)').getall()
Out[32]: ['image1.html', 'image2.html', 'image3.html', 'image4.html', 'image5.html']

Selector attrib 属性

除了这种做法,还有 Selector 的 .attrib 属性。
如果你更希望使用 Python 代码,而不是通过 XPath 或者 CSS 扩展来获取属性,那么这种方式也比较有用:

In [33]: [a.attrib['href'] for a in response.css('a')]
Out[33]: ['image1.html', 'image2.html', 'image3.html', 'image4.html', 'image5.html']

这种 python 代码直接读取的方式也可作用于 SelectorList 上;
将会将返回字典,包含第一个匹配元素的属性和值。
如果只需要选择器返回一个结果,那么这么做很方便:

In [34]: response.css('base').attrib
Out[34]: {'href': 'http://example.com/'}

In [35]: response.css('base').attrib['href']
Out[35]: 'http://example.com/'

1.6 在选择器中使用正则表达式

Selector 类还有 .re() 方法用于提取正则匹配的数据。
然而,不同于 .xpath() 和 .css() 方法,.re() 返回的是一个字符串列表。
所以 .re() 不能做内嵌的 .re() 调用。

提取图片名字的例子

In [36]: response.xpath('//a[contains(@href, "image")]/text()').re(r'Name:\s*(.*)')
Out[36]: ['My image 1 ', 'My image 2 ', 'My image 3 ', 'My image 4 ', 'My image 5 ']

In [37]: response.xpath('//a[contains(@href, "image")]/text()').re_first(r'Name:\s*(.*)')
Out[37]: 'My image 1 '

二、XPaths 的使用

在 Scrapy 中高效使用 XPath 的一些建议

扩展链接: http://www.zvon.org/comp/r/tut-XPath_1.html
扩展链接2:https://blog.scrapinghub.com/2014/07/17/xpath-tips-from-the-web-scraping-trenches/

2.1 使用 XPath 相对路径

假设你在嵌套选择器中使用 XPath,如果路径以 '/' 开头,那么 Xpath 会指向文档的绝对位置,而不是你现在调用的选择器。

比如,如果想提取 <div>元素 中的所有 <a> 元素。

首先,获取 <div> 元素,然后遍历 dvi 内容时使用 '//a' 定位元素

In [38]: divs = response.xpath('//div')

In [39]: for p in divs.xpath('//a'):
    ...:     print(p.get())
    ...:

结果返回为空。

实际上,第39行代码中的路径,意味着重新以文档起点为基准构建path,而不是在 <div> 元素中构建。

可以做这样的修改:

In [56]: for p in divs.xpath('.//a'):
    ...:     print(p)
    ...:
<Selector xpath='.//a' data='<a href="image1.html">Name: My image 1 <'>
<Selector xpath='.//a' data='<a href="image2.html">Name: My image 2 <'>
<Selector xpath='.//a' data='<a href="image3.html">Name: My image 3 <'>
<Selector xpath='.//a' data='<a href="image4.html">Name: My image 4 <'>
<Selector xpath='.//a' data='<a href="image5.html">Name: My image 5 <'>

更常规的使用方法,直接使用 a 即可:

In [56]: for p in divs.xpath('a'):
    ...:     print(p)
    ...:

2.2 当通过类 class 查询时,尽量考虑 CSS

由于一个元素可以包含多个 CSS classes,通过 XPath 锁定 class 的方式去选择元素会冗长无比。

你需要这样操作 contains(@class, 'someclass') 来弥补多个类的情况。

事实证明,Scrapy 选择器允许你使用「链选择器」,所以大多数情况下可以采用:先使用 CSS 筛选class,然后再切换到 XPath 继续筛选。

>>> sel = Selector(text='<div class="hero shout"><time datetime="2014-07-23 19:00">')
>>> sel.css('.shout').xpath('./time/@datetime').get()
'2014-07-23 19:00'

这种灵活使用的「链式筛选」非常实用,别忘了在使用 .xpath() 的时候,路径添加 '.'。

2.3 区别 //node[1] 和 (//node)[1]

//node[1] 返回的是所有父节点下,第一次出现的节点
(//node)[1] 返回所有文档匹配节点的第一个,只返回一个值

2.4 在条件中使用文本节点 ❤️

获取文本内容最好选择 XPath https://www.w3.org/TR/xpath/all/#section-String-Functions,而不要使用 .//text() 而选择 '.' 。

这是因为 .//text() 表达式会生成一个节点集合,当 节点集合(node-set) 转换成字符串,这个过程经常发生传递参数到字符串函数中,如 conatins() 或 starts_with() 函数,这会导致只会处理第一个元素。

>>> sel = Selector(text='<a href="#">Click here to go to the <strong>Next Page</strong>')
>>> sel.xpath('//a//text()').getall()
['Click here to go to the ', 'Next Page']
>>> sel.xpath("string(//a[1]//text())").getall() # convert it to string
['Click here to go to the ']

这里只返回了列表中的第一个元素

处理方式:将整个节点转换为字符串,这样就会把字内容都加在一起

>>> sel.xpath('//a[1]').getall()
['<a href="#">Click here to go to the <strong>Next Page</strong></a>']
>>> sel.xpath('string(//a[1])').getall()
['Click here to go to the Next Page']

从子节点中匹配字符串

如:

>>> sel.xpath("//a[contains(.//text(), 'Next Page')]").getall()
[]

使用 '.' ,情况变得不同

>>> sel.xpath("//a[contains(., 'Next Page')]").getall()
['<a href="#">Click here to go to the <strong>Next Page</strong></a>']

2.5 XPath 中的表达式

XPath 允许你在 XPath 表达式中引用变量,语法:$somevariable。
这有点类似于参数查询或 SQL 中的查询占位符 '?',意味着你可以预制查询点,再设置值。

举例一:不通过硬编码的方式,通过 id 属性来匹配元素

In [66]: response.xpath('//div[@id=$val]/a/text()', val='images').get()
Out[66]: 'Name: My image 1 '

举例二:找到包含五个<a>子标签的<div>标签的id值

In [69]: response.xpath('//div[count(a)=$cnt]/@id', cnt=5).get()
Out[69]: 'images'

在调用 .xpath() 的时候,必须绑定引用变量的值,否值会抛出 'ValueError: XPath error' 错误。

如果想了解更多,这部分内容是以 parsel 库为基础,更多详情和例子在 parse 教程中有介绍

2.6 移除域名

在使用爬虫项目时,经常会使用到「清除域名,只处理元素名」的操作,从而获得更简单/方便的 XPath。
你可以使用 Selector.remove_namespaces() 方法来移除域名。

首先,找到一个博客网站用于实践

(base) ☁  cherry  scrapy shell https://feeds.feedburner.com/PythonInsider

如果网页设定了命名空间,一旦我们想要尝试获取某些标签对象,会全部失败,返回None,因为 Atom XML 会混淆这些节点标签。

但是如果先调用了 Selector.remove_namespaces() 方法,那么就可以直接获取这些名称。

如果你在好奇为什么域名空间移除操作为什么不默认执行,而是采取手动的方式。简单来说有两个原因:

  1. 移除域名空间需要遍历和修改文档中所有节点,这意味着在Scrapy对所有文档执行默认抓取操作过程中,不可避免的产生高昂的操作量。
  2. 预防在某些情况下不需要使用域名空间,尽管这个操作很少见。

2.7 使用 EXSLT 扩展 ❤️

由于基于 lxml 顶层再封装,所以 Scrapy 也支持一些 EXSLT 扩展。
它可以预注册域名空间从而使用 XPath 表达式。

预置 域名 使用方式
re http://exslt.org/regular-expression http://exslt.org/regexp/index.html
set http://exsl.org/sets http://exslt.org/set/index.html

2.7.1 正则表达式

在 test() 方法中,可以提供许多 XPath 没法提供的方法

doc = u"""
<div>
    <ul>
        <li class="item-0"><a href="link1.html">first item</a></li>
        <li class="item-1"><a href="link2.html">second item</a></li>
        <li class="item-inactive"><a href="link3.html">third item</a></li>
        <li class="item-1"><a href="link4.html">fourth item</a></li>
        <li class="item-0"><a href="link5.html">fifth item</a></li>
    </ul>
</div>
"""
>>> sel = Selector(text=doc, type='html')
>>> sel.xpath('//li//@href').getall()
['link1.html', 'link2.html', 'link3.html', 'link4.html', 'link5.html']
>>> sel.xpath('//li[re:test(@class, "item-\d$")]//@href').getall()
['link1.html', 'link2.html', 'link4.html', 'link5.html']

这里的 li[re:test(@class, "item-\d$')] 使用了正则表达式,这前面的 re() 处理节点文本不同,是处理属性值的正则表达式,而这个功能是 XPath 提供的 contains() 和 start-with() 进阶版本,它可以实现更强的匹配功能。

注意 🧡
C 语言库的 libxslt 不支持本地 EXSLT 正则表达式,所以 lxml 的功能实现与 Python 的 re 挂钩。尽管如此,在 XPath 中使用正则表达式函数会对性能有影响。

2.7.2 set 操作

set 操作可以在抓取文本前很方便地排除部分文档树内容。

比如从某个网站提取微型结构数据:网址(http://schema.org/Product)。只提取 timescopes 和与 itemprops 有关的组。

props = scop.xpath('''set:difference(./descendant::*/@itemprop, .//*[@itemscope]/*/@itemprop)''')

2.8 其他的 XPath 扩展

has-class 扩展

<p class="foo bar-baz">First</p>
<p class="foo">Second</p>
<p class="bar">Third</p>
<p>Fourth</p>

可以这么使用

>>> response.xpath('//p[has-class("foo")]')
[<Selector xpath='//p[has-class("foo")]' data='<p class="foo bar-baz">First</p>'>,
 <Selector xpath='//p[has-class("foo")]' data='<p class="foo">Second</p>'>]
>>> response.xpath('//p[has-class("foo", "bar-baz")]')
[<Selector xpath='//p[has-class("foo", "bar-baz")]' data='<p class="foo bar-baz">First</p>'>]
>>> response.xpath('//p[has-class("foo", "bar")]')
[]

所以 XPath 路径 '//p[has-class("foo", "bar-baz")]' 大致等同于 CSS 路径 'p.foo.bar-baz'。

但是记住,XPath 的方式会更慢,因为他是纯 Python 函数,这种方式仅适用于 CSS 选择器无法正常描述的场景。

parsel 的简单扩展

parsel.xpathfuncs.set_xpathfunc(fname, func)

三、引用内建的选择器

3.1 Selector 选择器对象

class scrapy.selector.Selector(response=None, text=None, type=None, root=None, **kwargs)

Selector 对象是选择部分响应内容的包装。

response

是 HtmlResponse 或 XMLResponse 对象,用于选择和抓取数据

text

uincode 字符串或utf-8编码的文本,在 response 不可用的情况下替代。不会发生使用 text 或 reponse 。

type

定义了选择器的类型,可以为 html/xml/None(默认)

  1. 如果 type 为 None,那么选择器基于响应,会选择最好的类型解析内容;如果内容为文本,则默认为 html。
  2. 如果 type 为 None,且传入 response,那么选择器的类型将会按照下面类型推断:
    • HtmlResponse 对应类型为 html
    • XmlResponse 对应选择器类型为 xml
    • 否则选择器类型为 html
  3. 如果 type 设定了类型,则不会对内容格式进行检测,强制使用设置的类型。

方法一 xpath

xpath(query, namespace=None, **kwargs)

寻找匹配 xpath 查询的节点,返回 SelectorList 选择器列表实例,扁平化所有的元素。
元素列表也实现了 Selector 的接口。

XPath 查询体,字符串

通过 register_namespace(prefix, uri) 注册后的内容,可以通过 prefis: namespace-uri 的形式使用,不过仅限于当前 Selector 调用。
TODO:这种方式的使用,有一点了解,但是不是很透彻,可待来日解决

任何新增的参数名称都可以通过 XPath 表达式传输给 XPath 变量

为方便起见,这个方法可以被 response.xpath() 调用

方法二 css

css(query)

应用给定的 CSS 选择器,返回 SelectorList 实例

CSS 选择器,字符串格式

在后台,CSS 查询会被解析成 XPath,并执行 .xpath() 方法

方法三 get

get()

加载并返回匹配的节点

attrib

返回基础元素的属性字典

re

re(regex, replace_entities=True)

应用给定的正则表达式,返回匹配的 unicode 字符串列表

默认情况下,如 ‘&' 会被直接解析成对应的内容;replace_entities 为 False 则不会做这种改变。

re_first

re_first(regex, default=None, replace_entities=True)

应用给定的正则表达式,返回第一个被匹配的 unicode 字符串。
如果没有匹配,返回默认的值(如果默认值没有提供,则默认为None)

另一个可选参数,如上

remove_namespaces()

移除所有的域名,允许直接跨过文档获取无域名空间的路径。

_bool_()

如果没有任何内容,返回 True,否则返回 False。
换句话说,选择器是否有内容

getall()

加载并返回匹配的所有节点,以 unicode 字符串列表展示。

3.2 选择器列表对象 SelectorList objects

class scrap.selector.SelectorList

SelectorList 对象是 list 子类,提供一些额外的方法

xpath(xpath, namespaces=None, **kwargs)

每个元素都调用 .xpath() 方法,并扁平化返回另一个 SelectorList 对象

css(query)

为每个元素调用 .css() 方法

getall()

为每个元素调用 .get() 方法

get(default=None)

返回第一个元素调用 .get() 的结果。
如果列表为空,则返回默认的结果

re(regex, replace_entities=True)

为每个元素调用 .re() 方法,扁平化返回列表

re_first(regex, default=None, replace_entities=True)

attrib

四、例子

选择 XML 响应

抓取网址:https://support.google.com/merchants/answer/160589?hl=en&ref_topic=2473799

提取所有的价格,需要注册一个域名空间:

sel = Selector(xml_reponse)

# 注册域名空间
sel.regisgter_namespace("g", "http://base.google.com/ns/1.0")
sel.xpath("//g:price").getall()

[1] https://docs.scrapy.org/en/latest/topics/selectors.html

上一篇 下一篇

猜你喜欢

热点阅读