Github分布式爬虫——实现

使用Redis实现分布式爬虫

主要使用Scrapy、Redis、MongoDB实现,Scrapy作为异步爬虫框架、Redis实现分布式以及Cookies池的存储,MongoDB实现底层数据存储

分布式示意图

主机中安装Redis和MongoDB

Redis中存储:

  • 所有Scrapy爬虫的待爬取队列
  • 去重用的已发出Request指纹

MongoDB负责最终数据的存储

可创建多个Scrapy从机进行爬取,实现分布式。

Features

  • 共享爬取队列实现分布式
  • 生成Request的指纹实现分布式的去重
  • 随机指定User-Agent
  • 通过Redis实现Cookies池并进行更新

分布式实现

请求队列

各个分布式爬虫共同维护一个Request请求队列,使用的是Redis的list。队列可以实现FIFO,LIFO或者优先级队列。

爬取的时候一开始使用的是优先级队列,但是后来因为不好设置优先级,导致最后都是User的item,导致Rep的饥饿现象,所以到后面换成FIFO队列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class FifoQueue(Base):
"""Per-spider FIFO queue"""

def __len__(self):
"""Return the length of the queue"""
return self.server.llen(self.key)

def push(self, request):
"""Push a request"""
self.server.lpush(self.key, self._encode_request(request))

def pop(self, timeout=0):
"""Pop a request"""
if timeout > 0:
data = self.server.brpop(self.key, timeout)
if isinstance(data, tuple):
data = data[1]
else:
data = self.server.rpop(self.key)
if data:
return self._decode_request(data)

同时需要设置request队列的大小限制,因为爬取Github页面的时候可能爬取一个star列表以后就会产生几十个Request,这样很容易把Redis的队列挤爆:

1
2
3
4
5
6
7
8
9
10
11
def enqueue_request(self, request):
# set upper limit of request num
if len(self.queue) > REQUEST_NUM:
return False
if not request.dont_filter and self.df.request_seen(request):
self.df.log(request, self.spider)
return False
if self.stats:
self.stats.inc_value('scheduler/enqueued/redis', spider=self.spider)
self.queue.push(request)
return True

没有设置数量大小的时候,1G内存的主机跑一个多小时之后就挂掉了:

调度器

为了使用共同的请求队列,所以需要重写Scheduler,重写的Scheduler负责把要爬取的request放入队列以及从队列中找出request进行爬取

去重集合

本地爬虫通常需要解决一个去重问题,通常使用的是数据库查询,爬取之前判断url是否请求过。分布式爬虫就是在这个的基础上共用一个去重集合,使用的是Redis的set。

判断一个Request是否已经爬取过,这里不是使用url,因为不同的url可能代表的是同一种资源,比如:http://www.example.com/query?cat=222&id=111http://www.example.com/query?cat=222&id=111 事实表示的是一个东西。此外,发出的Request还可能与当时的Cookie有关,因为里面还会有用户信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
# 计算Request指纹判断有没有重复
def request_seen(self, request):
fp = self.request_fingerprint(request)
if USEBLOOMFILTER == True:
if self.bf.isContains(fp):
return True
else:
self.bf.insert(fp)
return False
else:
# This returns the number of values added, zero if already exists.
added = self.server.sadd(self.key, fp)
return added == 0

这里采用的方案是生成一个Request的指纹,实际上是把request的url,method和指定的header使用sha1算法得到一个hash值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Request 指纹计算方法
def request_fingerprint(request, include_headers=None):
if include_headers:
include_headers = tuple(to_bytes(h.lower())
for h in sorted(include_headers))
cache = _fingerprint_cache.setdefault(request, {})
if include_headers not in cache:
fp = hashlib.sha1()
fp.update(to_bytes(request.method))
fp.update(to_bytes(canonicalize_url(request.url)))
fp.update(request.body or b'')
if include_headers:
for hdr in include_headers:
if hdr in request.headers:
fp.update(hdr)
for v in request.headers.getlist(hdr):
fp.update(v)
cache[include_headers] = fp.hexdigest()
return cache[include_headers]

反反爬虫策略

随机User-Agent

使用中间件实现随机User-Agent,随机替换掉request的header的User-Agent参数

1
2
3
4
5
6

class GitHubUserAgentMiddleware(UserAgentMiddleware):

def process_request(self, request, spider):
agent = random.choice(agents)
request.headers['User-Agent'] = agent

Cookies池

首先使用多账号模拟登录,获取到很多已登录账号的cookies,放入到Redis的hashset中,各个爬虫共用这一个Cookies池。爬虫同样实现一个中间件,替换Request的Cookies。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Cookie 中间件
class GitHubCookieMiddleware(RetryMiddleware):

def __init__(self,settings):
RetryMiddleware.__init__(self,settings)
self.rconn = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, db=3)

@classmethod
def from_crawler(cls, crawler):
return cls(crawler.settings)

def process_request(self, request, spider):
keys = self.rconn.hkeys(REDIS_COOKIE)
if(len(keys) == 0):
print("cookies don't work!")
return
key = random.choice(keys)
# 获取最新cookie
value = self.rconn.hget(REDIS_COOKIE, key)
if( isinstance(value, bytes) ):
value = value.decode('utf-8')
cookies = json.loads(value)
request.cookies = cookies

总结

这是一个为了了解分布式工作原理而做的一个小的项目,其中很多地方存在缺陷,欢迎大家在Github上留建议:

https://github.com/Sixzeroo/GithubCrawler

同时也可以查看下一篇关于Github用户和仓库数据分析的文章