Scrapy selector介绍

王嘉木
2023-12-01

从HTML源文件库中解析数据通常有以下常用的库可以使用:

  • BeautifulSoup是在程序员间非常流行的网页分析库,它基于HTML代码的结构来构造一个Python对象, 对不良标记的处理也非常合理,但它有一个缺点:慢。
  • lxml是一个基于 ElementTree (不是Python标准库的一部分)的python化的XML解析库(也可以解析HTML)。

Scrapy提取数据有自己的一套机制。它们被称作选择器(seletors),因为他们通过特定的 XPath 或者 CSS 表达式来“选择” HTML文件中的某个部分。XPath 是一门用来在XML文件中选择节点的语言,也可以用在HTML上。 CSS 是一门将HTML文档样式化的语言。选择器由它定义,并与特定的HTML元素的样式相关连。

Scrapy选择器构建于 lxml 库之上,这意味着它们在速度和解析准确性上非常相似。不同于 lxml API的臃肿,该API短小而简洁。这是因为 lxml 库除了用来选择标记化文档外,还可以用到许多任务上。

1. Using selectors

1.1 Constructing selectors

Scrapy selectors是Selector类的实例,通过传入text或TextResponse来创建,它自动根据传入的类型选择解析规则(XML or HTML):

from scrapy.selecor import Selector
from scrapy.http import HtmlResponse

从text构建:

body = '<html><body><span>good</span></body></html>'
Selector(text=body).xpath('//span/text()').extract()

从response构建:

response = HtmlResponse(url='http://example.com', body=body)
Selector(response=response).xpath('//span/text()').extract()

response对象以 .selector 属性提供了一个selector, 您可以随时使用该快捷方法:

response.selector.xpath('//span/text()').extract()

1.2 Using selectors

以下面的文档来解释如何使用选择器:

<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>

打开shell:

scrapy shell http://doc.scrapy.org/en/latest/_static/selectors-sample1.html

当shell载入后,我们将获得名为 response 的shell变量, 并且在其 response.selector 属性上绑定了一个selector。

查看title内的文字:

response.selector.xpath('//title/text()')

由于在response中使用XPath、CSS查询十分普遍,因此,Scrapy提供了两个实用的快捷方式: response.xpath()response.css():

response.xpath('//title/text()')
response.css('title::text')

.xpath().css() 方法返回一个类 SelectorList 的实例, 它是一个新选择器的列表。这个API可以用来快速的提取嵌套数据。为了提取真实的原文数据,你需要调用 .extract() 方法如下:

response.css('img').xpath('@src').extract()

如果你只想要第一个匹配的元素,可以使用·.extract_first()·:

response.xpath('//div[@id="images"]/a/text()').extract_first()

注意CSS选择器可以使用CSS3伪元素(pseudo-elements)来选择文字或者属性节点:

response.css('title::text').extract()

现在我们将得到根URL(base URL)和一些图片链接:

1. response.xpath('//base/@href').extract()
1. response.css('base::attr(href)').extract()
2. response.xpath('//a[contains(@href, "image")]/@href').extract()
2. response.css('a[href*=image]::attr(href)').extract()
3. response.xpath('//a[contains(@href, "image")]/img/@src').extract()
3. response.css('a[href*=image] img::attr(src)').extract()

1.3 Nesting selectors(嵌套选择器)

选择器方法返回相同类型的选择器列表,因此你也可以对这些选择器调用选择器方法

links = response.xpath('//a[contains(@href, "image")]')
links.extract()
for index, link in enumerate(links):
    args = (index, link.xpath('@href').extract(), link.xpath('img/@src').extract())
    print 'Link number %d points to url %s and image %s' % args

1.4 Using selectors with regular expressions(正则表达式)

Selector 有一个 .re() 方法,用来通过正则表达式来提取数据。不同于使用 .xpath() 或者 .css() 方法, .re() 方法返回unicode字符串的列表,所以无法构造嵌套式的 .re() 调用。

下面是一个例子,从上面的 HTML code 中提取图像名字:

response.xpath('//a[contains(@href, "image")]/text()').re(r'Name:\s*(.*)')

1.5 Working with relative XPaths(相对XPath)

如果你使用嵌套的选择器,并使用起始为 / 的XPath,那么该XPath将对文档使用绝对路径。

