Skip to main content

选择器

当您抓取网页时,您需要执行的最常见任务是从HTML源中提取数据。有几个库可以实现这一点:

  • BeautifulSoup 是Python程序员中非常流行的网络抓取库,它基于HTML代码的结构构建一个Python对象,并且处理错误标记相当好,但它有一个缺点:它很慢。

  • lxml 是一个基于 ElementTree 的Python解析库(它还解析HTML)与一个pythonic API。 (lxml不是Python标准库的一部分。)

Scrapy自带了提取数据的机制。它们称为选择器,因为它们“选择”由 XPathCSS 表达式指定的HTML文档的某些部分。

XPath 是用于选择XML文档中的节点的语言,其也可以与HTML一起使用。 CSS 是一种用于将样式应用于HTML文档的语言。它定义了选择器以将这些样式与特定的HTML元素相关联。

Scrac选择器构建在 lxml 库上,这意味着它们的速度和解析精度非常相似。

这个页面解释了选择器是如何工作的,并描述了他们的API是非常小和简单,不像 lxml API是更大,因为 lxml 库可以用于许多其他任务,除了选择标记文档。

有关选择器API的完整参考,请参阅 选择器引用

使用选择器

构建选择器

Scrapy选择器是通过传递 文本TextResponse 对象构建的 Selector 类的实例。它根据输入类型自动选择最佳的解析规则(XML与HTML):

>>> from scrapy.selector import Selector
>>> from scrapy.http import HtmlResponse

从文本构造:

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

构建响应:

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

为了方便起见,响应对象公开了 .selector 属性上的选择器,在可能的情况下使用此快捷键是完全正确的:

>>> response.selector.xpath('//span/text()').extract()
[u'good']

使用选择器

为了解释如何使用选择器,我们将使用 Scrapy shell (提供交互式测试)和位于Scrapy文档服务器中的示例页面:

这里是它的HTML代码:

<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 属性中。

由于我们处理HTML,选择器将自动使用HTML解析器。

所以,通过查看该页面的 HTML代码,让我们构造一个XPath来选择标题标签内的文本:

>>> response.selector.xpath('//title/text()')
[<Selector (text) xpath=//title/text()>]

使用XPath和CSS查询响应是如此常见,以致响应包括两个方便的快捷方式:response.xpath()response.css():

>>> response.xpath('//title/text()')
[<Selector (text) xpath=//title/text()>]
>>> response.css('title::text')
[<Selector (text) xpath=//title/text()>]

如您所见,.xpath().css() 方法返回一个 SelectorList 实例,它是一个新的选择器列表。此API可用于快速选择嵌套数据:

>>> response.css('img').xpath('@src').extract()
[u'image1_thumb.jpg',
 u'image2_thumb.jpg',
 u'image3_thumb.jpg',
 u'image4_thumb.jpg',
 u'image5_thumb.jpg']

要实际提取文本数据,必须调用选择器 .extract() 方法,如下所示:

>>> response.xpath('//title/text()').extract()
[u'Example website']

如果只想提取第一个匹配的元素,可以调用选择器 .extract_first()

>>> response.xpath('//div[@id="images"]/a/text()').extract_first()
u'Name: My image 1 '

如果没有找到元素,则返回 None

>>> response.xpath('//div[@id="not-exists"]/text()').extract_first() is None
True

可以提供默认返回值作为参数,以代替 None

>>> response.xpath('//div[@id="not-exists"]/text()').extract_first(default='not-found')
'not-found'

请注意,CSS选择器可以使用CSS3伪元素选择文本或属性节点:

>>> response.css('title::text').extract()
[u'Example website']

现在我们要获取基本URL和一些图像链接:

>>> response.xpath('//base/@href').extract()
[u'http://example.com/']

>>> response.css('base::attr(href)').extract()
[u'http://example.com/']

>>> response.xpath('//a[contains(@href, "image")]/@href').extract()
[u'image1.html',
 u'image2.html',
 u'image3.html',
 u'image4.html',
 u'image5.html']

>>> response.css('a[href*=image]::attr(href)').extract()
[u'image1.html',
 u'image2.html',
 u'image3.html',
 u'image4.html',
 u'image5.html']

>>> response.xpath('//a[contains(@href, "image")]/img/@src').extract()
[u'image1_thumb.jpg',
 u'image2_thumb.jpg',
 u'image3_thumb.jpg',
 u'image4_thumb.jpg',
 u'image5_thumb.jpg']

>>> response.css('a[href*=image] img::attr(src)').extract()
[u'image1_thumb.jpg',
 u'image2_thumb.jpg',
 u'image3_thumb.jpg',
 u'image4_thumb.jpg',
 u'image5_thumb.jpg']

嵌套选择器

选择方法(.xpath().css())返回相同类型的选择器的列表,因此您也可以调用这些选择器的选择方法。这里有一个例子:

>>> links = response.xpath('//a[contains(@href, "image")]')
>>> links.extract()
[u'<a href="image1.html">Name: My image 1 <br><img src="image1_thumb.jpg"></a>',
 u'<a href="image2.html">Name: My image 2 <br><img src="image2_thumb.jpg"></a>',
 u'<a href="image3.html">Name: My image 3 <br><img src="image3_thumb.jpg"></a>',
 u'<a href="image4.html">Name: My image 4 <br><img src="image4_thumb.jpg"></a>',
 u'<a href="image5.html">Name: My image 5 <br><img src="image5_thumb.jpg"></a>']

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

Link number 0 points to url [u'image1.html'] and image [u'image1_thumb.jpg']
Link number 1 points to url [u'image2.html'] and image [u'image2_thumb.jpg']
Link number 2 points to url [u'image3.html'] and image [u'image3_thumb.jpg']
Link number 3 points to url [u'image4.html'] and image [u'image4_thumb.jpg']
Link number 4 points to url [u'image5.html'] and image [u'image5_thumb.jpg']

使用带有正则表达式的选择器

Selector 还有一个 .re() 方法用于使用正则表达式提取数据。但是,与使用 .xpath().css() 方法不同,.re() 返回一个unicode字符串列表。所以你不能构造嵌套的 .re() 调用。

下面是一个用来从上面的 HTML代码 中提取图像名称的例子:

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

有一个额外的助手往复式 .extract_first().re(),命名为 .re_first()。使用它只提取第一个匹配的字符串:

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

使用相对XPath

请记住,如果您嵌套选择器并使用以 / 开头的XPath,该XPath将是绝对的,而不是相对于您调用它的 Selector

例如,假设要提取 <div> 元素内的所有 <p> 元素。首先,你会得到所有的 <div> 元素:

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

首先,你可能会使用以下方法,这是错误的,因为它实际上从文档中提取所有 <p> 元素,而不仅仅是那些在 <div> 元素:

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

有关相对XPath的更多详细信息,请参阅XPath规范中的 Location Paths 部分。

XPath表达式中的变量

XPath允许您使用 $somevariable 语法来引用XPath表达式中的变量。这在某种程度上类似于SQL世界中的参数化查询或预准备语句,其中,您使用诸如 ? 的占位符替换查询中的一些参数,然后使用查询传递的值替换它们。

这里有一个例子来匹配元素基于它的“id”属性值,没有硬编码它(以前显示):

>>> # `$val` used in the expression, a `val` argument needs to be passed
>>> response.xpath('//div[@id=$val]/a/text()', val='images').extract_first()
u'Name: My image 1 '

这里有另一个例子,要找到包含五个 <a> 子节点的 <div> 标签的“id”属性(这里我们将值 5 作为整数传递):

>>> response.xpath('//div[count(a)=$cnt]/@id', cnt=5).extract_first()
u'images'

所有的变量引用在调用 .xpath() 时必须有一个绑定值(否则你会得到一个 ValueError: XPath error: 异常)。这是通过传递必要的命名参数。

parsel 是对Scrapy选择器提供动力的库,有更多的细节和 XPath variables 的例子。

使用EXSLT扩展

在构建在 lxml 之上时,Scrapy选择器还支持一些 EXSLT 扩展,并且带有这些预先注册的命名空间,以在XPath表达式中使用:

字首

命名空间

用法

回覆

http://exslt.org/regular-expressions

regular expressions

http://exslt.org/sets

set manipulation

正则表达式

例如,当XPath的 starts-with()contains() 不足时,test() 函数可以证明是非常有用的。

示例选择列表项中的链接,其中“类”属性以数字结尾:

>>> 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()
[u'link1.html', u'link2.html', u'link3.html', u'link4.html', u'link5.html']
>>> sel.xpath('//li[re:test(@class, "item-\d$")]//@href').extract()
[u'link1.html', u'link2.html', u'link4.html', u'link5.html']
>>>

警告

C库 libxslt 本身不支持EXSLT正则表达式,因此 lxml 的实现使用到Python的 re 模块的钩子。因此,在XPath表达式中使用regexp函数可能会增加小的性能损失。

设置操作

这些可以方便地在提取文本元素之前排除文档树的部分。

从项目范围组和相应的itemprops组中提取微数据(从 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>
... """
>>> sel = Selector(text=doc, type="html")
>>> 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 本身。

一些XPath提示

这里有一些提示,您可能会发现有用的,当使用XPath与Scrapy选择器,基于 this post from ScrapingHub’s blog。如果你不太熟悉XPath,你可能想先看看这个 XPath tutorial

在条件中使用文本节点

当您需要使用文本内容作为 XPath string function 的参数时,请避免使用 .//text(),而仅使用 .

这是因为表达式 .//text() 产生文本元素的集合–node-set。当一个节点集被转换为一个字符串,当它作为参数传递给一个字符串函数,如 contains()starts-with() 时,会导致第一个元素的文本。

例:

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

node-set 转换为字符串:

>>> sel.xpath('//a//text()').extract() # take a peek at the node-set
[u'Click here to go to the ', u'Next Page']
>>> sel.xpath("string(//a[1]//text())").extract() # convert it to string
[u'Click here to go to the ']

然而,转换为字符串的 node 将自身的文本加上其所有后代:

>>> 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() 节点集将不会选择任何内容:

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

注意//node[1]和(//node)[1]之间的区别

//node[1] 选择在其各自父亲下首先出现的所有节点。

(//node)[1] 选择文档中的所有节点,然后只获取其中的第一个节点。

例:

>>> from scrapy import Selector
>>> sel = Selector(text="""
....:     <ul class="list">
....:         <li>1</li>
....:         <li>2</li>
....:         <li>3</li>
....:     </ul>
....:     <ul class="list">
....:         <li>4</li>
....:         <li>5</li>
....:         <li>6</li>
....:     </ul>""")
>>> xp = lambda x: sel.xpath(x).extract()

这将获得所有第一个 <li> 元素,无论它是其父:

>>> xp("//li[1]")
[u'<li>1</li>', u'<li>4</li>']

这获得了整个文档中的第一个 <li> 元素:

>>> xp("(//li)[1]")
[u'<li>1</li>']

这将获得 <ul> 父下的所有第一个 <li> 元素:

>>> xp("//ul/li[1]")
[u'<li>1</li>', u'<li>4</li>']

这将获得整个文档中 <ul> 父下的第一个 <li> 元素:

>>> xp("(//ul/li)[1]")
[u'<li>1</li>']

当按类查询时,请考虑使用CSS

因为一个元素可以包含多个CSS类,XPath方法通过类选择元素是相当冗长:

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

如果你使用 @class='someclass',你可能会缺少有其他类的元素,如果你只是使用 contains(@class, 'someclass') 来弥补,你可能会得到更多的你想要的元素,如果他们有一个不同的类名称共享字符串 someclass

事实证明,Scrapy选择器允许你链选择器,所以大多数时候你可以只使用CSS选择类,然后在需要时切换到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技巧更清晰。只记得在随后的XPath表达式中使用 .

内置选择器参考

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

Selector 的一个实例是响应选择其内容的某些部分的包装器。

response 是将用于选择和提取数据的 HtmlResponseXmlResponse 对象。

对于 response 不可用的情况,text 是Unicode字符串或utf-8编码文本。一起使用 textresponse 是未定义的行为。

type 定义选择器类型,它可以是 "html""xml"None (默认)。

如果 typeNone,则选择器将根据 response 类型自动选择最佳类型(见下文),或者在与 text 一起使用时默认为 "html"

如果 typeNone 并且传递了 response,则从响应类型推断选择器类型如下:

否则,如果设置了 type,则强制选择器类型,不会发生检测。

xpath(query)

查找与xpath query 匹配的节点,并将结果作为 SelectorList 实例返回,并将所有元素展平。列表元素实现 Selector 接口。

query 是一个包含要应用的XPATH查询的字符串。

注解

为了方便起见,这种方法可以称为 response.xpath()

css(query)

应用给定的CSS选择器并返回一个 SelectorList 实例。

query 是一个包含要应用的CSS选择器的字符串。

在后台,CSS查询使用 cssselect 库转换为XPath查询,并运行 .xpath() 方法。

注解

为了方便起见,该方法可以称为 response.css()

extract()

序列化并返回匹配的节点作为unicode字符串列表。编码内容的百分比未引用。

re(regex)

应用给定的正则表达式并返回一个包含匹配项的unicode字符串的列表。

regex 可以是编译的正则表达式,也可以是将使用 re.compile(regex) 编译为正则表达式的字符串

注解

注意,re()re_first() 都解码HTML实体(除了 &lt;&amp;)。

register_namespace(prefix, uri)

注册要在此 Selector 中使用的给定命名空间。如果不注册命名空间,则无法从非标准命名空间中选择或提取数据。参见下面的例子。

remove_namespaces()

删除所有命名空间,允许使用无命名空间的xpaths遍历文档。参见下面的例子。

__nonzero__()

如果选择了任何实际内容,则返回 True,否则返回 False。换句话说,Selector 的布尔值由它选择的内容给出。

SelectorList对象

class scrapy.selector.SelectorList

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

xpath(query)

为此列表中的每个元素调用 .xpath() 方法,并将其结果作为另一个 SelectorList 展平。

query 是与 Selector.xpath() 中的参数相同的参数

css(query)

为此列表中的每个元素调用 .css() 方法,并将其结果作为另一个 SelectorList 展平。

query 是与 Selector.css() 中的参数相同的参数

extract()

为此列表中的每个元素调用 .extract() 方法,并将其结果作为unicode字符串列表返回扁平。

re()

为此列表中的每个元素调用 .re() 方法,并将其结果作为unicode字符串列表返回扁平。

__nonzero__()

如果列表不为空,则返回True,否则返回False。

HTML响应的选择器示例

这里有几个 Selector 例子来说明几个概念。在所有情况下,我们假设已经有一个用这样的 HtmlResponse 对象实例化的 Selector:

sel = Selector(html_response)
  1. 从HTML响应主体中选择所有 <h1> 元素,返回 Selector 对象的列表(即 SelectorList 对象):

    sel.xpath("//h1")
    
  2. 从HTML响应正文中提取所有 <h1> 元素的文本,返回unicode字符串列表:

    sel.xpath("//h1").extract()         # this includes the h1 tag
    sel.xpath("//h1/text()").extract()  # this excludes the h1 tag
    
  3. 迭代所有 <p> 标签并打印它们的类属性:

    for node in sel.xpath("//p"):
        print node.xpath("@class").extract()
    

XML响应的选择器示例

这里有几个例子来说明几个概念。在这两种情况下,我们假设已经有一个 Selector 用这样的 XmlResponse 对象实例化:

sel = Selector(xml_response)
  1. 从XML响应主体中选择所有 <product> 元素,返回 Selector 对象的列表(即 SelectorList 对象):

    sel.xpath("//product")
    
  2. 从需要注册命名空间的 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博客原子进行说明。

首先,我们打开shell,使用我们想要删除的url:

$ 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. 可能有一些情况下,实际上需要使用命名空间,以防某些元素名称在命名空间之间冲突。这些情况非常罕见。