Scrapy学习笔记(4)—Spider

来源:互联网 发布:移动运营商数据分析 编辑:程序博客网 时间:2024/05/17 05:57

本笔记介绍几种spider,分别是basic(默认Spider)、CrawlSpider、XMLFeedSpder、CSVFeedSpider四种,以及另外一种SitemapSpider

Spider

Spiders这个类定义如何爬取网页,包括如何执行爬虫,比如说追踪链接(follow links),和如何提取网页结构数据(比如爬取items),换句话说,Spiders就是定义爬虫行为和解析特定网页(一堆网页)的类。对于爬虫来说,爬取步骤基本如下:

  1. 从初始的网页开始,到回调(callback)解析方法(parse),然后response就从这些请求中下载。第一个请求(或第一批)传入start_requests() 方法通过特定的start_urls 属性发出请求并且利用parse() 方法回调;
  2. 在callback方法中,你需要将从网页爬下来的数据Item对象Request对象、或者可迭代的(interable)这些对象之中的一个或多个以dict(字典)形式返回.然后这些请求可能也会包含callback并且也会被scrapy下载接着被回调。
  3. 在回调方法中,使用Selectors解析网页的内容,CSS、XPath、BeautifulSoup、正则表达式、lxml等等都可以选择
  4. 最后,items将从spider送到database(比如item pipelines)或者使用Feed exports输出到文件中

这种过程适用于所有爬虫,之后会介绍四种默认的爬虫basic、crawl、csvfeed、xmlfeed和一种基于sitemap的爬虫。

scrapy.Spider

class scrapy.spiders.Spider

这是最简单的爬虫basic,所有的爬虫类都必须继承(inherit)这个类。它不提供任何特殊的方法,它只提供默认的start_requests()start_urls这个属性发出请求并且调用parse方法获得响应(response)

  • name
    定义爬虫名称的字符串。name决定爬虫如何被scrapy部署(实例化),所以它必须是独一无二的,但是没有什么可以阻止你实例化多个相同的爬虫实例。name是最重要的爬虫属性
    如果一个爬虫爬取单独的domain(域名),这个爬虫的name最好和域名保持一致,比如说要爬取“baidu.com”这个域名,它的name应该被命名为baidu。

  • allowed_domains
    一个可选的字符串列表包含一些允许爬虫爬取的域名。不属于这些域名的url链接不会被爬虫爬取,除非OffsiteMiddleware被使用。
    比如说你打算爬取https://www.example.com/1.html,那么你就可以加上example.com到allowed_domain中,你的爬虫就只会爬取域名为example.com的链接。

  • start_urls
    当没有特定的url链接被指定时,一个爬虫爬取的初始URL列表就是start_urls。随后的url陆续地从这个列表中生成。

  • custom_settings
    自定义的设置属性,字典形式,执行该爬虫时它将会覆盖settings.py中的设置。在实例化之前它将被定义为类属性。具体定义等讲settings之后就明白了。

  • crawler
    这个属性从实例化该类后的from_crawler()类方法中设置,这个属性将会连接到Crawler对象然后绑定这个爬虫。
    Crawler在项目中封装了许多组件,用于它们的单个条目访问(例如extensions、middlewares、signals managers等等)。具体等讲到Crawler API。

  • settings
    用于设置爬虫

  • logger
    使用爬虫的name创建的Python日志记录器。你可以使用它来发送日志消息。

  • from_crawler(crawler, *args, **kwargs)
    用来创建爬虫的类方法。
    你可能不需要直接重写这个方法,因为默认的实现__init__()方法的代理,用给定的参数args和命名参数kwargs来调用它。尽管如此,这个方法在新实例中设置了crawler和settings属性,以便稍后在爬虫代码中访问它们。
    参数:

    • crawler(Crawler实例):绑定爬虫的crawler
    • args(list):传给__init__()方法的参数
    • kwargs(dict):传给__init__()方法的关键字参数
  • start_requests()
    这个方法必须返回一个可迭代的Requests。当爬虫执行时被Scrapy调用。Scrapy只调用一次,所以将这个方法作为生成器实现是很安全的。
    默认的实现对start_urls中的每个url链接生成Request(url, dont_filter=True)
    如果你想要更改用于开始抓取某个域名的请求,重写该方法即可。

