Spider爬虫框架之Selectors

Selectors

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

  • BeautifulSoup是Python程序员中非常流行的网页抓取库,它基于HTML代码的结构构建了一个Python对象,同时也很好地处理了坏标记,但它有一个缺点:速度很慢。
  • lxml是一个基于ElementTree的pythonic API的XML解析库(它也解析HTML)。 (lxml不是Python标准库的一部分。

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

XPath是一种用于选择XML文档中的节点的语言,它也可以用于HTML。 

CSS是一种将样式应用于HTML文档的语言。 它定义选择器将这些样式与特定的HTML元素相关联。

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


使用Selectors

下面我们用Scrapy的Shell演示如何使用Selectors。

这是Scrapy官网的一个示例网页,地址https://doc.scrapy.org/en/latest/_static/selectors-sample1.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 https://doc.scrapy.org/en/latest/_static/selectors-sample1.html

然后,加载Shell之后,你将得到Shell下可用的一个变量response并且它附加了一个选择器在response.selector属性中。

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

通过查看该页面的HTML代码,我们构建一个用于选择标题标签内的文本的XPath:

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

response对象有两个方法 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

可以在参数default中替换掉这个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() or .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']

使用正则表达式的选择器

选择器还有一个.re() 方法,用于使用正则表达式提取数据。 但是,与使用 .xpath() or .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'

使用相对的XPaths

请记住,如果您嵌套选择器并使用以/开头的XPath,那么XPath对于文档来说绝对是绝对的,而不是相对于您从中调用的Selector。

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

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

起初,您可能会尝试使用以下方法,这是错误的,因为它实际上会从文档中提取所有<p>元素,而不仅仅是<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()

有关相关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 '

再举一个其他例子, 为了找到包含5个<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表达式:

prefix namespace usage
re http://exslt.org/regular-expressions regular expressions
set http://exslt.org/sets set manipulation

正则表达式

例如,当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()
[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’s re 模块中。 因此,,在XPath表达式中使用正则表达式函数会增加一点性能损失

Set 操作

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

使用itemscopes组和相应的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技巧

以下是一些技巧,您可能发现在使用Scrapy选择器的XPath时有用, 基于ScrapingHub’s blog的这篇文章。如果您还不熟悉XPath,那您可能需要先看看这个 XPath tutorial.

在条件中使用文本节点

当您需要使用文本内容作为XPath字符串函数的参数时,请避免使用 .//text() and use just . instead.

这是因为表示式 .//text() 产生了一组文本元素——一个节点集合。当一个节点集合被转换为一个字符串作为参数传递给像 contains() 或 starts-with()这样的字符串函数时,仅仅只会返回第一个元素。

例如:

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

节点集合转为字符串:

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

然而,一个转换为字符串的节点将自己的文本加上其所有后代:

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

B但使用 . 来指这个节点,就可以:

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

按class查询的时候, 请考虑使用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技巧更清晰。 只记得使用 . 。

参考链接:https://doc.scrapy.org/en/latest/topics/selectors.html


猜你喜欢

转载自blog.csdn.net/u014108439/article/details/79762587