这是全栈数据工程师养成攻略系列教程的第九期:9 实战 爬取豆瓣电影数据。
掌握了爬虫的基本原理和代码实现,现在让我们通过实战项目巩固一下。
确定目标
在写爬虫之前应当想清楚:我需要哪方面的数据?需要包含哪些字段?这些数据需要以何种形式呈现?
很多网站往往都是大家爬取的对象,例如提供住房信息的链家网,提供书评和影评信息的豆瓣网,以及提供餐饮生活娱乐信息的大众点评网。当然,这些网站之所以能很容易地被我们爬取,也是因为它们采取了内容开放的运营态度,没有进行过多的反爬处理。
有一个挺有意思的名词叫做“三月爬虫”,即在三月份左右频繁出现的大量小规模爬虫。这些爬虫从何而来?因为在每年的这个时候,学生们都要做课设项目了,没有数据怎么办?嗯,靠爬!
其实爬虫和反爬虫之间就好比矛与盾的关系,我们可以花更多的心思、时间和成本去爬取数据,数据运营方同样可以花更多的技术、金钱和人力以保护数据。识别代码请求并禁止,我们可以伪装成浏览器;对IP频繁请求采取限制,我们可以使用IP代理池;要求登录并输入复杂的验证码,我们同样可以模拟登录以及想出相应的解决办法。总而言之,没有一定能爬到的数据,也没有一定爬不到的数据,无非是攻守双方的博弈,看谁下的功夫更深、投入成本更多。
当然,之前介绍的都是最基础的爬取方法,所针对的也是采取开放运营态度,或者暂未采取防爬措施的网站。方法虽简单,但依旧足以爬取相当多的网站,至于爬虫的进一步深入研究,则需要花费更多时间去学习,这也是为什么有专业的爬虫工程师这一方向了。
在这次的项目实战中,我们需要获取豆瓣电影上的电影数据,数量自然是越多越好,每条数据应当包含电影名称、导演、演员、类型、片长、语言、上映时间、上映地区、评分等信息,这样在获取数据并存储之后便可进行后续分析和展示。
通用思路
写爬虫时往往会遵循以下通用思路:首先得找到一个汇总页,以链家网为例,可以是首页或搜索页。在汇总页中是一条条房源,以列表形式依次排列,可能一页会安排几十条房源,看完之后可以通过翻页功能跳转至下一页,从而进行对全部房源的浏览;汇总页中的每一个链接都对应一条房源的详情页,点进去即可查看房源的详细信息。这些详情页都是用同样的模版渲染出来的,只不过渲染时使用了不同的数据,因此十分便于批量获取,只要对详情页的页面结构进行分析和提取即可。
当然,以上所提的二层结构是最理想的情况,实际问题中未必能找到这样一个能直接涵盖并通往全部详情页的汇总页,因此三层、四层乃至更复杂的结构也完全可能出现。例如从链家的首页开始,先下钻到城市,再深入到地区,接着按户型进行分类,最后才能找到所对应的房源。其实我们会发现,每一层结构都对应着最终详情页的一个字段,例如房源的城市、地区、户型等信息,多层结构无非只是对二层结构按照若干个字段进行了逐层聚合,所以只要理清楚网站数据的整理结构,接下来的代码工作都是类似的。
寻找链接
回到这次的项目实战上,我们首先访问一下豆瓣电影的首页,https://movie.douban.com/,探探路子,看看该如何去寻找之前提及的汇总页和详情页。
我们首先看到豆瓣提供了一个正在热映
模块,展示当前正在上映的一些电影。由于我们希望尽可能多地获取电影数据,当然包括历史电影,所以这块忽略不计。
接下来的页面中有一个按电影分类进行筛选的模块,如下图所示,可以根据热门、最新、经典、可播放等标签显示相应的电影。按热度排序、按时间排序和按评价排序意义不大,希望我们希望获得尽可能多的电影数据全集,对排序则不关心。再往下有个加载更多
的按钮,我们会发现每次点击之后,页面上就会出现更多对应类别的电影,说明网页相应地又向服务端请求了更多数据。
所以爬取的基本思路有了,首先获得全部的类别标签,然后针对每个标签不断地请求相应的电影数据,最后从每部电影的详情页获取所需的字段。让我们用Chrome开发者工具来找出应当请求哪些链接,打开开发者工具之后刷新豆瓣电影首页,我们会发现在Network
的XHR
中有这么个链接,https://movie.douban.com/j/search_tags?type=movie,从名字上来看似乎是返回电影标签,在浏览器中访问果不其然,得到了以下内容:
{"tags":["热门","最新","经典","可播放","豆瓣高分","冷门佳片","华语","欧美","韩国","日本","动作","喜剧","爱情","科幻","悬疑","恐怖","动画"]}
说明这是一个GET类型的API,返回一个json格式的字符串,如果在Python中加载成字典之后,则包含一个键tags,对应的值是一个列表,里面的每一项都是一个电影标签。
我们还顺便发现了另一个GET类API,https://movie.douban.com/j/search_subjects?type=movie&tag=热门&sort=recommend&page_limit=20&page_start=0,可以根据提供的标签、排序方法、每页数量、每页开始编号等参数返回相应的电影数据,这里是按推荐程度排名,从0号开始,返回热门标签下的20条电影数据。在浏览器中访问以上链接,得到的也是一个json格式字符串,同样转成Python字典再处理即可。如果点击加载更多
按钮,会发现网页会继续请求这个API,不同的只是page_start
不断增加,通过改变开始编号即可请求到新的数据。
设计下代码实现的过程:针对每个标签,使用以上第二个API不断请求数据,如果请求结果中包含数据,则将page_start
增加20再继续,直到返回结果为空,说明这一标签下的电影数据已经全部拿到。
代码实现
我们已经掌握了如何用Python发起GET和POST请求,所以接下来的工作就是写代码实现。
# 加载库
import urllib
import urllib2
import json
from bs4 import BeautifulSoup
# 获取所有标签
tags = []
url = 'https://movie.douban.com/j/search_tags?type=movie'
request = urllib2.Request(url=url)
response = urllib2.urlopen(request, timeout=20)
result = response.read()
# 加载json为字典
result = json.loads(result)
tags = result['tags']
# 定义一个列表存储电影的基本信息
movies = []
# 处理每个tag
for tag in tags:
start = 0
# 不断请求,直到返回结果为空
while 1:
# 拼接需要请求的链接,包括标签和开始编号
url = 'https://movie.douban.com/j/search_subjects?type=movie&tag=' + tag + '&sort=recommend&page_limit=20&page_start=' + str(start)
print url
request = urllib2.Request(url=url)
response = urllib2.urlopen(request, timeout=20)
result = response.read()
result = json.loads(result)
# 先在浏览器中访问一下API,观察返回json的结构
# 然后在Python中取出需要的值
result = result['subjects']
# 返回结果为空,说明已经没有数据了
# 完成一个标签的处理,退出循环
if len(result) == 0:
break
# 将每一条数据都加入movies
for item in result:
movies.append(item)
# 使用循环记得修改条件
# 这里需要修改start
start += 20
# 看看一共获取了多少电影
print len(movies)
以上代码运行完毕之后,列表movies
中即包含了全部的电影数据,其中的每一项都是一个字典,包含rate
评分、title
电影标题、url
详情页链接、playable
是否可播放、cover
封面图片链接、id
电影的豆瓣id、is_new
是否为新电影等字段。
除了以上字段,我们还希望获取每部电影的更多信息,因此需要进一步爬取各部电影所对应的详情页。以下是《疯狂动物城》的豆瓣详情页,导演、编剧、主演、类型、语言、片长、简介等,都是值得进一步爬取的字段。
由于电影详情页的url类型属于Html,即访问后返回经浏览器渲染的网页内容,所以需要更加复杂的处理方法。BeautifulSoup包提供了解析html文本、查找和选择html元素、提取元素内容和属性等功能,但要求一些html和css语法基础。这里仅以详情页中的电影简介为例,展示下如何使用BeautifulSoup解析html文本,其他完整内容等后续章节介绍了相关基础后再回过头讲解。
import time
# 请求每部电影的详情页面
for x in xrange(0, len(movies)):
url = movies[x]['url']
request = urllib2.Request(url=url)
response = urllib2.urlopen(request, timeout=20)
result = response.read()
# 使用BeautifulSoup解析html
html = BeautifulSoup(result)
# 提取电影简介
# 捕捉异常,有的电影详情页中并没有简介
try:
description = html.find_all("span", attrs={"property": "v:summary"})[0].get_text()
except Exception, e:
# 没有提取到简介,则简介为空
movies[x]['description'] = ''
else:
# 将新获取的字段填入movies
movies[x]['description'] = description
finally:
pass
# 适当休息,避免请求频发被禁止,报403 Forbidden错误
time.sleep(0.5)
最后,可以将获取的电影数据写入txt文件,以便后续使用。
fw = open('douban_movies.txt', 'w')
# 写入一行表头,用于说明每个字段的意义
fw.write('title^rate^url^cover^id^description\n')
for item in movies:
# 用^作为分隔符
# 主要是为了避免中文里可能包含逗号发生冲突
fw.write(item['title'] + '^' + item['rate'] + '^' + item['url'] + '^' + item['cover'] + '^' + item['id'] + '^' + item['description'] + '\n')
fw.close()
其他内容
这次的实战其实是我一个Github项目的一部分,https://github.com/Honlan/data-visualize-chain。这个项目以豆瓣电影为例,展示了如何进行数据获取、清洗、存储、分析和可视化,和爬虫相关的代码都在spider
文件夹下。所以如果希望了解更多,以及对BeautifulSoup用法感兴趣,可以进一步研究。
另外,以上代码最终获取了五千部左右的电影数据,明显少于豆瓣电影的总量,说明之前所使用的API能获取的数据量十分有限。那应该怎么办呢?在豆瓣电影的网站上找一找,会发现这么个链接,https://movie.douban.com/tag/,点进去之后简直是到了一个崭新的世界。在这个页面中提供了各种分类标签,每个标签下的电影数量也十分惊人,全部爬取一遍能收获的数据量必然相当可观。所以只要按照之前所讲的通用思路,类似地写代码爬取即可。
视频链接: