本文为译文,原文见地址:https://docs.scrapy.org/en/latest/topics/spiders.html
爬虫
爬虫是一些类,这些类定义了如何对某个站点(或一组站点)进行抓取,包括如何执行抓取(即跟踪链接)以及如何从其页面中提取结构化数据(即抓取项)。换句话说,爬虫是为特定站点(或者在某些情况下是一组站点)爬行和解析页面定义自定义行为的地方。
对于爬虫,循环爬取的流程大致如下:
- 首先,生成初识请求来抓取第一个URL,然后指定一个回调函数,使用从这些请求下载的响应来调用该回调函数。
首个执行的请求是通过调用start_requests()方法发出的,该方法(默认情况下)为start_urls中指定的url生成请求,并将parse函数作为请求的回调函数。 - 在回调函数中,你可以解析响应(web页面),并且返回提取数据后组成的字典,Item对象,Request对象,或者这些对象的迭代器。这些请求将包含一个回调函数(可能与之前的相同),并且随后将由Scrapy进行下载,然后它们的响应将会被回调函数所处理。
- 在回调函数中,你解析了页面的内容,最典型的是使用选择器(但是你也可以使用BeautifulSoup,lxml或者其他你更加喜欢的方式),然后使用解析的数据来生成数据项。
- 最后,从爬虫中返回的数据项,一般会存储到一个数据库中(在数据项管道中)或者使用Feed exports写入到一个文件中。
尽管这个循环(或多或少)适用于任何类型的爬虫,但是出于不同的目的,将不同类型的默认爬虫绑定到Scrapy中。我们将在后续讨论这些类型。
scrapy.Spider
class scrapy.spiders.Spider
这是最简单的爬虫,也是其他所有爬虫都必须继承的爬虫(包括与Scrapy绑定的爬虫,以及你自己编写的爬虫)。这个爬虫并没有提供其他特别的功能,只是提供了默认的start_requests()执行方法,这个方法从爬虫的start_urls属性发送请求,并且对每一个响应结果调用爬虫的parse函数。
name
当前爬虫的名称字符串。这个名称是Scrapy用来定位(以及实例化)爬虫的,因为这个名称必须是唯一的。然而,没有什么可以阻止你对同一个爬虫进行多次实例化。这是爬虫最重要的属性且这也是必须的。
如果爬虫爬取的是单一网域,一个通用经验是,不管有没有TLD,都用域来命名爬虫。
注意,在Python2中,这个字符串只能是ASCII编码。
allowed_domains
一个可选列表,这个列表包含了一些域的字符串,表示爬虫可以爬取的域。如果OffsiteMiddleware是开启的,那么将不追踪对不属于此列表中指定的域名(或其子域名)的URL请求。
如果你的目标url是https://www.example.com/1.html,那么将添加’example.com’到这个列表中。
start_urls
当没有指定特定URL时,爬虫将从其中开始爬取的URL列表。因此,下载的第一个页面将在这里列出,后续请求将从开始URL中包含的数据连续生成。
custom_settings
一个配置项的字典,在运行这个爬虫的时候,这个字典将被项目级的配置给覆盖。它将被定义为类属性,因为在实例化之前已经更新了配置项。
有关可用内置配置的列表,请查阅:内置配置引用。
crawler
这个属性是在初始化类之后由类方法from_crawler()设置的,并链接到爬虫对象绑定的Crawler对象。
爬虫程序在项目中封装了很多组件,用于它们的单一入口访问(如扩展、中间件、信号管理器等)。有关它们的更多信息,请参见Crawler API。
settings
运行此爬虫的配置。这是一个Settings实例,有关这个主题的详细介绍,请参阅Settings主题。
logger
用爬虫的名称创建的Python的日志记录器。你可以使用它发送日志消息,就像爬虫日志记录所描述的那样。
from_crawler(crawler, *args, **kwargs)
Scrapy使用这个类函数来创建你的爬虫。
你可能不需要直接重写这个类函数,因为它的默认实现表现得像是__init__()函数的代理,调用这个函数需要给定参数args和命名参数kwargs。
但是,这个函数在新的实例中设置了crawler和settings属性,以便稍后可以在爬虫的代码中访问它们。
参数:
- crawler(Crawler实例)-爬虫将会绑定的爬行器。
- args(list)- 传递给__init__()函数的参数。
- kwargs(dict)- 传递给__init__()函数的关键字参数。
start_requests()
此函数必须返回一个可迭代的,携带了该爬虫爬行的第一个请求。当为了爬取而打开爬虫时,这个方法将被Scrapy调用。Scrapy只调用它一次,因此将start_requests()实现为生成器是安全的。
在这个函数中,默认为start_urls中的每一个URL生成Request(url, dont_filter=True)。
如果你希望更改用于开始抓取域的请求,则需要重写此方法。例如,如果你需要从使用POST请求登录开始,你可以这样做:
class MySpider(scrapy.Spider):
name = 'myspider'
def start_requests(self):
return [scrapy.FormRequest('http://www.example.com/login',
formdata={'user': 'john', 'pass': 'secret'},
callback=self.logged_in)]
def logged_in(self, response):
# here you would extract links to follow and return Requests for
# each of them, with another callback
pass
parse(response)
如果请求没有指定回调函数,那么将使用Scrapy默认的回调函数,用于处理下载的响应。parse函数则为Scrapy默认的回调函数。
parse函数负责处理响应以及返回爬取到的数据和/或更多的需要跟踪的URL。其他的请求回调函数拥有与Spider类相同的要求。
这个函数以及其他请求的回调函数,都必须返回一个Request的迭代器以及/或者字典或者Item对象。
参数:
response(Response) - 解析的响应对象。
log(message[, level, component])
通过爬虫的日志记录器发送日志消息的包装器,保持了向后兼容性。有关更多信息,请参见爬虫的日志记录。
closed(reason)
当爬虫关闭时调用。这个函数为signals.connect()提供了一个spider_closed信号。
示例:
import scrapy
class MySpider(scrapy.Spider):
name = 'example.com'
allowed_domains = ['example.com']
start_urls = [
'http://www.example.com/1.html',
'http://www.example.com/2.html',
'http://www.example.com/3.html',
]
def parse(self, response):
self.logger.info('A response from %s just arrived!', response.url)
从单个回调函数中返回多个Request和item:
import scrapy
class MySpider(scrapy.Spider):
name = 'example.com'
allowed_domains = ['example.com']
start_urls = [
'http://www.example.com/1.html',
'http://www.example.com/2.html',
'http://www.example.com/3.html',
]
def parse(self, response):
for h3 in response.xpath('//a/@href').extract():
yield scrapy.Request(url, callback=self.parse)
你也可以使用start_requests()替换start_urls;为了使数据更加结构化,你可以使用Items:
import scrapy
form myproject.items import MyItem
class MySpider(scrapy.Spider):
name = 'example.com'
allowed_domains = ['example.com']
def start_requests(self):
yield scrapy.Request('http://www.example.com/1.html', self.parse)
yield scrapy.Request('http://www.example.com/2.html', self.parse)
yield scrapy.Request('http://www.example.com/3.html', self.parse)
def parse(self, response):
for h3 in response.xpath('//h3').extract():
yield MyItem(title=h3)
for url in response.xpath('//a/@href').extract():
yield scrapy.Request(url, callback=self.parse)
爬虫参数
爬虫可以接受参数来修改它们自身的行为。爬虫参数的一些常见用途是定义起始URL或者将爬行器限制在站点的某些部分,但是它们可以用于配置爬虫的任何功能。
爬虫参数是通过crawl命令的-a选项传递的。举个栗子:
scrapy crawl myspider -a category=electronics
爬虫可以在它们的__init__函数中访问参数:
import scrapy
class 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]
# ...
默认__init__函数将携带一些爬虫参数并且复制这些爬虫参数给爬虫,作为了爬虫的属性。上面所有的示例也可以写成下面这样:
import scrapy
class MySpider(scrapy.Spider):
name = 'myspider'
def start_requests(self):
yield scrapy.Request('http://www.example.com/categories/%s' % self.category)
请记住,爬虫参数只能是字符串类型。爬虫本身不会对爬虫参数做任何的解析。如果你想从命令行设置start_urls属性,你必须自己解析爬虫参数字符串,比如使用ast.literal_eval或者json.loads,然后将解析的结果放入一个列表中,最后设置这个列表为start_urls属性。否则,你将会对start_urls字符串进行了迭代(一个非常常见的Python陷阱),导致每个字符串都会被视为一个单独的URL。
一个合法的使用案例是,设置HttpAuthMiddleware所使用的http身份验证凭证或者UserAgentMiddleware所使用的用户代理。
scrapy crawl myspider -a http_user=myuser -a http_pass=mypassword -a user_agent=mybot
爬虫参数也可以通过Scrapyd的schedule.json这个API进行传递。详情见Scrapyd documentation。
常用爬虫
Scrapy自带了一些很有用的通用爬虫,你可以直接继承这些通用爬虫来实现你自己的爬虫。这些爬虫旨在为一些通常的爬取情景提供便利的功能,比如依据一个确定的规则追踪站点的所有链接,从Sitemaps爬取数据,或者解析XML/CSV提要。
在后面举例的爬虫中,我们假设项目的myproject.items模块中已经有一个TestItem的声明:
import scrapy
class TestItem(scrapy.Item):
id = scrapy.Field()
name = scrapy.Field()
description = scrapy.Field()
CrawlSpider
class scrapy.spiders.CrawlSpider
这是最常用的爬虫,用于爬行常规网站,因为它通过定义一组规则为追踪链接提供了方便的机制。它可能不是最适合你特定的web站点或项目的,但是它对于一些情况来说是通用的,所以你可以从它开始,并根据需求来重写它,以实现更多定制功能,或者只实现你自己的spider。
除了从Spider类继承的属性之外(必须指定),这个类还支持一个新的属性。
rules
一组Rule对象(一个或多个)。每一个Rule为爬行站点定义了一个确定的行为。后面会有Rule对象的描述。如果同一个链接匹配多个规则,那么将使用匹配的第一个规则,这里说的第一个是指在rules这个属性中定义的顺序。
这个爬虫类同时公开了一个可重写的函数:
parse_start_url(response)
这个函数将用于start_urls对应的响应。这个函数允许解析初始的响应且必须返回一个Item对象,一个Request对象,或者包含Item对象和Request对象的迭代器。
爬行规则
class scrapy.spiders.Rule(link_extractor, callback=None, cb_kwargs=None, follow=None, process_links=None, process_request=None)
- link_extractor是一个Link Extractor对象,这个对象定义了如何从每个爬行的页面提取出链接。
- callback是一个可调用的函数或者一个字符串(在这种情况下,将使用来自spider对象的同名方法),主要用于使用指定的link_extractor来提取每个链接。这个回调接收一个响应作为它的第一个参数,并且这个回调必须返回一个包含了Item和/或Request对象(或者其他这两个对象类的派生类对象)的列表。
注意:当编写爬行规则时,避免使用parse作为回调,因为CrawlSpider使用了parse函数来实现其逻辑。因此如果你重写了parse函数,那么CrawlSpider将停止工作。
- cb_kwargs是一个字典,它包含的关键字参数将被传递给回调函数。
- follow是一个布尔类型,它指定了是否应该对这个规则提取的每个响应进行追踪。如果callback为None,则follow默认为True,否则follow默认为False。
- process_links是一个可调用的函数,或者一个字符串(在这种情况下,将使用来自spider对象的同名方法),在如下情况时被调用:通过使用指定的link_extractor,从每个响应中提取到每个链接列表时。这主要用于过滤的目的。
- process_request是一个可调用的函数,或者一个字符串(在这种情况下,将使用来自spider对象的同名方法),它将与此规则提取的每个请求一起调用,并且必须返回一个请求或者None(为了过滤请求)。
CrawlSpider示例
示例如下:
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
class TestcrawlSpider(CrawlSpider):
name = 'TestCrawl'
allowed_domains = ['example.com']
start_urls = ['http://www.example.com/']
rules = (
# 提取匹配了'category.php'的链接(但是不匹配'subsection.php')
# 从这些链接中继续追踪连接(因为没有回调意味着follow=True)
Rule(LinkExtractor(allow=('category\.php', ), deny=('subsection\.php', ))),
# 提取匹配了'item.php'的链接,并且使用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响应,使用XPath从HTML中提取一些数据,并将这些数据填充到Item对象中。
XMLFeedSpider
class scrapy.spiders.XMLFeedSpider
XMLFeedSpider是通过按特定节点名迭代XML提要来解析XML提要的。iterator(迭代器)参数可以是如下值:iternodes,xml,html。出于性能考虑,建议使用iternodes迭代器,因为xml和html迭代器会立即生成整个DOM来进行解析。然而,当解析一个标记有问题的XML时(比如缺少结束标签,等等),使用html作为迭代器可能是有用的。
为了设置迭代器和标签名称,你必须定义下面的类属性:
- iterator
一个字符串,用来定义要使用的迭代器类型。可以是如下值:- ‘iternodes’ - 基于正则表达式的快速迭代器
- ‘html’ - 使用选择器(Selector)的迭代器。请记住一点,这个迭代器将使用DOM解析,并且会一次加载所有DOM到内存中,若解析的源很大的话,这将是一个很大的问题。
- ‘xml’ - 使用选择器(Selector)的迭代器。请记住一点,这个迭代器将使用DOM解析,并且会一次加载所有DOM到内存中,若解析的源很大的话,这将是一个很大的问题。
默认使用’iternodes’。
- itertag
一个字符串,表示要迭代的节点(或者元素)的名称。
示例:
itertag = 'product'
- namespaces
一个含有元组(prefix, uri)的列表,这些元组定义了文档中可用的名称空间,这些名称空间将由这个爬虫处理。使用register_namespace()函数自动注册名称空间时会使用prefix和uri。
你可以稍后在itertag属性中指定带有命名空间的节点。
示例:
class YourSpider(XMLFeedSpider):
namespaces = [('n', 'http://www.sitemaps.org/schemas/sitemap/0.9')]
itertag = 'n:url'
# ...
除了上述的新属性以外,这个爬虫类还有如下需要重写的函数:
- adapt_response(response)
这个函数在爬虫开始解析响应之前,从爬虫中间件收到响应时调用。这个函数可以用来在解析响应之前,对响应进行修改。这个函数接收一个响应,同时也返回一个响应(这个响应可能与接收的相同,也可能是修改后的)。 - parse_node(response, selector)
这个函数用来匹配提供的标签名称(itertag)的节点。函数接收了每个节点的响应和选择器(selector)。重写此方法是必须的。否则,你的爬虫将不会工作。此函数必须返回一个Item对象,一个Request对象,或者是含有Item对象和Request对象任意一个的迭代器。 - process_results(response, results)
对于爬虫返回的每个结果(item或者request),将调用此函数,并且这个函数打算在结果返回到框架核心之前执行所需的任何最后一次处理,例如设置item的id。它接收到一个列表类型的结果,以及这些结果的原始响应对象。它必须返回类型为列表的结果(Item对象集合或者Request对象集合)。
XMLFeedSpider示例
示例如下:
from scrapy.spiders import XMLFeedSpider
from ..items import TestItem
class TestxmlfeedSpider(XMLFeedSpider):
name = 'TestXMLFeed'
allowed_domains = ['example.com']
start_urls = ['http://www.example.com/feed.xml']
iterator = 'iternodes' # 实际上这不是必须的,因为它是默认值
itertag = 'item'
def parse_node(self, response, selector):
self.logger.info('Hi, this is a <%s> node!: %s', self.itertag, ''.join(selector.extract()))
item = TestItem()
item['id'] = selector.xpath('@id').extract()
item['name'] = selector.xpath('name').extract()
item['description'] = selector.xpath('description').extract()
return item
基本上,我们在上面的操作就是创建一个爬虫,它从给定的start_urls下载提要,然后遍历每个item标签,打印它们,并在Item对象中存储一些随机数据。
CSVFeedSpider
class scrapy.spiders.CSVFeedSpider
这个爬虫与XMLFeedSpider非常类似,除了它遍历的是行而不是节点。在每次迭代中调用的函数是parse_row()。
- delimiter
CSV文件中每个字典的分隔符字符串,默认是","(逗号)。 - quotechar
CSV文件中每个字段的表示字符串的外部符号,默认为’"’(双引号)。 - headers
CSV文件中列名称的列表。 - parse_row(response, row)
接收一个响应和一个字典(表示每行),每个关键字用于提供(或者检测到)CSV文件的头。这个爬虫还提供了重写adapt_response和process_results函数的机会,用于预处理和后续处理目的。
CSVFeedSpider示例
from scrapy.spiders import CSVFeedSpider
from ..items import TestItem
class TestcsvfeedSpider(CSVFeedSpider):
name = 'TestCSVFeed'
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['url'] = row['url']
item['name'] = row['name']
item['description'] = row['description']
return item
SitemapSpider
class scrapy.spiders.SitemapSpider
SitemapSpider允许你通过使用站点地图(Sitemaps)发现URL来爬行站点。
这个类支持嵌套站点地图以及从robot.txt中发现站点地图的URL。
- sitemap_urls
是一个URL列表,指向了你希望爬行的URL的站点地图。
你也可以指向一个robot.txt,它将被解析,从而提取出站点地图的URL。 - sitemap_rules
元组(regex, callback)的列表:- regex是一个正则表达式,用来匹配从站点地图提取出的URL。regex可以是一个字符串,也可以是一个编译了的regex对象。
- callback是一个回调函数,用来处理与正则表达式匹配成功的URL。callback可以是一个字符串(指定了爬虫中的某个函数名称),也可以是一个函数。
举个例子:
规则是按顺序提交的,并且使用的永远是第一个匹配成功的项。sitemap_rules = [('/product/', 'parse_product')]
如果你忽略了这个属性,在站点地图中找到的所有URL都会由parse回调函数处理。 - sitemap_follow
应该遵循的站点地图的正则表达式列表。这仅适用于使用指向其他站点地图文件的站点索引文件(Sitemap index files)的站点。
默认情况下,所有站点都会被追踪。 - sitemap_alternate_links
指定是否应该遵循一个url的备用链接。这些链接是同一个网站在同样的url块中以另一种语言传递的。
举个例子:
当sitemap_alternate_links设置后,所有URL都将被检索。当sitemap_alternate_links不可用,只有http://example.com/会被检索。<url> <loc>http://example.com/</loc> <xhtml:link rel="alternate" hreflang="de" href="http://example.com/de"/> </url>
默认情况下,sitemap_alternate_links不可用。
SitemapSpider示例
最简单的示例:使用parse回调函数处理所有通过站点地图发现的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 ...
对于匹配规则不同的url使用不同的回调函数:
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 ...
遵循robot.txt文件中定义的站点地图,只追踪url包含/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与其他来源的URL:
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 ...