例子:

import scrapyclass MaterialSpider(scrapy.Spider):    name = 'material' # 一个项目中独一无二的爬虫名字    def start_requests(self):        urls = [            'http://588ku.com/pt/chengshi.html',            'http://588ku.com/pt/lvxing.html'        ]        for url in urls:            yield scrapy.Request(url=url, callback=self.parse)    def parse(self, response):        page = response.url.split('/')[-1][:-5]        filename = 'material-%s.html' % page        with open(filename, 'wb') as f:            f.write(response.body)        self.log('Saved file %s' % filename)
  • parse(response)
    这是默认的回调方法,用来处理响应(response)。
    parse方法用来管理处理响应并且返回爬取到的数据或者更多的URL链接来follow,其中,数据必须以字典的形式返回,或者返回Item对象,返回的Request必须是可迭代的,可以在for循环中使用yield来返回。其他的Requests回调方法有同样的要求。
    参数:

    • response(Response对象):需要解析的响应
  • log(message[, component])
    通过爬虫的logger发送日志消息的包装器(Wrapper),保持向后兼容性。

  • closed(reason)
    当关闭爬虫时调用。这个方法提供一个捷径来作为spider_closed signal调用signals.connect()方法。
    例子1:

import scrapyclass BilibiliSpider(scrapy.Spider):    name = 'bilibili.com'    allowed_domains = ['bilibili.com']    start_urls = [        'https://www.bilibili.com/video/music.html',        'https://www.bilibili.com/video/douga.html',        'https://www.bilibili.com/video/game.html'    ]    def parse(self, response):        self.logger.info('网页 %s 的内容提取成功!', response.url)

例子2,从一个callback返回多个Request和item:

import scrapyclass BilibiliSpider(scrapy.Spider):    name = 'bilibili.com'    allowed_domains = ['bilibili.com']    start_urls = [        'https://www.bilibili.com/video/music.html',        'https://www.bilibili.com/video/douga.html',        'https://www.bilibili.com/video/game.html'    ]    def parse(self, response):        for title in response.xpath('//p[@class="title"]/text()').extract():            yield {                'title': title                }        for url in response.xpath('//*[@id="primary_menu"]/ul/li/a/@href').extract():            url = 'https://' + url            yield scrapy.Request(url, callback=self.parse)

Spider arguments

爬虫可以接收参数来规范它们的行为。对于爬虫参数的一些常见用途是定义初始的url链接或将爬虫限制到站点的某些部分,但是它们可以用于配置爬虫的任何功能。
爬虫参数通过crawl -a命令来传达,用法:
scrapy crawl <spider> -a NAME=VALUE

爬虫可以在他们的__init__()中访问参数,例如:

import scrapyclass MySpider(scrapy.Spider):    name = 'myspider'    def __init__(self, category=None, * args, ** kwargs):        super(MySpider, self).__init__( * args, ** kwargs)        self.start_urls = ['http://www.example.com/categories/%s' % category]        # ...

就可以在执行crawl命令的时候利用-a给category传参数:
scrapy crawl myspider -a category=electronics

默认的__init__()方法将获得任何的参数并且复制他们给爬虫作为参数,比如你可以这样写:

import scrapyclass MySpider(scrapy.Spider):    name = 'myspider'    def start_requests(self):        yield scrapy.Request('http://www.example.com/categories/%s' % self.category)

不过你要记住,传给爬虫的参数都是字符串,爬虫不会给你解析参数的类型,所以如果你传给它的是start_url,你将不得不亲自解析它,把它变成列表,利用ast.literal_eval或者json.loads然后将它变成参数。否则,你将对一个字符串进行迭代导致一个字符被视为一个url链接,肯定爬不出来什么东西,所以要注意。

