基于 Scrapy-redis 的分布式爬虫详细设计

Posted 时间@煮雨~

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于 Scrapy-redis 的分布式爬虫详细设计相关的知识,希望对你有一定的参考价值。

基于 Scrapy-redis 的分布式爬虫设计

 

目录


前言

在本篇中,我假定您已经熟悉并安装了 Python3。 如若不然,请参考 Python 入门指南

关于 Scrapy

Scrapy 是一个为了爬取网站数据,提取结构性数据而编写的应用框架。 可以应用在包括数据挖掘,信息处理或存储历史数据等一系列的程序中。

其最初是为了 网络抓取 所设计的, 也可以应用在获取 API 所返回的数据(例如 Amazon Associates Web Services ) 或者通用的网络爬虫。

架构概览

 
Paste_Image.png

安装

环境

  

Redis 3.2.5
Python 3.5.2
    Scrapy 1.3.3
    scrapy-redis 0.6.8
    redis-py 2.10.5
    Pymysql 0.7.10
    SQLAlchemy 1.1.6

 

Debian / Ubuntu / Deepin 下安装

安装前你可能需要把 Python3 设置为默认的 Python 解释器,或者使用 virtualenv 搭建一个 Python 的虚拟环境,篇幅有限,此处不再赘述。

安装 Redis

sudo apt-get install redis-server

 

安装 Scrapy

sudo apt-get install build-essential libssl-dev libffi-dev python-dev
sudo apt install python3-pip
sudo pip install scrapy scrapy-reids

 

安装 scrapy-redis

sudo pip install scrapy-reids

 

Windows 下安装

由于目前 Python 实现的一部分第三方模块在 Windows 下并没有可用的安装包,个人并不推荐以 Windows 作为开发环境。

如果你非要这么做,你可能会遇到以下异常:

  • ImportError: DLL load failed: %1 不是有效的 Win32 应用程序
    • 这是由于你安装了 64 位的 Python,但却意外安装了 32 位的模块
  • Failed building wheel for cryptography
    • 你需要升级你的 pip 并重新安装 cryptography 模块
  • ERROR: \'xslt-config\' is not recognized as an internal or external command,
    operable program or batch file.
    • 你需要从 lxml 的官网下载该模块编译好的 exe 安装包,并用 easy_install 手动进行安装
  • ImportError: Nomodule named win32api
    • 这是个 Twisted bug ,你需要安装 pywin32 。

如果你还没有放弃,以下内容可能会帮到你:


基本使用

初始化项目

  • 命令行下初始化 Scrapy 项目
scrapy startproject spider_ebay

 

  • 执行后将会生成以下目录结构
└── spider_ebay
  ├── spider_ebay
  │   ├── __init__.py
  │   ├── items.py
  │   ├── middlewares.py
  │   ├── pipelines.py
  │   ├── settings.py
  │   └── spiders
  │       └── __init__.py
  └── scrapy.cfg

 

创建爬虫

  • 创建文件 
    spider_ebay/spider_ebay/spiders/example.py

     

  • 代码如下:
from scrapy.spiders import Spider

class ExampleSpider(Spider):
    name = \'example\'
    start_urls = [\'http://www.ebay.com/sch/allcategories/all-categories\']

    def parse(self, response):
        datas = response.xpath("//div[@class=\'gcma\']/ul/li/a[@class=\'ch\']")
        for data in datas:
            try:
                yield {
                    \'name\': data.xpath("text()").extract_first(),
                    \'link\': data.xpath("@href").extract_first()
                }
                # or
                # yield self.make_requests_from_url(data.xpath("@href").extract_first())
            except:
                pass

 

  • 该例爬取了 eBay 商品分类页面下的子分类页的 url 信息

  • ExampleSpider 继承自 Spider,定义了 name 、start_urls 属性与 parse 方法。
    程序通过 name 来调用爬虫,爬虫运行时会先从 strart_urls 中提取 url 构造 request,获取到对应的 response 时,
    利用 parse 方法解析 response,最后将目标数据或新的 request 通过 yield 语句以生成器的形式返回。

运行爬虫

cd spider_ebay
scrapy crawl example -o items.json

 

爬取结果

  

spider_ebay/items.json

 

[
{"name": "Antiquities", "link": "http://www.ebay.com/sch/Antiquities/37903/i.html"},
{"name": "Architectural & Garden", "link": "http://www.ebay.com/sch/Architectural-Garden/4707/i.html"},
{"name": "Asian Antiques", "link": "http://www.ebay.com/sch/Asian-Antiques/20082/i.html"},
{"name": "Decorative Arts", "link": "http://www.ebay.com/sch/Decorative-Arts/20086/i.html"},
{"name": "Ethnographic", "link": "http://www.ebay.com/sch/Ethnographic/2207/i.html"},
{"name": "Home & Hearth", "link": "http://www.ebay.com/sch/Home-Hearth/163008/i.html"},
{"name": "Incunabula", "link": "http://www.ebay.com/sch/Incunabula/22422/i.html"},
{"name": "Linens & Textiles (Pre-1930)", "link": "http://www.ebay.com/sch/Linens-Textiles-Pre-1930/181677/i.html"},
{"name": "Manuscripts", "link": "http://www.ebay.com/sch/Manuscripts/23048/i.html"},
{"name": "Maps, Atlases & Globes", "link": "http://www.ebay.com/sch/Maps-Atlases-Globes/37958/i.html"},
{"name": "Maritime", "link": "http://www.ebay.com/sch/Maritime/37965/i.html"},
{"name": "Mercantile, Trades & Factories", "link": "http://www.ebay.com/sch/Mercantile-Trades-Factories/163091/i.html"},
{"name": "Musical Instruments (Pre-1930)", "link": "http://www.ebay.com/sch/Musical-Instruments-Pre-1930/181726/i.html"},
{"name": "Other Antiques", "link": "http://www.ebay.com/sch/Other-Antiques/12/i.html"},
{"name": "Periods & Styles", "link": "http://www.ebay.com/sch/Periods-Styles/100927/i.html"},
{"name": "Primitives", "link": "http://www.ebay.com/sch/Primitives/1217/i.html"},
{"name": "Reproduction Antiques", "link": "http://www.ebay.com/sch/Reproduction-Antiques/22608/i.html"},
{"name": "Restoration & Care", "link": "http://www.ebay.com/sch/Restoration-Care/163101/i.html"},
{"name": "Rugs & Carpets", "link": "http://www.ebay.com/sch/Rugs-Carpets/37978/i.html"},
{"name": "Science & Medicine (Pre-1930)", "link": "http://www.ebay.com/sch/Science-Medicine-Pre-1930/20094/i.html"},
{"name": "Sewing (Pre-1930)", "link": "http://www.ebay.com/sch/Sewing-Pre-1930/156323/i.html"},
{"name": "Silver", "link": "http://www.ebay.com/sch/Silver/20096/i.html"},
{"name": "Art from Dealers & Resellers", "link": "http://www.ebay.com/sch/Art-from-Dealers-Resellers/158658/i.html"},
{"name": "Direct from the Artist", "link": "http://www.ebay.com/sch/Direct-from-the-Artist/60435/i.html"},
{"name": "Baby Gear", "link": "http://www.ebay.com/sch/Baby-Gear/100223/i.html"},
{"name": "Baby Safety & Health", "link": "http://www.ebay.com/sch/Baby-Safety-Health/20433/i.html"},
{"name": "Bathing & Grooming", "link": "http://www.ebay.com/sch/Bathing-Grooming/20394/i.html"},
{"name": "Car Safety Seats", "link": "http://www.ebay.com/sch/Car-Safety-Seats/66692/i.html"},
{"name": "Carriers, Slings & Backpacks", "link": "http://www.ebay.com/sch/Carriers-Slings-Backpacks/100982/i.html"},
{"name": "Diapering", "link": "http://www.ebay.com/sch/Diapering/45455/i.html"},
{"name": "Feeding", "link": "http://www.ebay.com/sch/Feeding/20400/i.html"},
{"name": "Keepsakes & Baby Announcements", "link": "http://www.ebay.com/sch/Keepsakes-Baby-Announcements/117388/i.html"},
......

 


进阶使用

分布式爬虫

架构

 
i (1).png
  1. MasterSpider 对 start_urls 中的 urls 构造 request,获取 response
  2. MasterSpider 将 response 解析,获取目标页面的 url, 利用 redisurl 去重并生成待爬 request 队列
  3. SlaveSpider 读取 redis 中的待爬队列,构造 request
  4. SlaveSpider 发起请求,获取目标页面的 response
  5. Slavespider 解析 response,获取目标数据,写入生产数据库
关于 Redis

Redis 是目前公认的速度最快的基于内存的键值对数据库

Redis 作为临时数据的缓存区,可以充分利用内存的高速读写能力大大提高爬虫爬取效率。

关于 scrapy-redis

scrapy-redis 是为了更方便地实现 Scrapy 分布式爬取,而提供的一些以 Redis 为基础的组件。

scrapy 使用 python 自带的 collection.deque 来存放待爬取的 request。scrapy-redis 提供了一个解决方案,把 deque 换成 redis 数据库,能让多个 spider 读取同一个 redis 数据库里,解决了分布式的主要问题。

配置

使用 scrapy-redis 组件前需要对 Scrapy 配置做一些调整

spider_ebay/settings.py

 

# 过滤器
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"

# 调度器
SCHEDULER = "scrapy_redis.scheduler.Scheduler"

# 调度状态持久化
SCHEDULER_PERSIST = True

# 请求调度使用优先队列
SCHEDULER_QUEUE_CLASS = \'scrapy_redis.queue.SpiderPriorityQueue\'

# redis 使用的端口和地址
REDIS_HOST = \'127.0.0.1\'
REDIS_PORT = 6379

 

增加并发

并发是指同时处理数量。其有全局限制和局部(每个网站)的限制。

Scrapy 默认的全局并发限制对同时爬取大量网站的情况并不适用。 增加多少取决于爬虫能占用多少 CPU。 一般开始可以设置为 100 。
不过最好的方式是做一些测试,获得 Scrapy 进程占取 CPU 与并发数的关系。 为了优化性能,应该选择一个能使CPU占用率在80%-90%的并发数。

增加全局并发数的一些配置:

# 默认 Item 并发数:100
CONCURRENT_ITEMS = 100

# 默认 Request 并发数:16
CONCURRENT_REQUESTS = 16

# 默认每个域名的并发数:8
CONCURRENT_REQUESTS_PER_DOMAIN = 8

# 每个IP的最大并发数:0表示忽略
CONCURRENT_REQUESTS_PER_IP = 0

 

缓存

scrapy默认已经自带了缓存,配置如下

# 打开缓存
HTTPCACHE_ENABLED = True

# 设置缓存过期时间(单位:秒)
#HTTPCACHE_EXPIRATION_SECS = 0

# 缓存路径(默认为:.scrapy/httpcache)
HTTPCACHE_DIR = \'httpcache\'

# 忽略的状态码
HTTPCACHE_IGNORE_HTTP_CODES = []

# 缓存模式(文件缓存)
HTTPCACHE_STORAGE = \'scrapy.extensions.httpcache.FilesystemCacheStorage\'

 

Redis 远程连接

安装完成后,redis默认是不能被远程连接的,此时要修改配置文件/etc/redis.conf

# bind 127.0.0.1

 

修改后,重启redis服务器

systemctl restart redis

 

如果要增加redis的访问密码,修改配置文件/etc/redis.conf

requirepass passwrd

 

增加了密码后,启动客户端的命令变为:

redis-cli -a passwrd

 

测试是否能远程登陆

使用 windows 的命令窗口进入 redis 安装目录,用命令进行远程连接 redis:

redis-cli -h 192.168.1.112 -p 6379

 

 

在本机上测试是否能读取 master 的 redis


 

在远程机器上读取是否有该数据


 

可以确信 redis 配置完成

MasterSpider

# coding: utf-8
from scrapy import Item, Field
from scrapy.spiders import Rule
from scrapy_redis.spiders import RedisCrawlSpider
from scrapy.linkextractors import LinkExtractor
from redis import Redis
from time import time
from urllib.parse import urlparse, parse_qs, urlencode


class MasterSpider(RedisCrawlSpider):
    name = \'ebay_master\'
    redis_key = \'ebay:start_urls\'

    ebay_main_lx = LinkExtractor(allow=(r\'http://www.ebay.com/sch/allcategories/all-categories\', ))
    ebay_category2_lx = LinkExtractor(allow=(r\'http://www.ebay.com/sch/[^\\s]*/\\d+/i.html\',
                                             r\'http://www.ebay.com/sch/[^\\s]*/\\d+/i.html?_ipg=\\d+&_pgn=\\d+\',
                                             r\'http://www.ebay.com/sch/[^\\s]*/\\d+/i.html?_pgn=\\d+&_ipg=\\d+\',))

    rules = (
        Rule(ebay_category2_lx, callback=\'parse_category2\', follow=False),
        Rule(ebay_main_lx, callback=\'parse_main\', follow=False),
    )

    def __init__(self, *args, **kwargs):
        domain = kwargs.pop(\'domain\', \'\')
        # self.allowed_domains = filter(None, domain.split(\',\'))
        super(MasterSpider, self).__init__(*args, **kwargs)

    def parse_main(self, response):
        pass
        data = response.xpath("//div[@class=\'gcma\']/ul/li/a[@class=\'ch\']")
        for d in data:
            try:
                item = LinkItem()
                item[\'name\'] = d.xpath("text()").extract_first()
                item[\'link\'] = d.xpath("@href").extract_first()
                yield self.make_requests_from_url(item[\'link\'] + r"?_fsrp=1&_pppn=r1&scp=ce2")
            except:
                pass

    def parse_category2(self, response):
        data = response.xpath("//ul[@id=\'ListViewInner\']/li/h3[@class=\'lvtitle\']/a[@class=\'vip\']")
        redis = Redis()
        for d in data:
            # item = LinkItem()
            try:
                self._filter_url(redis, d.xpath("@href").extract_first())

            except:
                pass
        try:
            next_page = response.xpath("//a[@class=\'gspr next\']/@href").extract_first()
        except:
            pass
        else:
            # yield self.make_requests_from_url(next_page)
            new_url = self._build_url(response.url)
            redis.lpush("test:new_url", new_url)
            # yield self.make_requests_from_url(new_url)
            # yield Request(url, headers=self.headers, callback=self.parse2)

    def _filter_url(self, redis, url, key="ebay_slave:start_urls"):
        is_new_url = bool(redis.pfadd(key + "_filter", url))
        if is_new_url:
            redis.lpush(key, url)


    def _build_url(self, url):
        parse = urlparse(url)
        query = parse_qs(parse.query)
        base = parse.scheme + \'://\' + parse.netloc + parse.path

        if \'_ipg\' not in query.keys() or \'_pgn\' not in query.keys() or \'_skc\' in query.keys():
            new_url = base + "?" + urlencode({"_ipg": "200", "_pgn": "1"})
        else:
            new_url = base + "?" + urlencode({"_ipg": query[\'_ipg\'][0], "_pgn": int(query[\'_pgn\'][0]) + 1})
        return new_url


class LinkItem(Item):
    name = Field()
    link = Field()

 

MasterSpider 继承来自 scrapy-redis 组件下的 RedisCrawlSpider,相比 ExampleSpider 有了以下变化:

  • redis_key
    • 该爬虫的 start_urls 的存放容器由原先的 Python list 改至 redis list,所以此处需要 redis_key 存放 redis list key
  • rules
    • rules 是含有多个 Rule 对象的 tuple
    • Rule 对象实例化常用的三个参数:link_extractor / callback / follow
      • link_extractor 是一个LinkExtractor 对象。 其定义了如何从爬取到的页面提取链接
      • callback 是一个 callablestring (该spider中同名的函数将会被调用)。 从 link_extractor中每获取到链接时将会调用该函数。该回调函数接受一个response作为其第一个参数, 并返回一个包含 Item 以及(或) Request 对象(或者这两者的子类)的列表(list)。
      • follow 是一个布尔(boolean)值,指定了根据该规则从response提取的链接是否需要跟进。 如果 callback None, follow 默认设置为 True ,否则默认为 False 。
      • process_links 处理所有的链接的回调,用于处理从response提取的links,通常用于过滤(参数为link列表)
      • process_request 链接请求预处理(添加headercookie等)
  • ebay_main_lx / ebay_category2_lx
    • LinkExtractor 对象
      • allow (a regular expression (or list of)) – 必须要匹配这个正则表达式(或正则表达式列表)的URL才会被提取。如果没有给出(或为空), 它会匹配所有的链接。
      • deny 排除正则表达式匹配的链接(优先级高于allow
      • allow_domains 允许的域名(可以是strlist
      • deny_domains 排除的域名(可以是strlist
      • restrict_xpaths 取满足XPath选择条件的链接(可以是strlist
      • restrict_css 提取满足css选择条件的链接(可以是strlist
      • tags 提取指定标签下的链接,默认从aarea中提取(可以是strlist
      • attrs 提取满足拥有属性的链接,默认为href(类型为list
      • unique 链接是否去重(类型为boolean
      • process_value 值处理函数(优先级大于allow
  • parse_main / parse_category2
    • 用于解析符合对应 ruleurlresponse 的方法
  • _filter_url / _build_url
    • 一些有关 url 的工具方法
  • LinkItem