作者:谢科-搜索引擎
链接:https://www.zhihu.com/question/20899988/answer/24923424
来源:知乎
什么是爬虫?
要想入门爬虫,首先要知道,什么是爬虫。网络爬虫(又称网页蜘蛛,网络机器人,更常称为网页追逐者),是一种按照一定的规则,自动地抓取万维网信息的程序或脚本。另外一些不常使用的名字还有蚂蚁、自动索引、模拟程序或者蠕虫。
入门爬虫首先需要学习
1. 基本的爬虫工作原理
2.基本的http抓取工具,scrapy
3.Bloom Filter:Bloom Filters by Example
4.如果需要大规模网页抓取,你需要学习分布式爬虫的概念,主要学会怎样维护一个所有集群机器能够有效分享的分布式队列。最简单的实现是python-rq:https://github.com/nvie/rq
5.rq和Scrapy的结合:darkrho/scrapy-redis · GitHub
6.后续处理,网页析取(grangier/python-goose · GitHub),存储(Mongodb)
说说当初写的一个集群爬下整个豆瓣的经验:
(1)首先要明白爬虫怎样工作。
想象你是一只蜘蛛,现在你被放到了互联网上,你需要把所有的网页都看一遍。怎么办呢?就随便从某个地方开始,比如说,人民日报的首页,叫initial pages,用$表示。
在人民日报的首页,看到那个页面引向的各种链接。于是你很开心地爬到了“国内新闻”那个页面。这样就爬完了两个页面(首页和国内新闻)。暂且不用管爬下来的页面怎么样处理,你就想象你把这个页面完整抄成了个html放到了你身上。
突然你发现,在国内新闻这个页面上,有一个链接链回“首页”。作为一只聪明的蜘蛛,肯定知道不用爬回去,所以需要用脑子,存下你已经看过的页面地址,这样,每次看到一个可能需要爬的新链接,你先查查是不是已经去过这个页面地址,如果去过,就别去了。
理论上,如果所有的页面都可以从initial page到达的话,那么证明你一定可以爬完所有的网页。以下是在python中的实现。
import Queue initial_page = "http://www.renminribao.com" url_queue = Queue.Queue() seen = set() seen.insert(initial_page) url_queue.put(initial_page) while(True): #一直进行直到海枯石烂 if url_queue.size()>0: current_url = url_queue.get() #拿出队例中第一个的url store(current_url) #把这个url代表的网页存储好 for next_url in extract_urls(current_url): #提取把这个url里链向的url if next_url not in seen: seen.put(next_url) url_queue.put(next_url) else: break
所有的爬虫的backone都在这里,下面分析一下为什么爬虫事实上是个非常复杂的东西--搜索引擎公司通常有一整个团队来维护和开发。
(2)效率
如果直接使用上边的代码运行,那么一整年才能爬下整个豆瓣的内容,更别说Google爬取全网的内容了。问题出在哪里呢?需要爬的网页实在是太多了,而上边的代码太慢太慢了。设想全网有N个网站,那么分析一下判重的复杂度就是N*log(N),因为所有的网页都要遍历一遍,而每次判重用set的话,需要log(N)的复杂度。虽然python的set实现的是hash,不过这样还是太慢了,至少内存使用效率不高。
通常的判重的做法是Bloom Filter。简单讲它仍是一种hash方法,但是它的特点是,他可以使用固定的内存(不随url的数量而增长)以O(1)的效率判定url是否已经在set中。他唯一的问题在于,如果这个url不在set中,BF可以100%确定这个url没有看过。但是如果这个url在set中,他就会告诉你,这个url应该已经出现过,不过我有2%的不确定性。注意这里的不确定性在你分配的内存足够大时,可以变得很小很小。一个简单的教程:Bloom Filters by Example。
现在已经接近处理判重最快的方法了,另外一个瓶颈--你只有一台机器,不管你的带宽有多大,只要你的机器下载网页的速度是瓶颈的话,那么你只有加快这个速度。用一台机子不够的话,用很多台。当然,我们假设每台机子都已经进了最大的效率--使用多线程(python的话,使用多线程吧)。
(3)集体化抓取
假设你现在有100台机器可以用,如何用python实现一个分布式的爬取算法呢?
我们把100台中的99台运算能力较小的机器叫做slave,另外一台较大的机器叫做master,那么回顾上边的代码中的url_queue,如果我们能把这个queue放到这台master机器上,所有的slave都可以通过网络跟master联通,每当一个slave完成下载一个网页,就向master请求一个新的网页来抓取。而每次slave新抓到一个网页,就把这个网页上所有的链接送到master的queue里去。同样,bloom filter也放到master上,但是现在master只发送确定没有访问过的url给slave。bloom filter放到master的内存里,而被访问过的url放到运行在master上的Redis里,这样保证所有操作都是O(1)。(至少平摊是O(1),Redis的访问效率见:LINSERT – Redis)。
考虑如何用python实现:
在各台slave上装好scrapy,那么各台机子就变成了一台有抓取能力的slave,在master上装好Redis和rq用作分布式队列。
代码于是写成
#slave.py current_url = request_from_master() to_send = [] for next_url in extract_urls(current_url): to_send.append(next_url) store(current_url); send_to_master(to_send) #master.py distributed_queue = DistributedQueue() bf = BloomFilter() initial_pages = "www.renmingribao.com" while(True): if request == 'GET': if distributed_queue.size()>0: send(distributed_queue.get()) else: break elif request == 'POST': bf.put(request.url)
完整代码见:darkrho/scrapy-redis · GitHub
(4)展望以及后处理
上边的代码用来爬一个整体的网站问题不大,但是如果附上后续处理,比如
1. 有效地存储(数据库应该怎样安排)
2. 有效地判重(网页判重,网页和抄袭他的网页不能都爬取)
3. 有效地信息抽取(比如怎么样抽取网页上所有的地址,搜索引擎通常不需要存储所有的信息)
4. 及时更新(预测这个网页多久会更新一次)
这里的每一个点都需要很长时间的研究!