Beautiful Soup 爬虫收集豆瓣电影评论

前段时间研究了下豆瓣电影评论区的结构,加上一些bs4的技巧做了一个收集豆瓣电影品论的爬虫。

准备阶段

计划是用mongodb储存结果,这里可以换成其他的数据库。

1
2
3
4
5
6
7
-mongodb
-python2.7
|--pymongo
|--BeautifulSoup
|--requests
|--json
|-re

验证阶段

开始之前一定要确认豆瓣电影评论区是什么网页类型。BeautifulSoup只能用来爬静态网页。豆瓣的电影评论在点击进入时就已经加载完毕了,所以可以使用BeautifulSoup制作的爬虫读取。再分析一下电影评论页面链接的结构。这个结构非常有利于爬虫工作,对于不同的电影只需要更改ID即可。

https://movie.douban.com/subject/1889243/comments?start=0&limit=20&sort=new_score

链接结构 –> 电影区/对象/电影ID/评论页面?起点&数量&排序方式

评论区结构

下面随便拿一条评论来看评论区的结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<div class="comment-item" data-cid="861695112">
<div class="avatar">
<a title="芒果真好吃" href="https://www.douban.com/people/62654988/">
<img src="https://img3.doubanio.com/icon/u62654988-30.jpg" class="">
</a>
</div>
<div class="comment">
<h3>
<span class="comment-vote">
<span class="votes pr5">9864</span>
<input value="861695112" type="hidden">
<a href="javascript:;" class="j a_show_login">有用</a>
</span>
<span class="comment-info">
<a href="https://www.douban.com/people/62654988/" class="">芒果真好吃</a>
<span>看过</span>
<span class="allstar50 rating" title="力荐"></span>
<span class="comment-time ">
2014-11-11
</span>
</span>
</h3>
<p class=""> 诺兰说:如果我活得够长 那么豆瓣TOP250就被我承包了。</p>
</div>
</div>

评论结构也非常工整。每条品论都在一个独立的comment-item里,而且有唯一的data-cid作为评论ID,comment-item内部结构如下:

1
2
3
4
5
6
7
8
9
10
-评论(评论ID)
|-头像部分
|-评论部分
|-点赞人数
|-评论信息
|-用户昵称(用户主页链接)
|-电影状态
|-评分
|评论时间
|-评论文字部分

现在我们感兴趣的是:评论ID(避免重叠评论)、评分、点赞人数、评论时间、电影状态(只考虑看过)、评论文字。
针对上面的html可以有如下方法获得对应信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def parseComments(self,html):
cid = html.attrs['data-cid']
info = html.find("span",{"class":"comment-info"})
rating = info.findChildren("span")
if rating[0].text != u'看过':
return 'viwer has not seen the move.'
else:
rating_allstar = rating[1]['class'][0]
if "allstar" not in rating_allstar:
return 'no rating found'
else:
rating = int(re.sub("[^0-9]", "", rating_allstar))
vote = int(
html.find("span",{"class":"comment-vote"}).contents[1].contents[0]
)
comment_text = html.find("p").contents[0]\
.replace(' ','').replace('\n','')
date = info.findChild("span",{"class":""}).contents[0]\
.replace(' ','').replace('\n','')

return {"cid":cid,"date":date,"rating":rating,"vote":vote,"comment":comment_text}

页面结构

一个加载完成的页面里会有若干个评论区(comment-item),需要将分析的代码应用到每一个评论区。

1
2
3
4
5
6
for item in commentsdiv:
comment_data = parseComments(item) #调用函数
if comment_data == 'no rating found' or comment_data == 'viwer has not seen the move.':
continue #无效评论,不考虑
else:
#有效评论,添加代码进行处理#

现在已经处理完一个页面里的所有评论,接着就要利用评论的网页链接来遍历所有评论,基本就是选择一个排序方式,比如按照点赞数排,用一个循环从第一条开始扫描下去。

1
2
3
4
5
从第一条评论开始,到第5000条,步长20条/页:
打开网页
用BeautifulSoup读取html
分析页面
分析评论区

验证码问题

豆瓣有一点好,不登录是无法查看大量评论的,而登录就要解决验证码的问题。验证码通常是一个单词,豆瓣的服务器验证的方式是把用户输入的单词和单词所在图片的编码比较。也就是说用户等录时对服务器发出一个post请求。

1
2
3
4
5
6
7
payload = {
'form_email': #用户名、邮箱#,
'form_password': #密码#,
'captcha-solution': #验证码内单词#,
'captcha-id': #验证码编码#,
'remember': #on/off 是否记住这台电脑#
}

登录一旦被拒绝,就需要输入验证码,那么这时豆瓣会重新载入登录页,编码和单词都可以在页面内找到。验证码图片可以通过id为captcha_image的图片tag定位,而编码就是这个图片链接的id部分。

1
2
3
4
5
def parseCaptcha(landingPage):
img = landingPage.find('img',{'id':'captcha_image'})
link = img.attrs['src']
captcha_id = link.split('id=')[1][:-7]
return {'pic':link, 'id':captcha_id}

这时只需要打开图片看一眼单词,然后把编码输入到payload里再次发送post request就可以,“记住这台电脑”打开后第二次就不许要输入验证码。登录成功记得返回request session,一个session的时间基本可以满足爬虫的需要(爬5000条没有问题)。

完整代码 GitHub

上述代码可以直接参考repo里的scraper.py

题外话

给你讲个笑话:中国网站的API(先笑十分钟好吧)。

其实如果有API数据接口,直接一个call就解决的问题,不知道是不重视数据还是过分重视数据,豆瓣API页面竟然是这么描述的:

为了保护豆瓣用户的数据,防止API被滥用或恶意使用,豆瓣要求每个API的使用者申请一个API Key,
而每个API Key唯一标识一个API使用者。
你可以在申请页面创建一个新应用,填写必要信息后提交,就能得到你的API Key。

太不专业了,哪有这么做API的?国际通行做法就是用户直接可以拿到key,根本不需要什么申请,然后非商业用途设有每天或者每小时的限制,商业用途可以联系客服洽谈。申请也行,这是申请链接

https://developers.douban.com/apikey/apply

点进去是“页面不存在”,Excuse me?? 不知道豆瓣做这个API的目的是什么。