基于 Scrapy-redis 的分布式爬虫详细设计
Posted 时间@煮雨~
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于 Scrapy-redis 的分布式爬虫详细设计相关的知识,希望对你有一定的参考价值。
基于 Scrapy-redis 的分布式爬虫设计
目录
前言
在本篇中,我假定您已经熟悉并安装了 Python3。 如若不然,请参考 Python 入门指南。
关于 Scrapy
Scrapy 是一个为了爬取网站数据,提取结构性数据而编写的应用框架。 可以应用在包括数据挖掘,信息处理或存储历史数据等一系列的程序中。
其最初是为了 网络抓取 所设计的, 也可以应用在获取 API 所返回的数据(例如 Amazon Associates Web Services ) 或者通用的网络爬虫。
架构概览
安装
环境
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 。
如果你还没有放弃,以下内容可能会帮到你:
- Windows上Python3.5安装Scrapy(lxml)
- Python爬虫进阶三之Scrapy框架安装配置
- Microsoft Visual C++ Compiler for Python 2.7
- easy_install lxml on Python 2.7 on Windows
基本使用
初始化项目
- 命令行下初始化 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"}, ......
进阶使用
分布式爬虫
架构
MasterSpider
对start_urls
中的 urls 构造request
,获取response
MasterSpider
将response
解析,获取目标页面的 url, 利用 redis 对 url 去重并生成待爬request
队列SlaveSpider
读取 redis 中的待爬队列,构造request
SlaveSpider
发起请求,获取目标页面的response
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
对象的 tupleRule
对象实例化常用的三个参数:link_extractor
/callback
/follow
link_extractor
是一个LinkExtractor
对象。 其定义了如何从爬取到的页面提取链接callback
是一个 callable 或 string (该spider中同名的函数将会被调用)。 从 link_extractor中每获取到链接时将会调用该函数。该回调函数接受一个response作为其第一个参数, 并返回一个包含 Item 以及(或) Request 对象(或者这两者的子类)的列表(list)。follow
是一个布尔(boolean)值,指定了根据该规则从response提取的链接是否需要跟进。 如果 callback 为None, follow 默认设置为 True ,否则默认为 False 。process_links
处理所有的链接的回调,用于处理从response提取的links,通常用于过滤(参数为link列表)process_request
链接请求预处理(添加header或cookie等)
ebay_main_lx
/ebay_category2_lx
LinkExtractor
对象allow
(a regular expression (or list of)) – 必须要匹配这个正则表达式(或正则表达式列表)的URL才会被提取。如果没有给出(或为空), 它会匹配所有的链接。deny
排除正则表达式匹配的链接(优先级高于allow)allow_domains
允许的域名(可以是str或list)deny_domains
排除的域名(可以是str或list)restrict_xpaths
: 取满足XPath选择条件的链接(可以是str或list)restrict_css
提取满足css选择条件的链接(可以是str或list)tags
提取指定标签下的链接,默认从a和area中提取(可以是str或list)attrs
提取满足拥有属性的链接,默认为href(类型为list)unique
链接是否去重(类型为boolean)process_value
值处理函数(优先级大于allow)
parse_main
/parse_category2
- 用于解析符合对应 rule 的 url 的 response 的方法
_filter_url
/_build_url
- 一些有关 url 的工具方法
LinkItem
- 继承自 Item 对象 以上是关于基于 Scrapy-redis 的分布式爬虫详细设计的主要内容,如果未能解决你的问题,请参考以下文章
基于Python使用scrapy-redis框架实现分布式爬虫 注
爬虫学习 17.基于scrapy-redis两种形式的分布式爬虫