跳至主要內容

Scrapy爬虫

CoderAn大约 29 分钟后端开发PythonScrapy

Scrapy爬虫

官方文档open in new window

Spiders

通用爬虫(Generic Spider)

Scrapy内置了一些通用的爬虫基类,你可以通过继承这些基类来快速构建自己的爬虫。这些内置爬虫基类提供了许多常用功能,比如:通过指定的规则,sitemaps或者xml/csv格式的feed文件爬取网站的链接。
接下来的例子,假定你已经创建了scrapy项目,在items.py 中申明TestItem类:

import scrapy
class TestItem(scrapy.Item):
    id = scrapy.Field()
    name = scrapy.Field()
    description = scrapy.Field()

CrawlSpider

class scrapy.spiders.CrawlSpider

这是爬取正规网站最常用的爬虫,它通过一系列的规则为跟踪网站链接提供方便的机制。对于特殊的网站或项目它或许不是最适合的,但对于常用的网站足矣。对于特殊的功能,你可以继承它,然后重载自定义部分即可,或者用它来实现你自己的爬虫。
与从spider继承的属性(需要指定)不同的是,该类支持一个特殊的属性:

rules

Rule对象的一个或多个列表,每个Rule为爬取网站定义了明确的行为。Rules Objects将在后面的部分进行介绍。如果多个Rule匹配了同一个链接,只有第一个匹配生效,取决于它们在改属性中的顺序。

CrawlSpider还暴露了一个可重写的方法:

parse_start_url(response)

这个方法将被start_urls的响应调用,它允许解析初始的responses,必须返回一个Item对象或一个Request对象,或者一个包含这2个对象的可迭代类型。

Crawling rules

class scrapy.spiders.Rule(link_extractor,callback=None,cb_kwargs=None,follow=None,process_links=None,process_request=None)   

link_extractor是一个Link Extractoropen in new window对象,用于提取爬取到的页面中的链接。
callback每个提取到的链接调用的函数或字符串,这个callback的第一个参数是链接的response,必须返回一个Item或Request对象。

注意:不能将parse作为callback,因为CrawlSpider使用parse作为它处理逻辑的默认方法,如果重写,crawl spider讲不能正常工作。

cb_kwargscallback的参数 follow布尔类型,控制是否爬取页面中提取到的链接。如果没有callback,follow的默认值为True,否则默认False process_links功能类似callback,主要用于过滤 process_request 被匹配本条Rule的request调用,必须返回一个request或者None

CrawlSpider例子

import scrapy
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor

class MySpider(CrawlSpider):
    name = 'example.com'
    allowed_domains = ['example.com']
    start_urls = ['http://www.example.com']

    rules = (
        # Extract links matching 'category.php' (but not matching 'subsection.php')
        # and follow links from them (since no callback means follow=True by default).
        Rule(LinkExtractor(allow=('category\.php', ), deny=('subsection\.php', ))),

        # Extract links matching 'item.php' and parse them with the spider's method parse_item
        Rule(LinkExtractor(allow=('item\.php', )), callback='parse_item'),
    )

    def parse_item(self, response):
        self.logger.info('Hi, this is an item page! %s', response.url)
        item = scrapy.Item()
        item['id'] = response.xpath('//td[@id="item_id"]/text()').re(r'ID: (\d+)')
        item['name'] = response.xpath('//td[@id="item_name"]/text()').extract()
        item['description'] = response.xpath('//td[@id="item_description"]/text()').extract()
        return item

这个爬虫将爬取example.com的首页,收集category和item页面的链接,item页面的链接使用parse_item方法解析,每个response将使用xpath提取数据,并返回一个Item对象。

XMLFeedSpider

class scrapy.spiders.XMLFeedSpider

XMLFeedSpider被设计用来通过迭代指定的节点解析xml格式的订阅内容。解析器可选:iternodes,xml和html。出于性能考虑,推荐使用iternodes,因为xml和html为了解析一次性生成了整个DOM。然而,当解析的XML标记有问题时,使用html作为解析器是不错的选择。
通过设置以下类属性来设置解析器和标签名:
iterator 字符串,定义解析器。

  • 'iternodes' 基于正则表达式的快速解析器,默认。
  • 'html' 使用Selector的解析器,将所有Dom加载进内存,然后通过Dom解析。
  • 'xml' 同html

