基于Scrapy分布式爬虫的开发与设计(内附python学习资料分享)
这个项目也是初窥python爬虫的一个项目,也是我的毕业设计,当时选题的时候,发现大多数人选择的都是网站类,实在是普通不过了,都是一些简单的增删查改,业务类的给人感觉一种很普通的系统设计,当时也刚好在知乎上看到了一个回答,你是如何利用计算机技术解决生活的实际问题,链接就不放了,有兴趣的可以搜索下,然后就使用了这个课题。
摘要:基于 python 分布式房源数据抓取系统为数据的进一步应用即房源推荐系统做数据支持。本课题致力于解决单进程单机爬虫的瓶颈,打造一个基于 Redis 分布式多爬虫共享队列的主题爬虫。本系统采用 python 开发的 Scrapy 框架来开发,使用 Xpath 技术对下载的网页进行提取解析,运用 Redis 数据库做分布式,使用MongoDb 数据库做数据存储,利用 Django web 框架和 Semantic UI开源框架对数据进行友好可视化,最后使用了Docker对爬虫程序进行部署。设计并实现了针对 58 同城各大城市租房平台的分布式爬虫系统。
一、系统功能架构
系统功能架构图
分布式爬虫抓取系统主要包含以下功能:
1.爬虫功能:
爬取策略的设计
内容数据字段的设计
增量爬取
请求去重
2.中间件:
爬虫防屏蔽中间件
网页非200状态处理
爬虫下载异常处理
3.数据存储:
抓取字段设计
数据存储
4.数据可视化
二、系统分布式架构
分布式采用主从结构设置一个Master服务器和多个Slave服务器,Master端管理Redis数据库和分发下载任务,Slave部署Scrapy爬虫提取网页和解析提取数据,最后将解析的数据存储在同一个MongoDb数据库中。分布式爬虫架构如图所示。
分布式爬虫架构图
应用Redis数据库实现分布式抓取,基本思想是Scrapy爬虫获取的到的detail_request的urls都放到Redis Queue中,所有爬虫也都从指定的Redis Queue中获取requests,Scrapy-Redis组件中默认使用SpiderPriorityQueue来确定url的先后次序,这是由sorted set实现的一种非FIFO、LIFO方式。因此,待爬队列的共享是爬虫可以部署在其他服务器上完成同一个爬取任务的一个关键点。此外,在本文中,为了解决Scrapy单机局限的问题,Scrapy将结合Scrapy-Redis组件进行开发,Scrapy-Redis总体思路就是这个工程通过重写Scrapu框架中的scheduler和spider类,实现了调度、spider启动和redis的交互。实现新的dupefilter和queue类,达到了判重和调度容器和redis的交互,因为每个主机上的爬虫进程都访问同一个redis数据库,所以调度和判重都统一进行统一管理,达到了分布式爬虫的目的。
三、系统实现
1)爬取策略的设计
由scrapy的结构分析可知,网络爬虫从初始地址开始,根据spider中定义的目标地址获的正则表达式或者Xpath获得更多的网页链接,并加入到待下载队列当中,进行去重和排序之后,等待调度器的调度。
在这个系统中,新的链接可以分为两类,一类是目录页链接,也就是我们通常看到的下一页的链接,一类是内容详情页链接,也就是我们需要解析网页提取字段的链接,指向的就是实际的房源信息页面。网络需从每一个目录页链接当中,提取到多个内容页链接,加入到待下载队列准备进一步爬取。爬取流程如下:
此处是Master端的目标链接的爬取策略,因为采取的分布式主从模式,Master端爬虫主要爬取下载到内容详情页链接,通过redis分享下载任务给其他slave端的爬虫。Slave端主要是负责对详情页链接的进一步解析提取存储到数据库中。
本论文以58同城租房为例,其初始页链接,其实也就是每个分类的第一页链接,主要有(以广东省几个城市为例):
① 东莞租房:(http://dg.58.com/chuzu/),
② 深圳租房:(http://sz.58.com/chuzu/),
③ 汕尾租房:(http://sw.58.com/chuzu/),
④ 广州租房:(http://gz.58.com/chuzu/),
其目录页链接如下所述:
⑤ 第二页(http://dg.58.com/chuzu/pn2)
⑥ 第三页(http://dg.58.com/chuzu/pn3)
⑦ 第四页(http://dg.58.com/chuzu/pn4)
其内容详情页如下:
⑧ (http://taishan.58.com/zufang/29537279701166x.shtml)
综上所述,网络房源爬取系统使用以下爬取策略:
1) 对于Master端:
最核心模块是解决翻页问题和获取每一页内容详情页链接。Master端主要采取以下爬取策略:
1. 向redis往key为nest_link插入初始链接,从初始页链接开始
2. 爬虫从redis中key为next_link中取到初始链接,开始运行爬虫
3. 将下载器返回的Response,爬虫根据spider定义的爬取规则识别是否有下一页链接,若有链接,存储进redis中,保存key为next_link,同时根据匹配规则是否匹配到多个内容详情页链接,若匹配到,则存储进Redis,保存key为detail_request插入下载链接,给slave端的spider使用,即是Slave端的下载任务。
4. 爬虫继续从redis中key为next_link取值,若有值,继续步骤2,若为空,爬虫则等待新的链接。
2) 对于Slave端:
最核心模块是从redis获得下载任务,解析提取字段。Slave端主要采取以下爬取策略:
1.爬虫从redis中key为detail_request中取到初始链接,开始运行爬虫
2.将下载器返回的Response,爬虫根据spider定义的爬取规则识别是否有匹配规则的内容字段,若有将字段存储,返回到模型中,等待数据存储操作。
重复步骤1,直到带爬取队列为空,爬虫则等待新的链接。
2)爬虫的具体实现
爬虫程序的包含四个部分,分别是对象定义程序,数据抓取程序,数据处理程序和下载设置程序,此处的组成是Slave端,Master少了对象定义程序以及数据处理程序,Master端主要是下载链接的爬取。
(1)数据抓取程序
数据抓取程序分Master端和Slave端,数据抓取程序从Redis中获得初始地址,数据抓取程序中定义了抓取网页的规则和使用Xpath提取字段数据的方法等,这里着重介绍Xpath提取字符数据的方法,Xapth使用路径表达式来选取网页文档中的节点或者节点集。在Xpath中有其中类型的几点:元素、属性、文本、命名空间、处理指令、注释和文档节点。网页文档是被当做节点树来对待,树的跟被称为文档节点和根节点,通过Xpath表达式定位目标节点即可抽取网页文档的字段数据,下面以Master抓取内容页链接和Slave提取字段数据为例。
a. Master端的例子
Xpath抽取下一页链接的方法:
response_selector.xpath(u'//div[contains(@class,"pager")]/a[contains(@class,"next")]/@href').extract()
Xpath抽取内容详情页链接的方法:
response_selector.xpath(u'//div[contains(@class,"listBox")]/ul[contains(@class,"listUl")]/li/@logr').extract(),
因为网站对内容详情页做了反爬措施,详情页点击之后,把id获取到再跳转到某个域名,因此,自己构造详情页id,实现如下:
response_url[0]+'/zufang/' + detail_link.split('_')[3] + 'x.shtml'
a. slave端的例子:
Xpath抽取内容页的方法:
帖子名称:
response_selector.xpath(u'//div[contains(@class,"house-title")]/h1[contains(@class,"c_333 f20")]/text()').extract()
帖子发布时间:
response_selector.xpath(u'//div[contains(@class,"house-title")]/p[contains(@class,"house-update-info c_888 f12")]/text()').extract()
因为有些数据不是用Xpath就可以提取出来,还需要正则进行匹配,如果出现异常也要进行处理,一般页面匹配不到相应的字段的时候,都应该设置为0,到item后进行处理,对itme进行过滤处理。
3)去重与增量爬取
去重与增量爬取,对于服务器有很重大的意义,能够减少服务器的压力以及保证数据的准确性。如果不采取去重处理,那么抓取的内容会抓取大量重复内容,让爬虫效率极大的下降。其实去重流程很简单,核心就是每次请求的时候,先判断这个请求是否在已经爬取的队列当中。如果已存在,则舍弃当前请求。
具体实现步骤:
(1) 从待爬队列中获取url
(2) 将即将请求的url判断是否已经爬取,若已爬取,则将请求忽略,未爬取,继续其他操作并将url插入已爬取队列中
(3) 重复步骤1
这里我们使用scrapy-redis的去重组件,所以也没有实现,不过原理还是要看懂的,具体可以看源码。
4)爬虫中间件
爬虫中间件能够帮助我们在scrapy抓取流程中自由的扩展自己的程序,以下有爬虫防屏蔽中间件,下载器异常状态中间件以及非200状态中间件。
(1)爬虫防屏蔽组件的实现
访问一个网站的网页的时候,会给网站带了一定的负载,而爬虫程序则是模拟了我们正常访问网页的过程,但是。大规模的爬虫会给网站增加大量的负载,影响正常用户的访问。为保证网页能够别大多数正常用户的访问,大多数网站都有相应的防爬虫策略。一旦访问行为被认定为爬虫,网站将会采取一定的措施,限制你的访问,比如提示你,访问过于频繁让你输入验证码,更严重者,会封掉你的ip,禁止你访问该网站。本系统定向抓取网页数据的时候,将不间断的访问网站内容,如果不采取伪装措施,很容易被网站识别为爬虫行为而屏蔽掉。
本系统采用以下方法来防止爬虫被屏蔽:
1. 模拟不同的浏览器行为
2. 以一定的频率更换代理服务器和网关
3. 本着君子协议,降低爬虫爬取网页的频率,减少并发爬取的进程,限制每个ip并发爬取的次数,牺牲一定的效率来换取系统的稳定性。
4. 禁用cookie,网站会在用户访问时在cookie中插入一些信息来判断是否是机器人,我们屏蔽调cookie,也有利于我们的身份不同意暴露。
5. 人工打码,这应该是无懈可击的防被禁措施,所有系统也比不过人工的操作,但是减少了自动化,效率也不高,但确实最有效的措施。爬虫被禁的时候,会重定向到一个验证码页面去,输入验证码即可重新有权限访问页面,为此,我加了邮件提醒模块,当爬虫被禁,发邮件提醒管理员解封,同时将重定向的请求重新加入到待爬取的下载队列当中,保证数据的完整度。
爬虫防网站屏蔽原理如下图所示:
(a)模拟不同浏览器行为实现思路及代码
原理:从scrapy的介绍我们可以知道,scrapy有下载中间件,在这个中间件我们可以对请求跟响应进行自定义处理,类似于spring面向切面编程,像一个钩子嵌入到程序的运行前后。核心就是对请求的属性进行修改
首先主要是对下载中间件进行了扩展,首先在seetings.py上面增加中间件,
其次,扩展中间件,主要是写一个useragent列表,将常用的浏览器请求头保存为一个列表,如下所示:
再让请求的头文件随机在列表中取一个agent值,然后到下载器进行下载。
综上,每次发出请求的时候模拟使用不同的浏览器对目标网站进行访问。
(b)使用代理ip进行爬取的实现思路及代码。
首先在seetings.py上面增加中间件,扩展下载组件请求的头文件随机从代理ip池中取出一个代理值然后到下载器进行下载。
1. 代理ip池的设计与开发流程如下:
a. 对免费代理ip网站进行抓取。
b. 对代理ip进行存储并验证
c. 验证通过存储进数据库
d. 如果满足ip最大数量,则停止爬去,一定时间后验证数据的ip有效性,将失效的ip删除
e. 直到数据库ip小于0,继续爬取ip,重复步骤a。
代理ip模块这里使用了七夜代理ip池的开源项目
代理ip爬虫运行截图:
(c)爬虫异常状态组件的处理
爬虫没有被屏蔽运行时,访问网站不是一直都是200请求成功,而是有各种各样的状态,像上述爬虫被禁的时候,其实返回的状态是302,防止屏蔽组件就是捕捉到302状态加以实现的。同时异常状态的处理有利于爬虫的健壮性。
在settings中扩展中间件捕捉到异常的情况之后,将请求Request重新加入到待下载队列当中流程如下:
(d)数据存储模块
数据存储模块主要负责将slave端爬取解析的页面进行存储。使用Mongodb对数据进行存储。
Scrapy支持数据存储的格式有json,csv和xml等文本格式,用户可以在运行爬虫时设置,例如:scrapy crawl spider -o items.json -t json,也可以在Scrapy工程文件额ItemPipline文件中定义,同时,Scrapy也支持数据库存储,如Monogdb,Redis等,当数据量大到一定程度时,可以做Mongodb或者Reids的集群来解决问题,本系统数据存储如下图所示:
(e)抓取字段设计
本文以网络房源数据为抓取目标,由slave端解析抓取字段数据,因此抓取的内容必须能够客观准确地反映网络房源数据特征。
以抓取58同城网络房源数据为例,通过分析网页结构,定义字段详情如下表所示。
序号字段名称字段含义
1title帖子标题
2money租金
3method租赁方式
4area所在区域
5community所在小区
6targeturl帖子详情
7city所在城市
8Pub_time帖子发布时间
字段选取主要是依据本系统的应用研究来进行的,因为系统开发单机配置比较低,没有下载图片文件到本机。减少单机承受的压力。
(f)数据处理
1) 对象定义程序
Item是定义抓取数据的容器。通过创建一个scrapy.item.Item类来声明。定义属性为scrapy.item.Field对象,通过将需要的item实例化,来控制获得的站点数据。本系统定义了九个抓取对象,分别是:帖子标题,租金,租赁方式,所在区域,所在小区,所在城市,帖子详情页链接,发布时间。此处字段的定义基于数据处理端的需要来定义的。关键代码如下:
class TcZufangItem(Item):
#帖子名称
title=Field()
#租金
money=Field()
#租赁方式
method=Field()
#所在区域
area=Field()
#所在小区
community=Field()
#帖子详情url
targeturl=Field()
#帖子发布时间
pub_time=Field()
#所在城市
city=Field()
2) 数据处理程序
Pipeline类中定义了数据的保存和输出方法,从Spider的parse方法返回的Item,数据将对应ITEM_PIPELINES列表中的Pipeline类处理后以顶一个格式输出。本系统传回管道的数据使用Mongodb来进行存储。关键代码如下:
def process_item(self, item, spider):
if item['pub_time'] == 0:
raise DropItem("Duplicate item found: %s" % item)
if item['method'] == 0:
raise DropItem("Duplicate item found: %s" % item)
if item['community']==0:
raise DropItem("Duplicate item found: %s" % item)
if item['money']==0:
raise DropItem("Duplicate item found: %s" % item)
if item['area'] == 0:
raise DropItem("Duplicate item found: %s" % item)
if item['city'] == 0:
raise DropItem("Duplicate item found: %s" % item)
zufang_detail = {
'title': item.get('title'),
'money': item.get('money'),
'method': item.get('method'),
'area': item.get('area', ''),
'community': item.get('community', ''),
'targeturl': item.get('targeturl'),
'pub_time': item.get('pub_time', ''),
'city':item.get('city','')
}
result = self.db['zufang_detail'].insert(zufang_detail)
print '[success] the '+item['targeturl']+'wrote to MongoDB database'
return item
(g)数据可视化设计
数据的可视化其实也就是,将数据库的数据转换成我们用户容易观察的形式,本系统使用Mongodb对数据进行存储。对数据进行可视化基于Django+Semantiui,效果如下图所示:
四、系统运行
系统以58同城租房平台为抓取目标,运行十小时之后,持续抓取网页数量共计几万条房源数据。
Master端运行截图:
Slave端运行截图:
五、系统部署
环境部署,因为分布式部署所需环境都是类似的,如果一个服务器部署程序都需要在配置下环境显得很麻烦,这里使用了docker镜像对爬虫程序进行部署,使用了Daocloud上的scrapy-env对程序进行了部署,具体docker部署过程可以参考网上。
最后,想学习Python的小伙伴们!
请关注+私信回复:“学习”就可以拿到一份我为大家准备的Python学习资料!
pytyhon学习资料
python学习资料