目录
一、Spiders 介绍
1-1 Spiders 运作过程
1-2 Spiders 提供的五种模板类
1-2-1 模板类的简单实用
二、scrapy.spiders.Spider 类 - 最基本的类
2-1 属性、方法详解
2-2 简单实例
2-2-0 基础实例
2-2-1 从单个回调中返回多个请求和项目
2-2-2 直接使用start_requests()代替start_urls
三、命令行下给予爬虫程序参数传递
四、基于多个多个爬虫的URL去重
4-1 方法一、新增类方法
4-2 方法二、使用Scrapy自带的去重功能
4-3 方法三、仿照RFPDupeFilter自定义去重规则
4-3-1 源码分析
一、Spiders 介绍
Spiders 官方文档
Spiders是由一系列类(定义了一个网址或一组网址将被爬取)组成,具体包括如何执行爬取任务并且如何从页面中提取结构化的数据。换句话说,Spiders是你为了一个特定的网址或一组网址自定义爬取和解析页面行为的地方
1-1 Spiders 运作过程
1-生成初始的Requests来爬取第一个URLS,并标识一个回调函数
- 第一个请求定义在start_requests()方法内
- 默认从start_urls列表内获取url地址来生成request请求
- 默认的回调函数是parse方法 - 回调函数在下载完成,返回response时自动触发
2- 在回调函数中,解析response并返回返回值
- 四中返回值
- 1- 包含解析数据的字典
- 2- Item对象
- 3- 新的Request对象(新的Requests也需要制定一个回调函数)
- 4- 可迭代对象(包含Items或Request)
3- 在回调函数中解析页面内容
- 通常使用Scrapy自带的Selectors进行解析
- 也可以使用Beutifulsoup、lxml等
4- 针对放回的Items对象,进行持久化(数据库保存)
- 通过Item Pipeline组件存到数据库
- Item Pipeline - 官方文档查询
- 通过Feed exports导出到不同的文件
- Feed exports - 官方文档查看
1-2 Spiders 提供的五种模板类
- scrapy.spiders.Spider
- !!注意:scrapy.Spider 等同于 scrapy.spiders.Spider
- scrapy.spiders.CrawlSpider
- scrapy.spiders.XMLFeedSpider
- scrapy.spiders.CSVFeedSpider
- scrapy.spiders.SitemapSpider
1-2-1 模板类的简单实用
# -*- coding: utf-8 -*- import scrapy from scrapy.spiders import Spider,CrawlSpider,XMLFeedSpider,CSVFeedSpider,SitemapSpider class AmazonSpider(scrapy.Spider): # 自定义类,继承Spiders提供的基类 name = 'amazon' allowed_domains = ['www.amazon.cn'] start_urls = ['http://www.amazon.cn/'] def parse(self, response): pass
二、scrapy.spiders.Spider 类 - 最基本的类
class Spider(object_ref): """Base class for scrapy spiders. All spiders must inherit from this class. 这是最简单的spider类,任何其他的spider类都需要继承它(包含你自己定义的)。 该类不提供任何特殊的功能 它仅提供了一个默认的start_requests方法默认从start_urls中读取url地址发送requests请求 并且默认parse作为回调函数 """ name = None custom_settings = None '''用于从start_urls内获取需要发送请求的地址''' def start_requests(self): cls = self.__class__ if method_is_overridden(cls, Spider, 'make_requests_from_url'): warnings.warn( "Spider.make_requests_from_url method is deprecated; it " "won't be called in future Scrapy releases. Please " "override Spider.start_requests method instead (see %s.%s)." % ( cls.__module__, cls.__name__ ), ) for url in self.start_urls: yield self.make_requests_from_url(url) else: for url in self.start_urls: yield Request(url, dont_filter=True) '''回调函数,子类必须重写这个方法,否侧抛出异常''' def parse(self, response): raise NotImplementedError('{}.parse callback is not defined'.format(self.__class__.__name__))
2-1 属性、方法详解
- name = ‘amazon’ - 设置爬虫名为amazon
- 定义爬虫名,scrapy会根据该值定位爬虫程序;所以它必须要有且必须唯一
- 通常使用爬取网站的域名为爬虫名 a spider that crawls
mywebsite.com
would often be calledmywebsite
.- 注意:In Python 2 this must be ASCII only.
- allowed_domains = ['www.amazon.cn'] - 定义允许爬取的域名
- 如果OffsiteMiddleware启动(默认就启动),那么不属于该列表的域名及其子域名都不允许爬取.
- 如果爬取的网址为:https://www.example.com/1.html,那就添加'example.com'到列表.
- start_urls = ['http://www.amazon.cn/'] - 爬取的url地址列表
- 如果没有指定start_requests,就从该列表中读取url来生成第一个请求,后续请求将从起始URL中包含的数据连续生成。
- custom_settings - 值为一个字典,定义一些配置信息
- 在运行爬虫程序时,这些配置会覆盖项目级别的配置
- custom_settings必须被定义成一个类属性,由于settings会在类实例化前被加载
- settings - 配置信息
- 通过self.settings['配置项的名字'],可以访问settings.py中的配置
- 如果自己定义了custom_settings 还是以custom_settings的为准
- logger
- 使用Spider名称创建的日志。您可以使用它来发送日志消息。
self.logger.debug('=============>%s' %self.settings['BOT_NAME'])
- crawler
- 初始化类后,他的属性由from_crawler()类方法设置,并链接到此spider实例绑定到的Crawler对象。
- Crawlers将项目中的许多组件封装为单一条目访问(例如扩展,中间件,信号管理器等)。
- from_crawler(crawler, *args, **kwargs) - Scrapy用于创建蜘蛛的类方法
- 您可能不需要直接覆盖它,因为默认实现充当__init __()方法的代理,使用给定参数args和命名参数kwargs调用它。
- 尽管如此,此方法在新实例中设置crawler和settings属性,以便稍后可以在spider代码中访问它们。
- 参数
- crawler - crawler to which the spider will be bound
- args (list) - arguments passed to the
__init__()
method- kwargs (dict) - keyword arguments passed to the
__init__()
method- start_requests() - 该方法用来发起第一个Requests请求,且必须返回一个可迭代的对象。
- 它在爬虫程序打开时就被Scrapy调用,Scrapy只调用它一次。
- 默认从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) - 默认回调函数
- 参数 - response (Response) – the response to parse
- 这是Scrapy在其请求未指定回调时处理下载的响应时使用的默认回调。
- 解析方法负责处理响应并返回要删除的数据和/或更多URL。其他请求回调与Spider类具有相同的要求。
- 此方法以及任何其他Request回调,必须返回可迭代的 Request 或 dicts 或 Item 对象。
- log(message[, level, component])
- 通过Spider记录器发送日志消息的包装器,用于向后兼容。
- closed(reason) - 爬虫程序结束时自动触发
2-2 简单实例
2-2-0 基础实例
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)
2-2-1 从单个回调中返回多个请求和项目
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('//h3').extract(): yield {"title": h3} # 返回生成器对象,简化return for url in response.xpath('//a/@href').extract(): yield scrapy.Request(url, callback=self.parse)
2-2-2 直接使用start_requests()代替start_urls
import scrapy from 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) ''' class Request(object_ref): def __init__(self, url, callback=None, method='GET', headers=None, body=None, cookies=None, meta=None, encoding='utf-8', priority=0, dont_filter=False, errback=None, flags=None): priority - 优先级 dont_filter - 是否过滤 errback - 错误回调,即发送错误时请求的回调函数 callback - 成功回调 meta - 可以为请求对象添加的额外的参数 '''
三、命令行下给予爬虫程序参数传递
# 命令行执行,给予参数 category=electronics scrapy crawl myspider -a category=electronics ''' !!注意!!:接收的参数全都是字符串,如果想要结构化的数据,你需要用类似json.loads的方法 如果要从命令行设置start_urls属性,则必须使用ast.literal_eval或json.loads等方法将其自行解析为列表,然后将其设置为属性。 否则,您将导致对start_urls字符串进行迭代(一个非常常见的python陷阱),导致每个字符被视为一个单独的URL。 ''' # 在__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__方法将接受任何spider参数并将它们作为属性复制到spider。 上面的例子也可以写成如下 ''' import scrapy class MySpider(scrapy.Spider): name = 'myspider' def start_requests(self): yield scrapy.Request('http://www.example.com/categories/%s' % self.category)
四、基于多个多个爬虫的URL去重
去重规则应该多个爬虫共享的,但凡一个爬虫爬取了,其他都不要爬了、
4-1 方法一、新增类方法
注意弊端: parse回调由请求的发送而出发,意味着,请求了才能执行去重代码。应该在请求前去拦截相同地址的请求。
''' # 新增类属性 visited=set() # 类属性,使用集合去重 # 回调函数parse方法 def parse(self, response): # 针对url可能过长,所以存放url的hash值 url=md5(response.request.url) if response.url in self.visited: return None # ....... self.visited.add(url) ''' import scrapy from myproject.items import MyItem class MySpider(scrapy.Spider): name = 'example.com' allowed_domains = ['example.com'] # 新增类属性 visited=set() # 类属性,使用集合去重 def parse(self, response): # 针对url可能过长,所以存放url的hash值 url=md5(response.request.url) if response.url in self.visited: print('已经请求过的地址') return [] self.visited.add(url)
4-2 方法二、使用Scrapy自带的去重功能
注意,需要配合start_requests函数使用,即start_url属性无效。
# 配置文件 settings.py ,增加以下三个选项配置 DUPEFILTER_CLASS = 'scrapy.dupefilter.RFPDupeFilter' # 默认的去重规则帮我们去重,去重规则在内存中 DUPEFILTER_DEBUG = False # 是否记录所有的重复请求,默认问第一个重复请求 # JOBDIR = "保存范文记录的日志路径,如:/root/" JOBDIR = "log/getlog/" # 最终路径为 /root/requests.seen,去重规则放文件中 ''' scrapy自带去重规则默认为RFPDupeFilter,只需要我们指定 Request(...,dont_filter=False) ,如果dont_filter=True则告诉Scrapy这个URL不参与去重。 '''
import scrapy from 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/1.html', self.parse) def parse(self, response): if response.url in self.visited: print('已经请求过的地址') return []
请求过的 url 被加密写入 requests.seen,若文件内存在 url 则不会再次爬取,即使数据不同。
4-3 方法三、仿照RFPDupeFilter自定义去重规则
# from scrapy.dupefilter import RFPDupeFilter,看源码,仿照BaseDupeFilter # 步骤一:在项目目录下自定义去重文件dup.py class UrlFilter(object): def __init__(self): self.visited = set() #或者放到数据库 @classmethod def from_settings(cls, settings): return cls() def request_seen(self, request): if request.url in self.visited: return True self.visited.add(request.url) def open(self): # can return deferred pass def close(self, reason): # can return a deferred pass def log(self, request, spider): # log that a request has been filtered pass # 步骤二:配置文件settings.py,使用自定义的去重规则 DUPEFILTER_CLASS = '项目名.dup.UrlFilter'
4-3-1 源码分析
from __future__ import print_function import os import logging from scrapy.utils.job import job_dir from scrapy.utils.request import request_fingerprint class BaseDupeFilter(object): @classmethod def from_settings(cls, settings): return cls() def request_seen(self, request): return False def open(self): # can return deferred pass def close(self, reason): # can return a deferred pass def log(self, request, spider): # log that a request has been filtered pass class RFPDupeFilter(BaseDupeFilter): """Request Fingerprint duplicates filter""" def __init__(self, path=None, debug=False): self.file = None self.fingerprints = set() self.logdupes = True self.debug = debug self.logger = logging.getLogger(__name__) # 地址存储为文件 if path: self.file = open(os.path.join(path, 'requests.seen'), 'a+') self.file.seek(0) self.fingerprints.update(x.rstrip() for x in self.file) @classmethod def from_settings(cls, settings): debug = settings.getbool('DUPEFILTER_DEBUG') return cls(job_dir(settings), debug) def request_seen(self, request): fp = self.request_fingerprint(request) if fp in self.fingerprints: return True self.fingerprints.add(fp) if self.file: self.file.write(fp + os.linesep) def request_fingerprint(self, request): return request_fingerprint(request) def close(self, reason): if self.file: self.file.close() def log(self, request, spider): if self.debug: msg = "Filtered duplicate request: %(request)s" self.logger.debug(msg, {'request': request}, extra={'spider': spider}) elif self.logdupes: msg = ("Filtered duplicate request: %(request)s" " - no more duplicates will be shown" " (see DUPEFILTER_DEBUG to show all duplicates)") self.logger.debug(msg, {'request': request}, extra={'spider': spider}) self.logdupes = False spider.crawler.stats.inc_value('dupefilter/filtered', spider=spider)
源码的规则选择
''' from scrapy.core.scheduler import Scheduler 见Scheduler下的enqueue_request方法: self.df.request_seen(request) ''' class Scheduler(object): def enqueue_request(self, request): if not request.dont_filter and self.df.request_seen(request): self.df.log(request, self.spider) return False dqok = self._dqpush(request) if dqok: self.stats.inc_value('scheduler/enqueued/disk', spider=self.spider) else: self._mqpush(request) self.stats.inc_value('scheduler/enqueued/memory', spider=self.spider) self.stats.inc_value('scheduler/enqueued', spider=self.spider) return True