如何入门 Python 爬虫?

关注者
56,270
被浏览
10,385,731

569 个回答

“入门”是良好的动机,但是可能作用缓慢。如果你手里或者脑子里有一个项目,那么实践起来你会被目标驱动,而不会像学习模块一样慢慢学习。

另外如果说知识体系里的每一个知识点是图里的点,依赖关系是边的话,那么这个图一定不是一个有向无环图。因为学习A的经验可以帮助你学习B。因此,你不需要学习怎么样“入门”,因为这样的“入门”点根本不存在!你需要学习的是怎么样做一个比较大的东西,在这个过程中,你会很快地学会需要学会的东西的。当然,你可以争论说需要先懂python,不然怎么学会python做爬虫呢?但是事实上,你完全可以在做这个爬虫的过程中学习python :D

看到前面很多答案都讲的“术”——用什么软件怎么爬,那我就讲讲“道”和“术”吧——爬虫怎么工作以及怎么在python实现。

先长话短说summarize一下:

你需要学习

  1. 基本的爬虫工作原理
  2. 基本的http抓取工具,scrapy
  3. Bloom Filter: Bloom Filters by Example
  4. 如果需要大规模网页抓取,你需要学习分布式爬虫的概念。其实没那么玄乎,你只要学会怎样维护一个所有集群机器能够有效分享的分布式队列就好。最简单的实现是python-rq: 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

写得已经很伪代码了。

所有的爬虫的backbone都在这里,下面分析一下为什么爬虫事实上是个非常复杂的东西——搜索引擎公司通常有一整个团队来维护和开发。

2)效率
如果你直接加工一下上面的代码直接运行的话,你需要一整年才能爬下整个豆瓣的内容。更别说Google这样的搜索引擎需要爬下全网的内容了。

问题出在哪呢?需要爬的网页实在太多太多了,而上面的代码太慢太慢了。设想全网有N个网站,那么分析一下判重的复杂度就是N*log(N),因为所有网页要遍历一次,而每次判重用set的话需要log(N)的复杂度。OK,OK,我知道python的set实现是hash——不过这样还是太慢了,至少内存使用效率不高。

通常的判重做法是怎样呢?Bloom Filter. 简单讲它仍然是一种hash的方法,但是它的特点是,它可以使用固定的内存(不随url的数量而增长)以O(1)的效率判定url是否已经在set中。可惜天下没有白吃的午餐,它的唯一问题在于,如果这个url不在set中,BF可以100%确定这个url没有看过。但是如果这个url在set中,它会告诉你:这个url应该已经出现过,不过我有2%的不确定性。注意这里的不确定性在你分配的内存足够大的时候,可以变得很小很少。一个简单的教程:Bloom Filters by Example

注意到这个特点,url如果被看过,那么可能以小概率重复看一看(没关系,多看看不会累死)。但是如果没被看过,一定会被看一下(这个很重要,不然我们就要漏掉一些网页了!)。 [IMPORTANT: 此段有问题,请暂时略过]


好,现在已经接近处理判重最快的方法了。另外一个瓶颈——你只有一台机器。不管你的带宽有多大,只要你的机器下载网页的速度是瓶颈的话,那么你只有加快这个速度。用一台机子不够的话——用很多台吧!当然,我们假设每台机子都已经进了最大的效率——使用多线程(python的话,多进程吧)。

3)集群化抓取
爬取豆瓣的时候,我总共用了100多台机器昼夜不停地运行了一个月。想象如果只用一台机子你就得运行100个月了...

那么,假设你现在有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. 及时更新(预测这个网页多久会更新一次)


如你所想,这里每一个点都可以供很多研究者十数年的研究。虽然如此,
“路漫漫其修远兮,吾将上下而求索”。

所以,不要问怎么入门,直接上路就好了:)

如果学完了爬虫你对搜索引擎还感兴趣,也欢迎阅读我正在写的教程:

我会一直更新,我自己的公号 HiXieke 里也会不断更新发布,欢迎关注。其它答案和文章:

——————最重要的话写在前面——————

0、新手/喜欢练习/欢迎交流/邀请/我是看着这个问题下面的答案学习的

1、带着一个目的来学爬虫。#我的目的实现了…所以我来写这个回答了。

2、不要怂就是干!系统学习固然好,直接写一个项目出来效果更加简单粗暴!(不过自己现在的水平写出来都是流水一般的面向过程的代码,代码的重复部分太多,正在回过头去学习面向对象编程,学习类和方法的使用。不过我还是坚定地认为入门的时候应该直接简单粗暴地实践一个项目

3、哪里不会搜哪里!哪里报错改哪里!相信我你遇到的99%的问题都能从网上找到相似的问题,你需要做的就是写代码!搜问题!调BUG!你搜不到解决办法的情况下,80%的情况是你搜索的姿势不对,另外20%可能需要你自己动动脑子,换个思路去做。

举个印象最深的例子。

我在统计知乎回答voters的具体情况的时候(后面会介绍)发现知乎的数据是这样发送的。

zhihu.com/answer/152197

什么鬼(摔)。

等到我辛辛苦苦用正则把里面的信息提出来的时候发现我得到的数据是这样的…


我的内心是崩溃的……

问题很明显是编码问题……用户那一列全部是unicode编码……转成中文就好了嘛……

我刚开始也是这么想的…当我尝试了各种encode和decode…以后整个人都不好了。

大概是这样的。我用Shell演示一下…应该能看懂。



但是我的字符串是自动获取的啊,怎么可能挨着用 u' '赋值……

于是我开始了漫长的搜索之路……在看了无数篇重复度高于百分之80的关于编码的文章后,在我都快要放弃的时候…看到了这个…水木社区-源于清华的高知社群 你能理解我当时内心的酸爽吗…


大概就是这样。

所以我遇到的问题已经很奇葩了依然还是有解决办法的嘛。Windows下面编码各种混乱,系统编码,编程时编辑器的编码,抓取网页以后网页的编码,Python2的编码,Python3的编码……新人真的很容易搞昏头。

例子不多言了。后面还有很多。


——————正文1:我的爬虫入门,不谈学习,只聊项目(代码已贴)——————

前面说到学爬虫需要一个目标。那我的目标是什么呢?听我慢慢讲。

前些日子我回答了一个问题高考后暑假应该做什么事? - 生活 我回答这个问题的时候呢关注人数也就才刚刚过百,我的赞数也涨的很慢…

可是突然有一天呢,我发现突然就出现了一个300赞的回答…当时那个问题的关注似乎还不到300。我百思不得其解…但是我看了看那个回答的赞同和答主的主页。

大概是这样的:


然后我隐隐觉得…可能会是刷赞?当然我们不能恶意地去揣测别人,要拿数据说话,毕竟知乎现在的三零真实用户还是蛮多的,不一定都是水军的小号。


于是我一个从来没有学过爬虫的人就开始学爬虫了…然而并不一帆风顺。首先是知乎显示“等人赞同”的方式做了修改,参见如何评价知乎新的「某某等人赞同」显示方式? - 如何评价 X

其次我刚开始的时候不会维持登陆…每次抓到的数据里都有很多的“知乎用户”(也就是未登录状态下抓取不成功)。

为了行文的连贯我跳过中间学习时做的几个小爬虫…直接放我做成功的结果吧。


选取的样本回答依次为:

@段晓晨高考后暑假应该做什么事? - 段晓晨的回答

@EdgeRunner高考后暑假应该做什么事? - EdgeRunner 的回答

@孔鲤高考后暑假应该做什么事? - 孔鲤的回答

@Emily L能利用爬虫技术做到哪些很酷很有趣很有用的事情? - Emily L 的回答

@chenqin为什么 2015 年初,上海有卫计委官员呼吁大家生二胎? - chenqin 的回答

感兴趣的可以下载数据

getvoters.xls_免费高速下载

getvoters2.xls_免费高速下载

getvoters3.xls_免费高速下载

getvoters4.xls_免费高速下载

getvoters5.xls_免费高速下载

结论就是……没有结论。

话说我回答的那个三零用户比例也好高啊……我真的没有刷赞我保证!(话说我的赞里面要是有水军的话我会很伤心的……我一直以为是我写的好他们才赞我的QAQ)


到这里第一个项目就结束了…

这个我暂时不贴代码…代码不完善…还得有点小修改。两天内放上来。


——来贴代码——

loveQt/Zhihu_voters · GitHub

使用前请填写config.ini文件,cookie不用填。

依然不完善。是这样的,知乎在获取“等人赞同”的时候有一个很畸形的地方在于……答案的id很畸形。

比如我现在这个答案。

zhihu.com/question/2089

当我点击“等人赞同”的时候。抓包得到请求地址。我用的是Firefox的Firebug

这个地址是这样的:

zhihu.com/answer/152992

如果你继续往下拉,知乎会自动加载更多用户,你会得到形如这样的地址:

zhihu.com/answer/152992

分析这个地址的构成就会发现

/answer/这里应该是这个回答的唯一id,而这个id显然与上面的/question/20899988/answer/49749466是不一样的,我抓了好多个回答,结论应该是没有规律的,知乎应该是给每个问题一个id,每个问题下面的回答一个id,但是只有点赞的时候这个回答才会得到它关于voters的id……


所以我没办法实现完全的自动化…你如果想爬指定的回答,似乎得先手动抓包了 QAQ

抓包的示意如上,打开网络面板,点击“等人赞同”,找到地址中的数字就可以了。


如果你会抓包了请接着看下去。

代码的下载地址在上面的github。Python版本为2.7,希望你们会用pip安装依赖的库。

简单说几个方面。


1、知乎的登陆。我模仿了 @egrcc@7sDream 的项目,使用了requests.session。

def login():
    cf = ConfigParser.ConfigParser()
    cf.read("config.ini")
    cookies = cf._sections['cookies']

    email = cf.get("info", "email")
    password = cf.get("info", "password")
    cookies = dict(cookies)
    global s
    s = requests.session()
    login_data = {"email": email, "password": password}
    header = {
    'User-Agent': "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:34.0) Gecko/20100101 Firefox/34.0",
    'Host': "www.zhihu.com",
    'Referer': "http://www.zhihu.com/",
    'X-Requested-With': "XMLHttpRequest"
        }
    r = s.post(Login_url, data=login_data, headers=header)

在实现了登陆之后,只要使用s.get(url)得到的页面都是登陆成功的状态。

//注意,不登陆或者登陆不成功的情况下也可以爬取数据。点赞、感谢、提问、回答的数值都能正常获取,但是会出现部分用户无法获取名称和用户地址,显示为“知乎用户”


2、获取数据

假如我们获取到了单页数据,那么使用正则可以很简单地获取到想要的数据,具体参见代码。

我们需要做的,其实是获取那些需要爬取的URL。

通过上面对于网址的分析我们可以发现,网址的组成为domain/answer/ans_id/voters_profile?total=xxx&offset=xx&...

后面那堆乱码不重要,重要的是total和offset,每次会展示出10个用户的数据,所以我们只需要获取到点赞的总数total,就可以知道需要循环多少步(total/10),注意从是0开始,不然会漏掉前十个数据。

而这也就是我在zhihu-vote.py中的做法。其余的部分就没有什么难度了,入门的同学应该都可以看懂。


3、改进

我们在zhihu-vote.py中通过构造地址的方法来,通过循环实现对所有voters_profile的遍历。但是如果我们了解json的知识的话,我们可以发现其实每个页面都是json格式的。


其中最关键的地方在于next。我们会发现其实每个页面中都包含了下一页的地址!这样我们是不是可以让爬虫每爬一页自己找到一个地址,然后自己去爬下一页呢?

可是这样做有一个问题,如何控制循环呢?

假如我们去看最后一个页面的话,会发现是这样的。


注意这里的next值为空

而我们知道(不知道的你现在知道了),空字符串在作为条件判断时相当于False

所以我写了zhihu-voteV2.py

其中核心的改动是

Vote_url = Zhihu + 'answer/' + ans_id +'/voters_profile'
h = s.get(Vote_url)
html = h.content.encode('utf-8')
target = json.loads(html)
while target['paging']['next']:
    Vote_url = 'http://www.zhihu.com'+target['paging']['next']

这样就实现了程序每次爬取页面时从页面中获取地址,而不是人为构造地址循环。下面是原来的做法。

for num in range (0,page_num):
    Vote_url = Zhihu + 'answer/' + ans_id +'/voters_profile?total='+str(total)+'&offset='+str(num)+'0'


讲实话我不知道这两种写法哪种好,但我还是蛮高兴自己发现了第二种做法。

于是我做了一个运行时间的测试…提车了,下一步需要做什么? - 车海沉浮高永强的回答 16K的赞

运行结果如下:


构造地址的办法用时451秒,第二种办法用时251秒。

……我不知道为什么方法二会比方法一快,可能是网速吧……QAQ。有了解的前辈还望告知原因…


到这里也就结束了。最后的结果是写入excel的,有知友说让我去学习csv,已经在看了,不过这次还是用的让人又爱又恨的excel。


按照惯例写To-dos:

  • 完善Github的文档说明
  • 想办法看能不能自动获取那个蛋疼的ans_id就不用每次都手动抓包了,selenium?我不会用啊TAT
  • 在点赞的页面我们只能得到用户的4个数据,也就是赞同、感谢、提问、回答,有些时候我们或许想知道他的关注人数和被关注人数…然而那个得到用户的页面中去爬取了。不过想通过用户URL得到用户的具体数据是有现成的轮子的……egrcc/zhihu-python · GitHub (PY2) @egrcc7sDream/zhihu-py3 · GitHub(PY3) @7sDream 我想办法看怎么把我现在获取答案点赞用户信息的方法pull给他们…直接调用他们的User类就ok了~
  • 抓特别多的数据时考虑多线程和gzip…?接下来就要学这个了…会的话我就用了…还记得我去爬知乎赞数最高的答案…一个答案爬了30分钟…

——更新完毕,大家学习愉快,共同进步——

——Windows 平台Py2编码问题畸形但有效解法——

在..\Python27\Lib\site-packages\下新建sitecustomize.py

添加代码

import sys
sys.setdefaultencoding("utf-8")


——————正文2:学习路上顺便写的项目——————

在学习路上写了许多类似test的小小项目,就不赘述了。下面贴出来三个还算有结果的。

1、抓取知乎话题下面的问题,分析容易得赞的问题

具体描述在 第一次在知乎获得 1000 以上的赞是什么体验? - 段晓晨的回答 写过了。

代码在知乎将如何应对「狗日的知乎」计划? - 段晓晨的回答 里面有。需要用到7sDream/zhihu-py3 · GitHub


2、写完1中项目以后。我爬取了爬虫话题分类下面的所有回答。结果爬虫话题所有问题_20150531.xls_免费高速下载

然后我从其中挑选了“关注量/回答量”较大的问题(也就是有人关注但有效回答较少)写了以下两个回答,大家可以看看。

如何使用 python 抓取 雪球网页? - 段晓晨的回答

如何用Python抓取写一个抓取新浪财经网指定企业年报的脚本? - 段晓晨的回答


——————结语:谈谈学习——————

至此我能说的就说完了。

鼓起勇气来回答这个问题,不知道自己有没有资格。毕竟自己也就才学了一周多一点。自认为还谈不上入门……因为不会的实在太多。

系统学习爬虫的思路别人讲的肯定比我好。我的经验在开头已经说过了……不要怂就是干哪里不会搜哪里!哪里报错改哪里!

如果一定要再补充写什么,贴上我之前回复知友的评论吧。

首先要带着一个目的去学,这个目的不能太复杂,不能一上来就搞那种需要模拟登陆,需要js动态实现的网站,那样你会在登陆那儿卡很久,又在js实现那儿卡很久,很容易挫伤学习积极性。比如我最初的目的就是爬知乎。知乎登陆/不登陆数据会有差别,比如抓不到某些人的数据,返回“知乎用户”这种。

有了目的,你需要一些基础知识。html是什么,标签是什么,浏览器和服务器之间通信(比如抓包)。爬虫的原理就是要把网页的源码整个下载下来,然后去里面寻找我们需要的信息。所以首先你得能获取正确的网址,然后通过配置你的程序(Headers伪装浏览器,代理防止封ip等)来成功访问网页并获取源码。…………诸如此类的基础知识,其实特别简单。你可以去找一些爬百度贴吧,爬煎蛋,爬糗事百科的例子,很容易就会上手。

有了源码你需要去里面寻找东西,比较简单的有正则表达式,更方便的有BeautifulSoup。对json解析有json。等等。

最后你可能需要一些模块化的思想。比如我在写爬知乎问题的时候,写了一些代码来让它把输出的结果自动保存到excel里…那我是不是可以把写入excel这个行为单独抽出来,定义为一个方法。以后每次遇到需要excel的地方我就拿过来改一下就能用。同样的思路,登陆过程,post数据的过程,解析数据的过程,是不是都可以自己慢慢积累为模块。就好像你有了很多乐高积木,以后再做的时候就不需要做重复的事情,直接搭积木就好~


最后感谢一下在我学习过程中参考过的别人的回答和博客。太多了无法一一列举。再次感谢。


编程是最容易获得的超能力。你还在等什么?