逆向爬虫19 Scrapy增量式和分布式

逆向爬虫19 Scrapy增量式和分布式

一、增量式爬虫

1. 场景引入

设想一个场景,你想获取一个论坛的某个板块上所有的帖子信息,但这个板块每时每刻都有可能会有新帖子被发出来。因此,每当有帖子更新,你就得启动一次爬虫爬取该板块的所有帖子数据。如果使用之前的技术来实现这个需求,会有个 问题,就是 每次板块更新时,你的爬虫程序会把当前所有的帖子都抓取一遍,里面会包含很之前爬过重复的数据。 因此我们需要设计一种新的爬虫模式,每当我们要抓取URL时,先判断一下该URL历史上有没有被抓取过,如果已经抓取过了,则跳过,若没抓过,则正常抓取它。该需求场景下设计的爬虫就是 增量式爬虫,即每次只爬取增加的数据。

2. 原理分析

从上述场景引入可知,实现这个功能的关键在于,怎么才能让爬虫在抓取URL时,知道历史上是否有抓取过。Scrapy本身的 调度器 模块具备URL去重功能,但是它仅限Scrapy一次运行之中,多次启动同一个Scrapy程序时,它无法知道上一次启动时爬取过哪些URL。因此需要考虑使用第三方工具,来帮助实现该功能。这里我们打算使用Redis来保存每次抓取过的URL信息,因为Redis工作时时存储在内存中的数据库,因此它的速度相比于MySQL,MongoDB要快很多,这里需要保存的是控制程序运行状态的标志信息,数据量远比真实要抓取的数据少,因此使用Redis既可满足速度,又能实现功能。

3. 流程框图

在这里插入图片描述

4. 开始动手

抓取天涯国际观察板块内的帖子数据

scrapy startproject tianya
cd tianya
scrapy genspider ty

ty.py文件

在这里插入图片描述

ty.py源码

import scrapy
from scrapy.linkextractors import LinkExtractor
from tianya.items import TianyaItem
from redis import Redis

class TySpider(scrapy.Spider):
    name = 'ty'
    allowed_domains = ['tianya.cn']
    start_urls = ['http://bbs.tianya.cn/list-worldlook-1.shtml']

    def __init__(self, name=None, **kwargs):
        self.red = Redis(host='localhost', port=6379, db=1)
        # 让父类能初始化
        super(TySpider, self).__init__(name, **kwargs)

    def parse(self, resp):
        # print(resp.xpath('//title/text()').extract_first())
        le = LinkExtractor(restrict_xpaths=(
            '//td[@class="td-title faceblue"]/a',))
        links = le.extract_links(resp)
        for link in links:
            # print(link.url)     # 打印下看看
            # 1.直接往redis里set集合怼
            # 进入到详情页的条件是: 我在redis里面没有存储过该url
            # 2. 在这里判断是否存在该元素
            result = self.red.sismember("tianya:ty:detail:url", link.url)
            if result:
                print(f"该url已经被抓取过了 {link.url}")
            else:
                yield scrapy.Request(
                    url=link.url,
                    callback=self.parse_detail
                )

        # 有重复的数据产生了.
        # 如何来进行去除重复的问题
        # 1. 使用python的set集合来去重.
        # 2. 推荐使用redis的set集合去除重复
        # 用redis有两个方案去除重复
        # 1. url , 优点: 简单, 缺点. 如果url内部进行了更新. 你可能会忽略掉一些数据
        # 2. 数据 , 优点: 准确性高,  缺点. 如果数据集非常庞大. 对于redis而言是很不利的.

        # 考虑分页
        le = LinkExtractor(restrict_xpaths=("//div[@class='short-pages-2 clearfix']/div/a[last()]",))
        link = le.extract_links(resp)[0].url
        if link:
            yield scrapy.Request(
                url=link,
                callback=self.parse
            )
        else:
            print("所有分页已经爬光了!")

    def parse_detail(self, resp):
        title = resp.xpath(
            '//span[@class="s_title"]/span/text()').extract_first().strip()
        content = resp.xpath(
            '//div[@class="bbs-content clearfix"]/text()').extract_first().strip()
        item = TianyaItem()
        item['title'] = title
        item['content'] = content
        self.red.sadd("tianya:ty:detail:url", resp.url)
        return item

piplines.py源码(保存数据到MySQL,用于和Redis中保存的URL数量对比是否一致)

from itemadapter import ItemAdapter
import pymysql
from tianya.settings import MYSQL

class TianyaPipeline:
    def open_spider(self, spider):
        self.conn = pymysql.connect(
            host=MYSQL['host'],
            port=MYSQL['port'],
            user=MYSQL['user'],
            password=MYSQL['password'],
            database=MYSQL['database']
        )

    def close_spider(self, spider):
        if self.conn:
            self.conn.close()

    def process_item(self, item, spider):
        print(item['title'], item['content'])
        try:
            cursor = self.conn.cursor()
            sql = "insert into ty (title, content) values (%s, %s)"
            cursor.execute(sql, (item['title'], item['content']))
            self.conn.commit()
        except:
            self.conn.rollback()
        finally:
            if cursor:
                cursor.close()
        return item

5. 小结

增量式爬虫比较简单,就是在爬取之前和爬取之后,增加了数据库操作,这里可以把Redis换成其他任意数据库。

二、分布式爬虫

1. 什么是分布式爬虫?

分布在不同区域的多台计算机(公网IP不同)联合起来完成同一个爬虫任务的爬虫程序叫分布式爬虫。

2. 为什么要分布式爬虫?

单台计算机的能力是有限的,当我们希望可以更快地完成某些爬虫任务时,可以考虑使用分布式爬虫。

3. 原理分析

之前我们写的都是单机爬虫程序,我们只能控制一个Scrapy程序对某个网站进行爬取,现在我们希望可以在多个不同的计算机上运行Scrapy程序来完成爬虫工作。试想一下我们会遇到什么问题?

由于我们每个Scrapy程序时独立的,因此不同的程序之间会爬取到重复的数据,这样即使架好集群,也无法发挥出多台计算机并行爬取网站的优势。因此我们必须让不同的Scrapy程序可以对不同的URL发起请求,如何才能实现这个功能呢?我们需要回到Scrapy的工作流程,对 调度器 的工作原理做进一步的研究。

单机程序下,Scrapy 调度器爬虫 这里获取到start_url的request请求,再交给 下载器 ,由 下载器 对目标URL进行HTTP请求。现在我们希望多台计算机联合起来工作,如果每台计算机拥有自己各自的 调度器 的话,相互之间就没有任何关联,自顾自地调度URL地分配。因此考虑将多台计算机地Scrapy程序的调度器统一起来,由一台机器来负责 调度 分配URL,剩下的机器负责下载,解析,以及存储。

在这里插入图片描述

上图是Scrapy分布式爬虫的原理图,上方的蓝色长方形就是负责给所有其他计算机分配URL的 调度器,里面包含四个东西:

  1. 去重集合,和单机版Scrapy一样,用于过滤掉重复的URL。
  2. 请求队列,其它所有计算机共享一个请求队列,因此相互不会抓重复的数据。
  3. redis_key,和单机版程序中的start_urls是一个意思,用于给分布式爬虫指定起始URL的。
  4. 数据队列 (可选),方便数据整合,如果没有则数据分布在每个计算机之上,后期需要手动整合。

总的来说,分布式爬虫就是把调度器分离出去,一般我们用Scrapy_redis来做这个调度器,剩下的部分都可以不变。

4. 开始动手

目标依然是天涯国际观察板块内的帖子数据,因此没有贴出来的文件都是一样的

scrapy startproject tianya2
cd tianya2
scrapy genspider ty

ty.py文件

在这里插入图片描述

settings.py文件

在这里插入图片描述

ty.py源码

import scrapy
from scrapy_redis.spiders import RedisSpider, RedisCrawlSpider
from scrapy.linkextractors import LinkExtractor
from tianya2.items import TianyaItem

class TySpider(RedisSpider):
    name = 'ty'
    allowed_domains = ['tianya.cn']
    # start_urls = ['http://tianya.cn/']
    redis_key = "ty_start_url"

    def parse(self, resp):
        # print(resp.xpath('//title/text()').extract_first())
        le = LinkExtractor(restrict_xpaths=(
            '//td[@class="td-title faceblue"]/a',))
        links = le.extract_links(resp)
        for link in links:
            # print(link.url)     # 打印下看看
            # 这里不用管是否重复,所有的判断工作,全部交给scrapy_redis完成
            yield scrapy.Request(
                url=link.url,
                callback=self.parse_detail
            )

        # 考虑分页
        le = LinkExtractor(restrict_xpaths=("//div[@class='short-pages-2 clearfix']/div/a[last()]",))
        link = le.extract_links(resp)[0].url
        if link:
            yield scrapy.Request(
                url=link,
                callback=self.parse
            )
        else:
            print("所有分页已经爬光了!")

    def parse_detail(self, resp):
        title = resp.xpath('//span[@class="s_title"]/span/text()').extract_first().strip()
        content = resp.xpath('//div[@class="bbs-content clearfix"]/text()').extract_first().strip()
        item = TianyaItem()
        item['title'] = title
        item['content'] = content
        return item

settings.py源码

BOT_NAME = 'tianya2'

SPIDER_MODULES = ['tianya2.spiders']
NEWSPIDER_MODULE = 'tianya2.spiders'

ROBOTSTXT_OBEY = False
LOG_LEVEL = "WARNING"

# 配置MySQL
MYSQL = {
    'host':'localhost',
    'port':3306,
    'user':'root',
    'password':'xxxxxx',
    'database':'tianya'
}

ITEM_PIPELINES = {
    'scrapy_redis.pipelines.RedisPipeline': 301,    # 可选项
    'tianya2.pipelines.Tianya2Pipeline': 300,
}

# redis相关配置
REDIS_HOST = "127.0.0.1"
REDIS_PORT = 6379
REDIS_DB = 2
# REDIS_PARAMS = {
#     "password":"123456"
# }

# scrapy_redis相关配置
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
SCHEDULER_PERSIST = True  # 如果为真. 在关闭时自动保存请求信息, 如果为假, 则不保存请求信息
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" # 去重的逻辑. 要用redis的

启动程序

开启三个命令行终端,两个输入Scrapy crawl ty
一个进入redis指定redis_key
PS C:\Users\Apphao> redis-cli
127.0.0.1:6379> select 2
OK
127.0.0.1:6379[2]> lpush ty_start_url http://bbs.tianya.cn/list-worldlook-1.shtml
(integer) 1
127.0.0.1:6379[2]>
指定好redis_key后另外两个终端就会联合爬取目标网站了

5. 小结

利用Scrapy_redis来完成Scrapy中的 调度器 的工作,在传统单机Scrapy程序上增加了3个功能。

  1. 增量式爬虫
  2. 断点续爬
  3. 分布式爬虫

修改代码的地方很少,大部分是在配置的文件中。


版权声明:本文为weixin_40743639原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。