itertag 定义解析的节点

    itertag = 'product'

namespaces 包含(prefix,uri)的列表,prefix和uri将会通过调用register_namespace()方法注册。 然后,你就可以在itertag中通过命名空间指定节点.

class Spider(XMLFeedSpider):
    namespaces = [('n', 'http://www.sitemaps.org/schemas/sitemap/0.9')]
    itertag = 'n:url'

可重写的方法:

adapt_response(response) 在response到达spider中间件后,spider解析前,对response body进行修改,返回一个新的reponse或者原来的response

parse_node(response, selector) 被itertag匹配的nodes调用,接收node的response和Selector,这个方法是必须重写的,否则,spider将不能正常工作。 返回一个Item对象或Request对象,或者包含它们的可迭代内容。

process_results(response, results) 被spider返回的结果调用,用于处理返回至框架核心前的最后一次请求,比如:设置item的ID。它接收一个结果的列表和生成这些结果的response。它必须返回一个结果的列表。

XMLFeedSpider 例子:

from scrapy.spiders import XMLFeedSpider
from myproject.items import TestItem

class MySpider(XMLFeedSpider):
    name = 'example.com'
    allowed_domains = ['example.com']
    start_urls = ['http://www.example.com/feed.xml']
    iterator = 'iternodes'  # This is actually unnecessary, since it's the default value
    itertag = 'item'

    def parse_node(self, response, node):
        self.logger.info('Hi, this is a <%s> node!: %s', self.itertag, ''.join(node.extract()))

        item = TestItem()
        item['id'] = node.xpath('@id').extract()
        item['name'] = node.xpath('name').extract()
        item['description'] = node.xpath('description').extract()

spider下载start_urls中给定的feed文档,然后通过itertag标签迭代节点item,输出并存储随机数据到Item中。

CSVFeedSpider

class scrapy.spiders.CSVFeedSpider

CSVFeedSpider和XMLFeedSpider很像,除了迭代的对象是rows而不是nodes。每个迭代过程中调用parse_row()。

delimiter

每个字段的分隔符,在CSV文件中默认是','

quotechar

包含每个字段的符号,在CSV中默认是'"'

headers

CSV文件的列名,一个列表[]

parse_row(response,row)

接收一个reponse参数和一个字典(代表每一行,键是csv文件的列名)。

CSVFeedSpider也可重写adapt_response 和 process_results方法。

实例:

from scrapy.spiders import CSVFeedSpider
from myproject.items import TestItem

class MySpider(CSVFeedSpider):
    name = 'example.com'
    allowed_domains = ['example.com']
    start_urls = ['http://www.example.com/feed.csv']
    delimiter = ';'
    quotechar = "'"
    headers = ['id', 'name', 'description']

    def parse_row(self, response, row):
        self.logger.info('Hi, this is a row!: %r', row)

        item = TestItem()
        item['id'] = row['id']
        item['name'] = row['name']
        item['description'] = row['description']
        return item

SitemapSpider

class scrapy.spiders.SitemapSpider

SitemapSpider允许你根据网站的sitemaps进行爬取内容。它支持嵌套的sitemaps和从robots.txt文件中发现sitemap。

sitemap_urls

一个指向sitemaps的列表,包含了爬虫将要爬取得urls,也可指向robots.txt文件,它将从中提取出sitemap的urls

sitemap_rules

一个(regex,callback)的列表: regex: 从sitemaps中匹配urls的正则表达式,regex可以是字符串或编译的正则表达式对象。
callback:字符串或可调用的函数

sitemap_rules = [('/product/', 'parse_product')]

规则按顺序匹配,选择匹配到的第一个。如果省略该属性,sitemap_urls中的所有urls将被parse处理。

sitemap_follow

需要被跟踪的sitemap的正则表达式列表。仅对使用sitemap文件指向其他sitemap文件的站点有效。
默认的所有站点将被followed。

sitemap_alternate_links

指定 alternate links是否应该被followed。alternate links是指向同一网站的不同语言链接,使用的同一个url标记。 如:

<url>
    <loc>http://example.com/</loc>
    <xhtml:link rel="alternate" hreflang="de" href="http://example.com/de"/>
</url>

如果配置了sitemap_alternate_links,将会检测http://example.com/和http://example.com/de。 如果sitemap_alternate_links 为disabled,将只会检测http://example.com/,默认为disabled。

例子:

//解析sitemaps中的所有url

from scrapy.spiders import SitemapSpider

class MySpider(SitemapSpider):
    sitemap_urls = ['http://www.example.com/sitemap.xml']

    def parse(self, response):
        pass # ... scrape item here ...

//通过sitemap_rules分别指定不同的链接的解析
from scrapy.spiders import SitemapSpider

class MySpider(SitemapSpider):
    sitemap_urls = ['http://www.example.com/sitemap.xml']
    sitemap_rules = [
        ('/product/', 'parse_product'),
        ('/category/', 'parse_category'),
    ]

    def parse_product(self, response):
        pass # ... scrape product ...

    def parse_category(self, response):
        pass # ... scrape category ...
//通过robots.txt文件跟踪sitemaps,并且只跟踪包含/sitemap_shop的链接

from scrapy.spiders import SitemapSpider

class MySpider(SitemapSpider):
    sitemap_urls = ['http://www.example.com/robots.txt']
    sitemap_rules = [
        ('/shop/', 'parse_shop'),
    ]
    sitemap_follow = ['/sitemap_shops']

    def parse_shop(self, response):
        pass # ... scrape shop here ...

结合SitemapSpider和其他的urls

from scrapy.spiders import SitemapSpider

class MySpider(SitemapSpider):
    sitemap_urls = ['http://www.example.com/robots.txt']
    sitemap_rules = [
        ('/shop/', 'parse_shop'),
    ]

    other_urls = ['http://www.example.com/about']

    def start_requests(self):
        requests = list(super(MySpider, self).start_requests())
        requests += [scrapy.Request(x, self.parse_other) for x in self.other_urls]
        return requests

    def parse_shop(self, response):
        pass # ... scrape shop here ...

    def parse_other(self, response):
        pass # ... scrape other here ...

Selectors

当你爬取网站页面的时候,最常见的事情就是从html中提取数据。下面介绍几个这方面的库:

  • BeautifulSoup 在python开发者中非常流行的爬虫库,通过html结构创建python对象,能够合理的处理坏标签。但是它有一个缺点:慢。
  • lxml xml解析库,也能解析html,通过一个pythonic的API:ElementTree。lxml不是python标准库的一部分。

Scrapy有自己的提取数据的机制,被称为selectors,因为它通过XPath或CSS表达式选择HTML中指定部分。

XPath是一重在XML文档中选择nodes的语言,也能够用于html。CSS是应用于html样式的语言。它定义selectors关联到那些指定样式的html元素。

Scrapy selectors是建立在lxml库上的,这意味着它们在速度和解析准确度上非常相似。

下面介绍selectors是如何工作的,以及描述它小而简单的API,不同于lxml API,lxml API非常大,因为它被用来实现很多任务,不仅仅是选择文档标记。

完整的Selectoropen in new window参考

使用Selectors

构建Selectors Scrapy selectors是Selector类的实例,通过传入text或TextResponse对象生成。它根据输入的类型自动选择最合适的解析规则(XML vs HTML)。

>>from scrapy.selector import Selector
>>from scrapy.http import HtmlResponse
//通过text构建
>>body = '<html><body><span>good</span></body></html>'
>>Selector(text=body).xpath('//span/text()').extract()
[u'good']
//从response构建
>>response = HtmlResponse(url='http://example.com',body=body)
>>Selector(response=response).xpath('//span/text()').extract()
[u'good]

为了更便捷,response暴露了一个属性.selector,完全等价于上面的操作:

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

使用 为了解释怎么使用selectors,我们接下来将使用Scrapy shell和Scrapy服务器上的页面做实验:
官网链接open in new window

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

然后,等待加载完成,你可以通过response变量获取响应结果,并且包含了response.selector属性。
因为我们处理的HTML,选择器自动使用HTML parser。 查看HTML页面代码,让我们一起构建一个XPath来获取title标记中的文本:

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

因为查询响应使用XPath和CSS太常用了,所以reponses提供了2个便利的方式:response.xpath() 和 response.css():

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

如你所见,.xpath()和.css()方法返回一个selector实例的列表,使用这个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,可通过传递一个参数来设置默认值取代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()返回一些相同类型的selectors,因此你能为这些selectors调用selection的方法。

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

提取匹配的第一个字符串:.re_first()

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

使用相对路径的XPaths 如果你使用'/'开头的XPath进行嵌套选择,那么XPath将是相对于文档的绝对路径,而不是上一层选择器的路径。比如,你想选择出所有div下的p元素:

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

接下来,你可能想这样提取p元素:

>>[p.extract() for p in div.xpath('//p')]

但是,这是错误的,这样将提取到所有的p元素。应该这样来写:

>> [p.extract() for p in div.xpath('.//p')]

更多关于XPath相对路径open in new window的资料

XPath表达式中的变量 XPath允许在表达式中使用变量,语法:$var。 下面通过id属性的值来匹配元素,没有硬编码。

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

另外一个例子:查找含有5个a标签的div的id

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

所有引用的变量都必须赋值,不然xpath将报错。 更多关于XPath Variableopen in new window

使用EXSLT扩展

建立在lxml上,Scrapy选择器也支持EXSLT扩展,内置了一些预先注册好的命名空间可以在XPath表达式中使用。

prefixnamespaceusage
rehttp://exslt.org/regular-expressionsregular expressions
sethttp://exslt.org/setsset manipulation

正则表达式

当XPath的starts-with()或contains()不够用的时候,test()函数将会非常有用。 例如:从class以数字结尾的list的元素下提取链接。

>>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//a/@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']

设置操作 可以方便的在提取文本元素前排除部分dom元素。 例如提取item的范围和相对应的属性

>>> 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 category 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元素,针对每个itemscope查找itemprops并排除那些包含在其他itemscop中的itemprops。

关于XPath的一些建议一篇来自ScrapingHub的博客open in new windowXPath文档open in new window

使用text节点的时候需要注意,当你将text内容作为参数传递给XPath string function时,避免使用.//text(),用.代替即可。 因为表达式.//text()产生一个text元素的集合node-set,当node-set转换为string的时候,比如传递给contains()或starts-width(),将导致只会传递第一个元素的值。

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

将一个node节点转换为string,将提取出该节点下的所有text

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

所以,选择含有Next Page文本的a标签,如果这样写:

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

将不会有任何输出,因为原意是想先获取a的所有文本,然后检测是否包含'Next Page', 但是此处的.//text()转换为string只会输出[u'Click here to go to the '],并不包含'Next Page'。所以,应该这样写:

>> 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()
>>> xp("//li[1]")
[u'<li>1</li>', u'<li>4</li>']
>>> xp("(//li)[1]")
[u'<li>1</li>']
>>> xp("//ul/li[1]")
[u'<li>1</li>', u'<li>4</li>']
>>> xp("(//ul/li)[1]")
[u'<li>1</li>']

使用class查询时,考虑使用CSS
因为一个元素能够包含多个css,XPath选择元素相对css来说是很冗长的:

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

如果你使用@class='someclass',将会丢失许多含有其他class的元素,如果使用contains(@class,'someclass'),将有可能包含多余的class含有someclass子字符串的元素。 Scrapy允许链式调用selectors,所以你可以先用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']

内置的Selectors参考

Selector 对象

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

Selector的实例是包装在response上用于提取指定的内容。
response:一个 HtmlResponseXmlResponse对象,用于选择和导出数据。
text:unicode字符串或utf-8编码的文本,当response不存在的情况下。同时使用textresponse是不可行的。
type:定义选择器类型:"html","xml"或"None"(默认)

如果typeNone时,选择器自动根据response选择最合适的类型,或如果类型为text时将默认为"html"
如果typeNone,传递了response,选择器类型将从response类型中推算:

  • "html"HtmlResponse
  • "xml"XmlResponse
  • "html":anything

否则,如果type设定了,选择器类型将被强制指定为type,不再进行检测。

xpath(query)

css(query)

extract()

re(regex)

register_namespace(prefix,url)

remove_namespaces()

nonzero() 是否选中内容

SelectorList objects

class scrapy.selector.SelectorList

SelectorList类是list的一个子类,提供了一些附加的方法。

xpath(query)

css(query)

extract()

re()

基于HTML Response的Selector例子

假定sel是用一个HtmlResponse实例化的Selector

sel = Selector(html_response)
sel.xpath("//h1")
sel.xpath("//h1").extract()  # includes h1
sel.xpath("//h1/text()").extract() # excludes h1
# 遍历所有的p标签,打印它们的类属性
for node in sel.xpath("//p"):
    print node.xpath("@class").extract()

基于XML Response的Selector例子

假设sel是用XmlResponse实例化的Selector

sel = Selector(xml_response)
sel.xpath("//product")

从指定文档提取所有价格,需要使用命名空间:

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

移除命名空间

在Scrapy项目中,为了写更多简单便捷的XPaths,经常需要移除命名空间,仅剩元素名称。 Selector.remove_namespaces()

>> scrapy shell https://github.com/blog.atom
>> response.xpath("//link")
[]
# 因为Atom XML命名空间扰乱了节点
>>> 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. 移除命名空间需要遍历所有的文档,对于爬虫来说这是一个相当费时且昂贵的操作。
  2. 有些时候需要使用命名空间来避免名称冲突。

Items

声明Itmes

使用class和Field对象来声明一个Item,如下所示:

import scrapy

Class Product(scrapy.Item):
    name = scrapy.Field()
    price = scrapy.Field()
    stock = scrapy.Field()
    last_updated = scrapy.Field(serializer=str)
注意:Scrapy中的Items定义和Django中Models定义很像,不同的是Items中没有多种字段类型,只有简单的`scrapy.Field`

Item Fields

Field对象为每个字段指定metadata,例如上例中last_updated字段的serializer方法。

可以为每个字段指定任何类型的metadata,Field对象并没有限制接收的values,所以,这里并没有列出所有的metadata keys。在Field对象中定义的每个字段对应不同的功能,你也可以根据你的需要定义别的字段。Field对象的主要目标是提供一种在一个地方定义所有元数据(metadata)的方法。你可以查阅相关文档来使用metadata。

值得注意的是Field对象用来声明item字段的时候,该字段并不是作为类的属性, 相反,可以通过Item.fields(此处Item对应Product)属性来访问。(类似dict而不是object)

使用Items

这里有一些使用items的通用案例,使用上面声明的Product,你将会发现API非常类似dict API

创建items

>>> product = Product(name='Desktop PC',price=1000)
>>> print product
Product(name='Desktop PC',price=1000)

获取字段值

>> product['name']
Desktop PC
>>> product.get('name')
Desktop PC
>>> product['price']
1000
>>> product['last_updated']
Traceback (most recent call last):
    ...
KeyError: 'last_updated'
>>> product.get('last_updated', 'not set')
not set
>>> product['lala'] # getting unknown field
Traceback (most recent call last):
    ...
KeyError: 'lala'
>>> product.get('lala', 'unknown field')
'unknown field'
>>> 'name' in product  # is name field populated?
True
>>> 'last_updated' in product  # is last_updated populated?
False
>>> 'last_updated' in product.fields  # is last_updated a declared field?
True
>>> 'lala' in product.fields  # is lala a declared field?
False

设置字段值

>>> product['last_updated'] = 'today'
>>> product['last_updated']
today

>>> product['lala'] = 'test' # setting unknown field
Traceback (most recent call last):
    ...
KeyError: 'Product does not support field: lala'

访问所有值

类似使用字典的API

>>> product.keys()
['price','name']

>>> product.items()
[('price', 1000), ('name', 'Desktop PC')]

其他通用的任务

复制items

>>> product2 = Product(product)
>>> print product2
Product(name='Desktop PC', price=1000)

>>> product3 = product2.copy()
>>> print product3
Product(name='Desktop PC', price=1000)

items to dicts

>>> dict(product) # create a dict from all populated values
{'price': 1000, 'name': 'Desktop PC'}

dicts to items

>>> Product({'name': 'Laptop PC', 'price': 1500})
Product(price=1500, name='Laptop PC')

>>> Product({'name': 'Laptop PC', 'lala': 1500}) # warning: unknown field in dict
Traceback (most recent call last):
    ...
KeyError: 'Product does not support field: lala'

扩展Items

可以通过继承Item来创建新的Item,以便增加字段或改变某些字段的信息。

class DiscountedProduct(Product):
    discount_percent = scrapy.Field(serializer=str)
    discount_expiration_date = scrapy.Field()

你也可以使用前面定义的字段元数据来扩展字段元数据,添加或改变原来的值。

class SpecificProduct(Product):
    name = scrapy.Field(Product.fields['name'], serializer=my_serializer)

此处扩展了Product的name字段的元数据,并在原来的基础上增加了serializer元数据。

Item对象

class scrapy.item.Item([arg]) 从所给的参数中返回一个可选的初始化Item。 Items复制了标准的dict API,包括它的构造函数。只是添加了fields属性。 > fields
包含了所有声明字段的字典。字段的名称作为键,声明的Field对象作为值。

Field对象

class scrapy.item.Field([arg])Field类只是内置dict类的一个别名,没有提供任何额外的功能或属性。换句话说Field只是普通的Python字典。一个单独用类属性来声明Item的类。

Item Loaders

Item Loaders为构建scraped Itmes提供了便捷的途径。即使能够使用Item的类字典API来构建,但是Item Loaders在爬取过程中提供了许多便捷的API来构建items,通过一些自动化的通用任务,比如:在分发之前解析原始提取到的数据。

换句话说,Items为爬取到的数据提供容器,而Item Loaders提供便捷的途径来构造这个容器。

Item Loaders被设计为灵活、高效和简单的机制来扩展和覆盖不同字段的解析规则,无论是爬虫还是源格式(HTML,XML)维护起来都很方便。

使用Item Loaders来构建Items

使用Item Loader前需要先实例化,你可以使用类字典(Item或者dict)或Item Loader构造器使用ItemLoader.default_item_class指定的属性来自动实例化Item。

然后,开始收集数据到Item Loader,通常使用Selectors。你可以添加多个值到相同的item字段,Item Loader会用合适的方法来处理。

这里有一个经典的Item Loader使用案例,使用前面章节定义的Product Item:

from scrapy.loader import ItemLoader
from myproject.items import Product

def parse(self,response):
    l = ItemLoader(item=Product(),response=response)
    l.add_xpath('name','//div[@class="product_name"]')
    l.add_xpath('name','//div[@class="product_title"]')
    l.add_xpath('price','//p[@id="price"]')
    l.add_css('stock','p#stock')
    l.add_value('last_updated','today')
    return l.load_item()

从上面的代码可以看到,name字段从不同的页面提取了2次: 1.//div[@class="product_name"] 2.//div[@class="product_title"]

换句话说,被指定给name字段的数据通过add_xpath()方法从2个XPath路径提取。

然后,同样的方法添加price和stock(stock使用CSS选择器add_css()方法添加),最后用add_value直接给last_updated赋值'today'。

最后,当所有数据收集完成,ItemLoader.load_item()方法被调用,并返回用前面的add_xpath(),add_css(),add_value()提取的数据填充的item。

输入输出处理

每个Item Loader为每个Item 字段都有一个输入处理器和一个输出处理器。输入处理器在收到数据的时候通过add_xpath(),add_css(),add_value()方法尽快提取数据,输入处理器的处理结果将被收集并存储在ItemLoader中。待所有数据收集完成,ItemLoader.load_item()方法将被调用,构建并返回Item对象。此时,伴随着前面提取的数据输出处理器将被调用。输出处理器的结果将是Item的最终值。

下面通过一个例子来阐明输入、输出处理器针对特定字段是怎样被调用的(其他字段是同样的原理):

l = ItemLoader(Product(),some_selector)
l.add_xpath('name',xpath1) # 1
l.add_xpath('name',xpath2) # 2
l.add_css('name',css) # 3
l.add_value('name','test') # 4
return l.load_item() # 5
  1. 数据从xpath1被提取,然后传递给name字段的输入处理器, 输入处理器的结果被收集并保持在Item Loader中(并没有赋值给item)。
  2. 同上,数据提取后appended到1提取到的数据中。
  3. 同上,提取方法改变而已,数据提取后appended到1、2提取到的数据中。
  4. 同上,只是这里的value被转换为一个可迭代的元素,因为输入处理器只能接受可迭代的参数。数据提取后appended到1、2、3提取到的数据中。
  5. 1/2/3/4步中提取的数据被传递给输出处理器,输出处理器的结果将被赋值给item。

值得注意的是,处理器只是可调用的对象,这些对象伴随着要解析的数据被调用,并返回一个已解析的值。因此,您可以使用任何函数作为输入或输出处理器。唯一的要求是它们必须接受一个(并且只有一个)可迭代的参数。

注意:输入、输出处理器必须接受一个迭代器作为它的第一个参数。那些输出方法可任意,输入处理器的结果将被添加至一个内部的list(在Loader中),包含收集到的该字段的值
。输出处理器的值最终将被赋给item。

最后,Scrapy自带了一些通用的处理器。

声明Item Loaders

Item Loaders像Items一样使用类定义语法来声明。

from scrapy.loader import ItemLoader
from scrapy.loader.processors import TakeFirst,MapCompose,Join

class ProductLoader(ItemLoader):
    defaule_output_processor = TakeFirst()
    name_in = MapCompose(unicode.title)
    name_out = Join()
    price_in = MapCompose(unicode.strip)
    # ...

如你所见,输入处理器使用_in后缀来声明,而输出处理器使用_out后缀。你也可以通过ItemLoader.default_input_processorItemLoader.default_output_processor定义默认的输入/输出处理器。

定义输入输出处理器

官网open in new window

Feed exports

New in version 0.10.

实现爬虫最常用的一个特性,用于存储爬取到的数据,通常会使用爬取到的数据生成一个“export file”(通常叫作“export feed”)。

Scrapy通过Feed Exports提供了开箱即用的功能,允许你将scraped items生成多种序列化的feed格式,并在后端存储。

序列化格式

feed exports使用Item exporters序列化爬取到的数据。这些格式开箱即用:

 - JSON
 - JSON lines
 - CSV
 - XML 

但是,你也可以扩展支持的格式,通过FEED_EXPORTERS设置。

JSON

JSON lines

  • FEED_FORMAT:jsonlines
  • Exporter used:JsonLinesItemExporter

CSV

  • FEED_FORMAT:csv
  • Exporter used:CsvItemExporter
  • 通过FEED_EXPORT_FIELDS指定导出的列和顺序。其他格式也可以使用这个选项,但是CSV不像其他格式,它使用的是固定的头部。

XML

  • FEED_FORMAT:xml
  • Exporter used:XmlItemExporter

Pickle

  • FEED_FORMAT:pickle
  • Exporter used:PickleItemExporter

Marshal

  • FEED_FORMAT:marshal
  • Exporter used:MarshalItemExporter

Storages

使用URI定义存储feed的位置(通过FEED_URI设置)。feed exports支持多种后端存储格式,通过URI方案定义。 后端开箱即用的存储格式:

  • 本地文件系统
  • FTP
  • S3(需要botocore 或 boto)
  • 标准输出

如果依赖的额外库不可用的话,有的后端存储将不可用。例如:S3后端只有当botocore和boto库都已安装才可用。(Scrapy只在Python 2上支持boto)

Storage URI参数

存储URI还包含在创建是被替换的参数,被替换的参数如下:

  • %(time)s 当feed被创建时用时间戳替换
  • %(name)s%被爬虫名称替换

其他的参数将会被爬虫的同名属性替换,如%(site_id)s在feed被创建的时候将会被spider.site_id属性替换。

下面有一些例子来解释:

  • 使用FTP存储,每个爬虫一个目录

    • ftp://user:password@ftp.example.com/scraping/feeds/%(name)s/%(time)s.json
  • 使用S3存储,每个爬虫一个目录

    • s3://mybucket/scraping/feeds/%(name)s/%(time)s.json

Storage 后端开发s

本地文件系统

feeds存储在本地系统:

  • URI scheme:文件
  • URI例子:file:///tmp/export.csv
  • 额外的库:none 注意:使用本地文件系统的时候,如果指定了绝对路径(/tmp/export.csv),可以忽略scheme,但是仅在Unix系统上有效。

FTP

feeds存储在FTP服务器上

  • URI shceme:ftp
  • Example URL:ftp://user:pass@ftp.example.com/path/to/export.csv
  • 额外的库:none

S3

feeds存储在Amazon S3上。

  • URI scheme:s3
  • Example URIs:
    • s3://mybucket/path/to/export.csv
    • s3://aws_key:aws_secret@mybucket/path/to/export.csv
  • 需要额外的库:botocoreboto

AWS证书可以在URI中以user/password传输,或者可以通过以下设置传输:

  • AWS_ACCESS_KEY_ID
  • AWS_SECRET_ACCESS_KEY

标准输出

feeds被写进Scrapy进程的标准输出流中。

  • URI scheme:stdout
  • Example URI:stdout:
  • 需要额外的库:none

设置

feed导出配置项:

  • FEED_URI(强制的)
  • FEED_FORMAT
  • FEED_STORAGES
  • FEED_EXPORTERS
  • FEED_STORE_EMPTY
  • FEED_EXPORT_ENCODING
  • FEED_EXPORT_FIELDS
  • FEED_EXPORT_INDENT

FEED_URI

默认:None 导出feed的URI,关于URI schemes的资料查看Storage 后端开发s 这个设置对于导出feed是必须的。

FEED_FORMAT

序列化feed用的格式,查看[Serialization formats]

FEED_EXPORT_ENCODING

Default:None 被用于feed的编码

如果没有设置或设置为None,将使用UTF-8作为除了JSON格式输出的编码,JSON使用安全数字编码(\uXXXX序列)是有历史原因的。 如果你愿意,使用utf-8作为JSON编码格式也是可以的。

FEED_EXPORT_FILEDS

Default:None

被导出字段列表选项,例如: FEED_EXPORT_FIELDS=["foo","bar","baz"]

使用FEED_EXPORT_FIELDS选项来定义导出的字段和顺序。

FEED_EXPORT_FIELDS是空的或None时,Scrapy使用在dicts中定义的字段或Item子类。

如果导出需要一个固定字段的集合(如:CSV),但FEED_EXPORT_FIELDS是空或None时,Scrapy将通过导出数据推导字段名,目前使用第一个item的字段名。

FEED_EXPORT_INDENT

Default:0 输出文件不同层级的缩进空格数量。如果FEED_EXPORT_INDENT是一个非负整数,然后数组元素和对象成员将按照设定的缩进进行展示。缩进级别为0或负数,将会输出每个item到新的行。 当前仅仅JsonItemExporterXmlItenExporter可用。例如当你导出.json.xml格式时。

FEED_STORE_EMPTY

Default:False 是否允许导出空的feeds(如:没有items的feeds)

FEED_STORAGES

Default:{}

项目提供的的额外feed存储后端支持。键为URI schemes,值为存储类的路径。

FEED_STORAGES_BASE

Default:

{
    '':'scrapy.extensions.feedexport.FileFeedStorage',
    'file': 'scrapy.extensions.feedexport.FileFeedStorage',
    'stdout': 'scrapy.extensions.feedexport.StdoutFeedStorage',
    's3': 'scrapy.extensions.feedexport.S3FeedStorage',
    'ftp': 'scrapy.extensions.feedexport.FTPFeedStorage',
}

Scrapy内置的一个包含feed后端存储的字典。你可以在FEED_STORAGES中配置值为None来禁用一个选项。例如:禁用内置的FTP后端存储,不是替换,把下面的代码放入settings.py中:

FEED_STORAGES={
    'ftp':None
}

FEED_EXPORTERS

Default:{} 项目提供的额外exporters字典。键为序列化的格式,值为Item exporter类的路径。

FEED_EXPORTERS_BASE

Default:

{
    'json': 'scrapy.exporters.JsonItemExporter',
    'jsonlines': 'scrapy.exporters.JsonLinesItemExporter',
    'jl': 'scrapy.exporters.JsonLinesItemExporter',
    'csv': 'scrapy.exporters.CsvItemExporter',
    'xml': 'scrapy.exporters.XmlItemExporter',
    'marshal': 'scrapy.exporters.MarshalItemExporter',
    'pickle': 'scrapy.exporters.PickleItemExporter',
}

Scrapy提供的内置feed exporters字典。你可以在FEED_EXPORTERS中禁止某些exporters。例如:禁止内置的CSV exporter(而不是替换),把下面的代码放入settings.py:

FEED_EXPORTERS={
    'csv':None,
}

Item Exporters

经常需要把爬取到的数据导出,以便其他应用使用,毕竟这是爬虫的目的。

Scrapy提供了一些不同导出格式(XML,CSV or JSON)的Item Exporters。

使用Item Exporters

如果你很忙,只是想用Item Exporter导出爬取到的数据,那么请看Feed export

上次编辑于:
贡献者: 宗安