比如,假设你想提取在 <div> 元素中的所有 <p> 元素。首先,你将先得到所有的 <div> 元素:

divs = response.xpath('//div')

开始时,你可能会尝试使用下面的错误的方法,因为它其实是从整篇文档中,而不仅仅是从那些 <div> 元素内部提取所有的 <p> 元素:

for p in divs.xpath('//p'):  # this is wrong - gets all `<p>` from the whole document
    print p.extract()

下面是比较合适的处理方法(注意 .//p XPath的点前缀):

for p in divs.xpath('.//p'):  # extracts all `<p>` inside
    print p.extract()

另一种常见的情况将是提取所有直系 <p> 的结果:

for p in divs.xpath('p'):
    print p.extract()

1.6 Using EXSLT extensions

因建于 lxml 之上, Scrapy选择器也支持一些 EXSLT 扩展,可以在XPath表达式中使用这些预先制定的命名空间:

前缀命名空间用途
rehttp://exslt.org/regular-expressions正则表达式
sethttp://exslt.org/sets集合操作

正则表达式:

例如在XPath的 starts-with() 或 contains() 无法满足需求时, test() 函数可以非常有用。

例如在列表中选择有”class”元素且结尾为一个数字的链接:

from scrapy import Selector
doc = """
<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').extract()
sel.xpath('//li[re:test(@class, "item-\d$")]//@href').extract()

C语言库 libxslt 不原生支持EXSLT正则表达式,因此 lxml 在实现时使用了Python re 模块的钩子。 因此,在XPath表达式中使用regexp函数可能会牺牲少量的性能。

集合操作:
集合操作可以方便地用于在提取文字元素前从文档树中去除一些部分。

例如使用itemscopes组和对应的itemprops来提取微数据(microdata)(来自http://schema.org/Product的样本内容):

doc = """
... <div itemscope itemtype="http://schema.org/Product">
...   <span itemprop="name">Kenmore White 17" Microwave</span>
...   <img src="kenmore-microwave-17in.jpg" alt='Kenmore 17" Microwave' />
...   <div itemprop="aggregateRating"
...     itemscope itemtype="http://schema.org/AggregateRating">
...    Rated <span itemprop="ratingValue">3.5</span>/5
...    based on <span itemprop="reviewCount">11</span> customer reviews
...   </div>
...
...   <div itemprop="offers" itemscope itemtype="http://schema.org/Offer">
...     <span itemprop="price">$55.00</span>
...     <link itemprop="availability" href="http://schema.org/InStock" />In stock
...   </div>
...
...   Product description:
...   <span itemprop="description">0.7 cubic feet countertop microwave.
...   Has six preset cooking categories and convenience features like
...   Add-A-Minute and Child Lock.</span>
...
...   Customer reviews:
...
...   <div itemprop="review" itemscope itemtype="http://schema.org/Review">
...     <span itemprop="name">Not a happy camper</span> -
...     by <span itemprop="author">Ellie</span>,
...     <meta itemprop="datePublished" content="2011-04-01">April 1, 2011
...     <div itemprop="reviewRating" itemscope itemtype="http://schema.org/Rating">
...       <meta itemprop="worstRating" content = "1">
...       <span itemprop="ratingValue">1</span>/
...       <span itemprop="bestRating">5</span>stars
...     </div>
...     <span itemprop="description">The lamp burned out and now I have to replace
...     it. </span>
...   </div>
...
...   <div itemprop="review" itemscope itemtype="http://schema.org/Review">
...     <span itemprop="name">Value purchase</span> -
...     by <span itemprop="author">Lucas</span>,
...     <meta itemprop="datePublished" content="2011-03-25">March 25, 2011
...     <div itemprop="reviewRating" itemscope itemtype="http://schema.org/Rating">
...       <meta itemprop="worstRating" content = "1"/>
...       <span itemprop="ratingValue">4</span>/
...       <span itemprop="bestRating">5</span>stars
...     </div>
...     <span itemprop="description">Great microwave for the price. It is small and
...     fits in my apartment.</span>
...   </div>
...   ...
... </div>
... """
>>>
>>> for scope in sel.xpath('//div[@itemscope]'):
...     print "current scope:", scope.xpath('@itemtype').extract()
...     props = scope.xpath('''
...                 set:difference(./descendant::*/@itemprop,
...                                .//*[@itemscope]/*/@itemprop)''')
...     print "    properties:", props.extract()
...     print
current scope: [u'http://schema.org/Product']
    properties: [u'name', u'aggregateRating', u'offers', u'description', u'review', u'review']

current scope: [u'http://schema.org/AggregateRating']
    properties: [u'ratingValue', u'reviewCount']

current scope: [u'http://schema.org/Offer']
    properties: [u'price', u'availability']

current scope: [u'http://schema.org/Review']
    properties: [u'name', u'author', u'datePublished', u'reviewRating', u'description']

current scope: [u'http://schema.org/Rating']
    properties: [u'worstRating', u'ratingValue', u'bestRating']

current scope: [u'http://schema.org/Review']
    properties: [u'name', u'author', u'datePublished', u'reviewRating', u'description']

current scope: [u'http://schema.org/Rating']
    properties: [u'worstRating', u'ratingValue', u'bestRating']

在这里,我们首先在 itemscope 元素上迭代,对于其中的每一个元素,我们寻找所有的 itemprops 元素,并排除那些本身在另一个 itemscope 内的元素。

1.7 Some XPath tips

1.7.1 谨慎的使用text nodes

当你想要使用文本内容作为XPath函数的参数时,避免使用.//text(),采用.来替代。

这是因为.//text()会产生一个text元素的集合——一个节点集合。当一个node-set被转换成一个string(例如,当它被当做参数传递给contains()或者start-with()函数的时候),它只返回第一个元素。

示例如下:

>>> from scrapy import Selector
>>> sel = Selector(text='<a href="#">Click here to go to the <strong>Next Page</strong></a>')

把一个node-set转化成string:

>>> sel.xpath('//a//text()').extract() # 查看一下node-set
[u'Click here to go to the ', u'Next Page']
>>> sel.xpath("string(//a[1]//text())").extract() #转换成string
[u'Click here to go to the ']

节点被转化成了string,但是,它本身以及子节点都放在了一起。

>>> sel.xpath("//a[1]").extract() # select the first node
[u'<a href="#">Click here to go to the <strong>Next Page</strong></a>']
>>> sel.xpath("string(//a[1])").extract() # convert it to string
[u'Click here to go to the Next Page']

因此,使用.//text()node-set不会得到任何结果:

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

但是,使用.会奏效:

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

1.7.2 注意 //node[1](//node)[1]的区别

  • //node[1] 选择它们的父节点的第一个子节点(occurring first under their respective parents)
  • (//node)[1] 选择文档中的所有node,然后选取其中的第一个

1.7.3 当通过class查询的时候, 考虑使用CSS
因为一个元素可能含有多个CSS class,用XPath的方式选择元素会很冗长:

*[contains(concat(' ', normalize-space(@class), ' '), ' someclass ')]

如果使用@class='someclass'可能会遗漏含有其他class的元素,如果使用contains(@class, 'someclass')去补偿的话,会发现其中包含了多余的含有相同的someclass的元素。

因此,scrapy允许链式使用选择器,因此多数情况下你可以先用CSS选择class,再使用XPath:

>>> from scrapy import Selector
>>> sel = Selector(text='<div class="hero shout"><time datetime="2014-07-23 19:00">Special  date</time></div>')
>>> sel.css('.shout').xpath('./time/@datetime').extract()
[u'2014-07-23 19:00']

这比上面的冗长的XPath不知道高到哪里去了。

2. 关于Selector的详细介绍

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

Selector是对response的封装,用于选取其中的特定内容。

下面是Selector的主要成员变量

  • response 一个HtmlResponse或者XmlResponse对象
  • text 一个unicode字符串或者utf-8文本,当response为空的时候才有效。同时使用text和response是未定义行为
  • type 定义selector的类型,可以是htmlxmlNone(default)

    • 如果type为None,那么selector会根据response自动选择最佳的type,如果定义了text那么默认成html类型
    • response的类型确定:
    • xmlXmlResponse
    • htmlHtmlResponse
    • html:其他类型

    • 如果已经设定了type那么强制使用设定好的type。

主要成员函数

  • xpath() 寻找匹配xpath query 的节点,并返回 SelectorList 的一个实例结果,单一化其所有元素。返回的列表元素也实现了 Selector 的接口。query 是包含XPATH查询请求的字符串。

  • css() 应用给定的CSS选择器,返回 SelectorList 的一个实例。在后台,通过 cssselect 库和运行 .xpath() 方法,CSS查询会被转换为XPath查询

  • extract() 串行化并将匹配到的节点返回一个unicode字符串列表。 结尾是编码内容的百分比
  • reg(regex) 应用给定的regex,并返回匹配到的unicode字符串列表。regex 可以是一个已编译的正则表达式,也可以是一个将被 re.compile(regex) 编译为正则表达式的字符串。
  • register_namespaces(prefix, uri) 注册给定的命名空间,其将在 Selector 中使用。 不注册命名空间,你将无法从非标准命名空间中选择或提取数据。
  • remove_namespaces() 移除所有的命名空间,允许使用少量的命名空间xpaths遍历文档
  • __nonzero__() 如果选择了任意的真实文档,将返回 True ,否则返回 False 。 也就是说, Selector 的布尔值是通过它选择的内容确定的。

SelectorList对象

class scrapy.selector.SelectorList

SelectorList 类是内建 list 类的子类,提供了一些额外的方法。

  • xpath(query) 对列表中的每个元素调用 .xpath() 方法,返回结果为另一个单一化的 SelectorList
  • css(query) 对列表中的各个元素调用 .css() 方法,返回结果为另一个单一化的 SelectorList
  • extract() 对列表中的各个元素调用 .extract() 方法,返回结果为单一化的unicode字符串列表
  • re() 对列表中的各个元素调用 .re() 方法,返回结果为单一化的unicode字符串列表
  • __nonzero__() 列表非空则返回True,否则返回False

在XML响应上的选择器样例
我们假设已经有一个通过 XmlResponse 对象实例化的 Selector ,如下:

sel = Selector(xml_response)

选择所有的 元素,返回SelectorList :

sel.xpath(“//product”)

从 Google Base XML feed 中提取所有的价钱,这需要注册一个命名空间:

sel.register_namespace("g", "http://base.google.com/ns/1.0")
sel.xpath("//g:price").extract()

移除命名空间

在处理爬虫项目时,可以完全去掉命名空间而仅仅处理元素名字,这样在写更多简单/实用的XPath会方便很多。为此可以使用Selector.remove_namespaces()方法。

让我们来看一个例子,以Github博客的atom订阅来解释这个情况。

首先,我们使用想爬取的url来打开shell:

scrapy shell https://github.com/blog.atom

一旦进入shell,我们可以尝试选择所有的 <link> 对象,可以看到没有结果(因为Atom XML命名空间混淆了这些节点):

>>> response.xpath("//link")
[]

但一旦我们调用 Selector.remove_namespaces() 方法,所有的节点都可以直接通过他们的名字来访问:

>>> response.selector.remove_namespaces()
>>> response.xpath("//link")  
[<Selector xpath='//link' data=u'<link xmlns="http://www.w3.org/2005/Atom'>,
<Selector xpath='//link' data=u'<link xmlns="http://www.w3.org/2005/Atom'>,
...

如果你对为什么命名空间移除操作并不总是被调用,而需要手动调用有疑惑。这是因为存在如下两个原因,按照相关顺序如下:

  1. 移除命名空间需要迭代并修改文件的所有节点,而这对于Scrapy爬取的所有文档操作需要一定的性能消耗
  2. 会存在这样的情况,确实需要使用命名空间,但有些元素的名字与命名空间冲突。尽管这些情况非常少见。

xpath解析带命名空间页面的方法:

If the XPath expression does not include a prefix, it is assumed that the namespace URI is the empty namespace. If your XML includes a default namespace, you must still add a prefix and namespace URI to the XmlNamespaceManager; otherwise, you will not get any nodes selected

上面一段话的意思就是:如果XPath没有指定命名空间的话,那么它的命名空间为空。如果待解析XML文件含有默认命名空间的话,那么你必须添加那个命名空间的前缀,并且把命名空间的URI添加到XmlNamespaceManager中,否则,你得不到任何查询结果。

对于scrapy,这里提供了register_namespaces(prefix, uri)remove_namespaces()两个函数来解决这个问题。

 类似资料: