5.scrapy过滤器

scrapy过滤器

1. 过滤器

当我们在爬取网页的时候可能会遇到一个调转连接会在不同页面出现,这个时候如果我们的爬虫程序不能识别出

该链接是已经爬取过的话,就会造成一种重复不必要的爬取。所以我们要对我们即将要爬取的网页进行过滤,把重

复的网页链接过滤掉。

2. 指纹过滤器

去重处理可以避免将重复性的数据保存到数据库中以造成大量的冗余性数据。不要在获得爬虫的结果后进行内容过

滤,这样做只不过是避免后端数据库出现重复数据。

去重处理对于一次性爬取是有效的,但对于增量式爬网则恰恰相反。对于持续性长的增量式爬网,应该进行”前置过

滤”,这样可以有效地减少爬虫出动的次数。在发出请求之前检查询爬虫是否曾爬取过该URL,如果已爬取过,则让爬

虫直接跳过该请求以避免重复出动爬虫。

Scrapy 提供了一个很好的请求指纹过滤器(Request Fingerprint duplicates filter)

scrapy.dupefilters.ReppupeFilter ,当它被启用后,会自动记录所有成功返回响应的请求的URL,并将其以文件

(requests.seen)方式保存在项目目录中。请求指纹过滤器的原理是为每个URL生成一个指纹并记录下来,一旦

当前请求的URL在指纹库中有记录,就自动跳过该请求。

默认情况下这个过滤器是自动启用的,当然也可以根据自身的需求编写自定义的过滤器。

默认过滤器开启的地方:

    def start_requests(self):
        
        for url in self.start_urls:
            # 每一个url封装成Request对象,交给调度器
            # 这里的dont_filter=True 就是默认开启scrapy自带的过滤器
            yield Request(url, dont_filter=True)  

2.1 过滤器源码

2.1.1 基本过滤器

这是一个基类,scrapy自带的指纹过滤器就是继承这个类,然后重写这些方法实现的。

如果你经常看源码的话,你话发现很多功能的实现都有一个最基础的基类,然后实现功能的那个类,继承它

并对他的方法进行重写。scrapy框架中就是这样的。

class BaseDupeFilter:
    # 基本的过滤器

    @classmethod
    def from_settings(cls, settings):
        """这个方法可以从settings.py中获取数据"""
        return cls()

    def request_seen(self, request):
        # 对 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

2.2.2 默认的过滤器

继承BaseDupeFilter类,然后对内部重写。

class RFPDupeFilter(BaseDupeFilter):
    """Request Fingerprint duplicates filter 指纹过滤器 对整个request的去重"""
    # 默认的话是一个指纹过滤器, 会对整个request对象进行过滤 (url/method/params...)
    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):
        # 拿到request 传到 request_fingerprint方法 摘要出来的指纹 fp
        fp = self.request_fingerprint(request)
        # 如果指纹在集合中
        if fp in self.fingerprints:
            # 返回 True 
            return True
        # 不在就追加到集合
        self.fingerprints.add(fp)
        if self.file:
            # 指纹写入到文件
            self.file.write(fp + '\n')

    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 (referer: %(referer)s)"
            args = {'request': request, 'referer': referer_str(request)}
            self.logger.debug(msg, args, 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)

执行流程:

细心的小伙伴可能发现了,这个和pipeline类源码的执行流程差不多,对没错就是差不多。

  1. 执行 from_settings(cls, settings): 这个类方法
  2. 执行 _ _init _ _ (self, path=None, debug=False): 实例化对象
  3. 执行 request_seen(self, request): 拿到由请求生产的指纹
  4. 执行 log(self, request, spider): 记录日志
  5. 执行 close(self, reason): 关闭记录指纹的文件

由于 scrapy.dupefilters.RFPDupeFilter 采用文件方式保存指纹库,对于增量爬取且只用于短期运行的项目还能
应对。一旦遇到爬取量巨大的场景时,这个过滤器就显得不太适用了,因为指纹库文件会变得越来越大,过滤器在启动时会一次性将指纹库中所有的URL读入,导致消耗大量内存。

所以我们情况下,在使用scrapy过滤器的时候,都是自己重新自定义。

3. 自定义过滤器

虽然自带的过滤器不好用,但是我们可以用Scrapy提供的 request_fingerprint 函数为请求生成指纹,然后将

指纹写入内存,这样会从内存中存取数据会很快。然后写好的这个类,位置可以随便放,但是一定要在settings.py

文件中从新指定过滤器。

# 这里是放在了当前项目的中间件里面了.
DUPEFILTER_CLASS = 'qd_04_english.middlewares.URLFilter'
# 过滤器先启动,再执行爬虫
import hashlib
from scrapy.dupefilters import BaseDupeFilter


class URLFilter(BaseDupeFilter):
    """根据URL过滤"""

    @classmethod
    def from_settings(cls, settings):
        # 从settings里面取到配置文件
        debug = settings.getbool('DUPEFILTER_DEBUG')
        return cls()

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 过滤url的集合 
        self.url_set = set()


    def request_seen(self, request):
        """对每一个请求进行过滤"""
        url = self.request_fingerprint(request)
        if url in self.url_set:
            # 返回True就代表这个url已经被请求过了
            return True
        else:
            self.url_set.add(request.url)

    def request_fingerprint(self, request):
       	# 返回由url摘要后的字符串
        return hashlib.md5(request.url.encode()).hexdigest()

注意:start_urls 中的请求,默认是不过滤的。

4. 总结

之前我们在管道中,讲到的数据去重,是对结果的去重,这里我们讲的过滤是对请求的去重。

一定一定要会看源码,会自定义一些组件。因为自带的公司一般都不会用的,因为不好用。