一个重要的情况是设置HttpAuthMiddleware使用的http身份验证凭证(http auth credentials)或 UserAgentMiddleware使用的user agent:
scrapy crawl myspider -a http_user=myuser -a http_pass=mypassword -a user_agent=mybot

爬虫参数也可以通过爬取的 schedule.json API 传递。

通用的 Spiders

Scrapy提供了一些通用的爬虫你可以通过继承他们来使用。主要目的就是为了方便,为那些常见、通用的情况提供便利的方法,比如基于一些特定的rules爬取、从Sitemaps爬取 或 解析XML/CSV来爬取网站的所有链接并且follow他们。

接下来的例子要先在items.py中定义如下:

import scrapyclass HelloprojectItem(scrapy.Item):    id = scrapy.Field()    name = scrapy.Field()    description = scrapy.Field()

CrawlSpdiers

class scrapy.spiders.CrawlSpider

这是最常见的用来爬普通网页的爬虫,因为它通过定义一组规则为接下来的链接提供了一种方便的机制。它可能不是最适合爬取你指定网页或项目的爬虫,但在几种情况下,它是通用的,所以你可以从它开始,根据你的需要重写它的方法来更加自定义化。
除了从Spider继承的属性以外,这个类支持一个新的属性,rules

  • rules
    包含一个或多个Rule对象的列表。每个Rule对象定义了爬取网页的一种行为,规则对象将在下面介绍。根据它们在这个属性中定义的顺序来逐个实现,如果多个规则匹配相同的链接,那么第一个规则将被使用。

这个类有一个可以重写的方法:

  • parse_start_url(response)
    这个方法用来调用start_urls的响应(response)。它允许解析初始的响应并且必须返回Item对象或者Request对象,或者两者中可以迭代的对象。

Crawling rules

类:class scrapy.spiders.Rule(link_extractor, callback=None, follow=None, process_link=None, process_request=None)

  • link_extractor
    link_extractor是一个Link Extractor对象, 定义了从每次爬取的页面中提取链接的行为,下面会细讲。
  • callback
    callback后加可调用的方法的名字,用来调用每一个被指定的link_extractor提取的链接(在这种情况下,使用该名称的爬虫对象的方法将被使用,和之前用过的callback=self.parse一个道理,但是请注意!!!看下面的Warning↓)。callback接收response作为它的第一个参数,并且必须返回包含Item对象**and/or**Request对象

Warning:当你要写rules的时候,请避免使用parse作为回调函数,因为CrawlSpider用parse方法本身来实现这种逻辑。

  • cb_kwargs
    cb_kwargs是一个字典,它包含了要传给callback函数的关键字参数(keyword arguments)

  • follow
    follow是一个布尔型变量,指定出用此rule顺着爬出来的链接是否要继续爬下去如果callback是None,则follow默认是True,否则它默认是False

  • process_links
    process_links是可调用的,或者是字符串(在这种情况下,使用该名称的、爬虫对象的方法将被使用),它将使用指定的link_extractor从每个响应中提取出的每个链接列表。这主要用于过滤(filtering)目的。

  • process_request
    process_request是一个可调用的或一个字符串(在这种情况下,使用该名称的、爬虫对象的方法将被使用),它将调用通过rule所提取的每个请求,并且必须返回一个request或None(以过滤request)。

CrawlSpider 例子

让我们看一个有rules的CrawlSpider的例子:

# -*- coding: utf-8 -*-import scrapyfrom scrapy.linkextractors import LinkExtractorfrom scrapy.spiders import CrawlSpider, Ruleclass MyspiderSpider(CrawlSpider):    name = 'example.com'    allowed_domains = ['example.com']    start_urls = ['http://example.com/']    rules = (        # Extract links matching 'category.php' (but not matching 'subsection.php')        # and follow links from the, (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链接,通过parse_item解析。对于每一个item response,一些数据将会从HTML中用XPath提取,并且Item将被它填充。

Link extractors对象专门用来解析网页链接,用于CrawlSpider爬虫,当然你也可以单独使用在你的爬虫里。

其中的extract_links方法接收一个Response对象并且返回由scrapy.link.Link对象组成的列表。Link Extractors实例化一次要调用几次extract_links()根据不同的Response来解析不同的链接。

scrapy默认的link extractor是LinkExtractor,它和LxmlLinkextractors一样。

LxmlLinkExtractor

class scrapy.linkextractors.lxmlhtml.LxmlLinkExtractor(allow=(), deny=(), allow_domains=(), deny_domains=(),    restrict_xpaths=(), tags=('a', 'area'), attrs=('href',), canonicalize=False,     unique=True, process_value=None, deny_extensions=None, restrict_css=(), strip=True)

LxmlLinkExtractor是推荐使用的链接提取器,它是用lxml的 HTMLParser实现的。

参数

  • allow (单独的正则表达式或者列表的正则表达式)
    它用来匹配链接,没有匹配到的链接就扔掉。如果为空,或者没有这个参数,它将匹配所有的链接

  • deny (一个或一列表正则表达式)
    如果匹配到链接,它将排除(exclude)该链接,否则不排除,deny参数优先于allow参数

  • allow_domains (字符串或列表)
    包含的域名将被解析来提取链接,不包含的不会提取。

  • deny_domains (字符串或列表)
    所包含的域名不会被解析。

  • deny_extensions(列表)
    单个的值或者一列表的字符串,在解析链接时会把包含这些字符串的链接无视掉。

  • restrict_xpaths(字符串或列表)
    用XPath定义的区域,链接将在这个区域被解析出来。

  • restrict_css(字符串或列联表)
    用CSS 选择器定义的区域,链接将在这个区域被解析出来

  • tags(字符串或列表)
    一个或一列表的标签(tag)在解析链接时需要考虑。默认的是tag=(‘a’, ‘area’)

  • attrs(列表)
    一个或由属性组成的列表(只会作用于指定的tag中的属性)在解析链接的时候会被考虑。默认的是attrs=(‘href’,)

  • canonicalize(boolean变量)
    使用w3lib.url.canonicalize_url来规范每个提取的链接,默认False。它是用来重复检查的,它可以更改服务器端可见的URL,因此对于具有规范化和原始URL的请求,其响应可能不同。如果你使用LinkExtractor来提取链接,那最好将它设置为默认的False。

  • unique(boolean变量)
    是否应该对爬取的链接进行重复过滤,正如它的意思一样——独一无二!

  • process_value(callable可调用的函数)
    接收每个从标签和属性中提取出的值并且修改这个值然后返回一个新的值,或者返回None来完全无视这个链接。如果没有设置,它默认是lambda x: x(意思就是返回本身,可以百度lambda)。举个例子:
    比如这个链接:

<a href="javascript:goToPage('../other/page.html'); return false">Link text</a>

你可以定义如下方法:

def process_value(value):    m = re.search("javascript:goToPage\('(.*?)'", value)    if m:        return m.group(1)

来匹配单引号中的网址。

  • strip(boolean变量,默认是True)
    是否从提取的属性中删除whitespaces(比如空格、换行、空行)。根据HTML5的标准,在开头、结尾的空行必须从某些元素中去除,比如<a>、<area> 标签的href属性,<img>标签的src属性,<iframe>元素等等。所以LinkExtractor默认是去除空行的。不过你可以将它设置为False(比如你爬取一些允许有空格的网站,这种情况可以设置为False)

XMLFeedSpider

介绍

class scrrapy.spiders.XMLFeedSpider
XMLFeedSpider被设计用于通过迭代某个确定的节点来解析XML源(XML feed)。迭代器可以从iternodesxmlhtml 中选择。从性能角度一般推荐iternodes。因为xml和html迭代器一次生成整个DOM(文档对象模型Document Object Model)来解析。然而,使用html作为迭代器解析XML时对付错的markup会很有用。

为了设置迭代器和标签名,你必须定义下列类属性:

  • iterator 定义迭代器的字符串。它可以是下列的一种(默认iternodes):
    • iternodes 基于正则表达式(regex)高速的迭代器
    • html 使用选择器(Selector)的迭代器。要记住:它使用DOM来解析并且必须将所有的DOM载入内存,当要解析的数据非常大时,会造成很大的麻烦。
    • xml 使用选择器(Selector)的迭代器。要记住:它使用DOM来解析并且必须将所有的DOM载入内存,当要解析的数据非常大时,会造成很大的麻烦。(你没看错,和上面的描述一样)
  • itertag 要迭代的结点(或元素)的名字(字符串)。比如:
    itertag = 'product'
  • namespaces(prefix, uri)元组组成的列表,定义了在该文档(document)中将被spider处理的、可用的namespaces。prefixuri会自动地被register_namespace()方法调用。(PS:可以手动百度uri与url的区别)例子:
class YourSpider(XMLFeedSpider):    namespaces = [('n', 'http://www.sitemaps.org/schemas/sitemap/0.9')]    itertag = 'n:url'    # ...

除了这些新的属性,这个爬虫也有下列可重写的方法:

  • adapt_response(response) response一到达middleware,该方法就调用它,也就是在Spider解析前,先经过该方法处理。它用来在spider解析前修改response的body。这个方法接收一个response也返回一个response(可以和之前的response一样或者有所修改)
  • parse_node(response, selector) 这个方法在匹配提供的标签名(itertag)时被调用。接收response和一个匹配结点的选择器(Selector)。强制(mandatory强制的)重写该方法。否则你的爬虫不会工作。这个方法必须返回一个Item对象Request对象或者一个包含两者之一的迭代器。
  • process_results(response, results) 这个方法在处理spider返回的每个结果(item或者request)时被调用,并且在向框架核心(framework core)返回结果之前它将要执行最后一次的处理请求,比如设定item的ID。它接收一个结果(item或request)的列表和从这些结果产生的response。它必须返回一个结果的列表(Items或Requests)

例子

这些爬虫很好用,让我们看一个例子:

# -*- coding: utf-8 -*-from scrapy.spiders import XMLFeedSpiderfrom HelloProject.items import HelloprojectItemclass XmlspiderSpider(XMLFeedSpider):    name = 'xmlspider'    allowed_domains = ['example.com']    start_urls = ['http://www.example.com/feed.xml']    iterator = 'iternodes' # you can change this; see the docs    itertag = 'item' # change it accordingly    def parse_node(self, response, node):        self.logger.info('Hi, this is a <%s> node!: %s', self.itertag, ''.join(node.extract()))        item = HelloprojectItem()        item['id'] = node.xpath('@id').extract()        item['name'] = node.xpath('name').extract()        item['description'] = node.xpath('description').extract()        return item

基本上,我们在那里做的就是创建一个爬虫并从给定的链接下载一个源(feed),然后从每个item标签开始迭代,将他们打印,并在Item中储存一些随机的数据。

CSVFeedSpider

在介绍该爬虫之前,先了解一下CSV文件比较好Wiki百科-CSV(左下角中文翻译,建议看英文)

介绍

class scrapy.spiders.CSVFeedSpider
这个爬虫和XMLFeedSpider很像,但它是按行(row)遍历,而XMLFeedSpider是按结点遍历。每次遍历时调用的方法是parse_row()

  • delimiter 在CSV文件中分开每个field的分隔符(separator character),类型为字符串,默认是,(逗号,comma)。(PS:delimiter的意思是定界符)
  • quotechar CSV文件中用于引用每个记录的字符串,默认为"(quotation mark,引号)(PS:quotechar的意思是引用字符;原文中enclosure character,不好翻译。不过了解了CSV文件之后应该好理解)
  • headers CSV文件中列的名字(column names)的列表。
  • parse_row(response, row) 接收一个response,以及一个字典,这个字典由CSV文件中提供或检测出的以headers的名字为key组成。在这个爬虫中,你可以通过重写adapt_response()process_results()方法来预处理(pre-process)和后处理(post-processing)。

例子

让我们看一个和之前很像的例子,却用的是CSVFeedSpider:

# -*- coding: utf-8 -*-from scrapy.spiders import CSVFeedSpiderfrom HelloProject.items import HelloprojectItemclass CsvspiderSpider(CSVFeedSpider):    name = 'csvspider'    allowed_domains = ['example.com']    start_urls = ['http://www.example.com/feed.csv']    headers = ['id', 'name', 'description']    delimiter = ','    quotechar = "'"    def parse_row(self, response, row):        item = HelloprojectItem()        item['id'] = row['id']        item['name'] = row['name']        item['description'] = row['description']        return item

SitemapSpider

介绍

class scrapy.spiders.SitemapSpider
SitemapSpider允许你通过Sitemap发现URL链接来爬取一个网站。(解释见Wiki)它支持嵌套的(nested)sitemaps和从robots.txt发现sitemap。你可以找个网站看一看sitemap.xml究竟是什么,比如极客学院的sitemap.xml。下面介绍该爬虫:

  • sitemap_urls URL链接组成的列表,指向某网站的sitemap.xml,比如http://www.jikexueyuan.com/sitemap.xml,当然可能有其他格式比如说txt之类。你也可以指向robots.txt,然后它将从里面解析网页(很强势)。
  • sitemap_rules 由元组(regex, callback)组成的列表:
    • regex:从sitemaps匹配链接的正则表达式。可以是字符串或者re.compile对象。
    • callback:用来处理被正则表达式匹配的链接,它是爬虫中一个方法的名字,也可以是一个callable。例如:
      sitemap_rules = [('/product/', 'parse_product')]
      Rules 按顺序执行,并且只有第一个匹配的规则被使用
      如果你忽略(omit)这个属性,所有在sitemap中找到的链接都会被回调函数parse()处理
  • sitemap_follow 用于匹配将要跟进(follow)的sitemap的正则表达式(regexs)组成的列表。这个属性只有在使用 Sitemap index files 来指向其他sitemap文件的站点时才会应用。默认跟进所有sitemaps
  • sitemap_alternate_links 指定当一个url链接可选时是否跟进。这些链接对于同一个网页提供不同的语言,例如
<url>    <loc>http://example.com/</loc>    <xhtml:link rel="alternate" hreflang="de" href="http://example.com/de/"></url>

    当sitemap_alternate_link设置后,两个链接都会被跟进;如果没有设置,只有http://example.com/会跟进。默认不设置。

例子

最简单的例子:使用parse处理所有通过sitemap找到的链接:

from scrapy.spiders import SitemapSpiderclass MySpider(SitemapSpider):    sitemap_urls = ['http://www.example.com/sitemap.xml']    def parse(self, response):        pass # 在这里定义如何解析

使用确定的回调函数处理一些链接,其他链接用不同的回调函数处理:

from scrapy.spiders import SitemapSpiderclass MySpider(SitemapSpider):    sitemap_urls = ['http://www.example.com/sitemap.xml']    sitemap_rules = [        ('/product/', 'parse_product'),        ('/category/', 'parse_category'),    ]    def parse_product(self, response):        pass # 定义如何爬取带有/product/的链接    def parse_category(self, response):        pass # 定义如何爬取带有/category/的链接

跟进在robots.txt中定义的sitemaps,并且只跟进包含/sitemap_shop的链接:

from scrapy.spiders import SitemapSpiderclass 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 # 定义如何爬取shop

将SitemapSpider与其他url源结合:

from scrapy.spiders import SitemapSpiderclass MySpider(SitemapSpider):    sitemap_urls = ['http://www.example.com/robots.txt']    sitemap_rules = [        ('/shop/', 'parse_shop'),    ]    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 # 定义如何爬取shop    def parse_other(self, response):        pass # 定义如何爬取其他网页

end?

这里都是理论基础,例子并不是很多,也不实用,所以我打算在之后多写一些关于这些类型的爬虫的例子,否则光介绍不举例子很难让人接受,写了这么多知识点,不如举一堆例子来的容易。当然举例子靠的也就是这些基础。本笔记参考的是scrapy官方英文文档,我之后也会不断地翻这个笔记,因为这些知识点可能有错误,并且也不是很细,我会及时更改。