我们曾经抓取过猫眼电影TOP100,并进行了简单的分析。但是众所周知,豆瓣的用户比较小众、比较独特,那么豆瓣的TOP250又会是哪些电影呢?
我在整理代码的时候突然发现一年多以前的爬虫代码竟然还能使用……那今天就用它来演示下,如何通过urllib+BeautifulSoup来快速抓取解析豆瓣电影TOP250。
首先我们观察url地址,连续点击几页之后我们发现,豆瓣电影TOP250一共分10页,每页有25部电影,每页的url
地址的格式为https://movie.douban.com/top250?start={0}&filter=
,大括号中的部分用这一页的第一部电影的编号代替,编号从0开始一直到249。
因此我们可以通过格式化字符串来生成所有的url
地址:
url_init = 'https://movie.douban.com/top250?start={0}&filter='
urls = [url_init.format(x * 25) for x in range(10)]
print(urls)
输出为:
['https://movie.douban.com/top250?start=0&filter=',
'https://movie.douban.com/top250?start=25&filter=',
'https://movie.douban.com/top250?start=50&filter=',
'https://movie.douban.com/top250?start=75&filter=',
'https://movie.douban.com/top250?start=100&filter=',
'https://movie.douban.com/top250?start=125&filter=',
'https://movie.douban.com/top250?start=150&filter=',
'https://movie.douban.com/top250?start=175&filter=',
'https://movie.douban.com/top250?start=200&filter=',
'https://movie.douban.com/top250?start=225&filter=']
当然,这个地址未必就是我们需要请求的地址。我们先打开TOP250的第一页,右键检查,单击进入网络(Network)选项卡,刷新一下,可以看到出现了一大堆请求的返回结果,我们先打开第一个document
类型的请求。
我们切换到Headers标签,可以看到,在General
下,Request URL
的取值与网页地址一样。也就是说,我们上边生成的10条url,正是我们需要请求的地址。
接下来我们看一下电影的信息藏在哪里。我们切换到Response标签下,搜索网页中的内容,比如我们先搜一下榜首的名称:肖申克的救赎。
可以看到,所有的电影信息都在一个class="grid_view"
的
(有序列表)标签下,每一部电影是一个标签。
在每个下:
class="title"
的
标签下;的子标签
中;
- 得分和评分人数在
下
好,接下来我们就开始抓取并解析这些内容。
三、抓取并解析
首先我们定义一个函数,用来打开url
并返回BeautifulSoup对象。
# -*- coding:utf-8 -*-
from urllib.request import urlopen
from bs4 import BeautifulSoup
from collections import defaultdict
import pandas as pd
import time
import re
class DoubanMovieTop():
def __init__(self):
self.top_urls = ['https://movie.douban.com/top250?start={0}&filter='.format(x*25) for x in range(10)]
self.data = defaultdict(list)
self.columns = ['title', 'link', 'score', 'score_cnt', 'top_no', 'director', 'writers', 'actors', 'types',
'edit_location', 'language', 'dates', 'play_location', 'length', 'rating_per', 'betters',
'had_seen', 'want_see', 'tags', 'short_review', 'review', 'ask', 'discussion']
self.df = None
def get_bsobj(self, url):
html = urlopen(url).read().decode('utf-8')
bsobj = BeautifulSoup(html, 'lxml')
return bsobj
在这个函数中,我们使用urllib.requests.urlopen
来发起请求,对返回的响应结果,我们使用.read()
方法来读取内容。但是这里读取到的内容是字节(bytes),我们要将其转化为字符串,所以我们要再使用字节对象的.decode('utf-8')
方法进行转化;然后我们使用bs4.BeautifulSoup()
从字符串中生成BeautifulSoup
对象。
这里我们为什么选择utf-8
编码进行解码呢?这是因为这个网页的编码正是utf-8
。一般情况下,我们可以从网页的
中找到编码信息,这样我们就可以对于不同编码的网页进行针对性的解码了。
接下来我们开始解析电影的信息。我们定义一个函数,以上边get_bsobj(url)
函数输出的BeautifulSoup
对象为输入,以数据列表为输出。
def get_info(self):
for url in self.top_urls:
bsobj = self.get_bsobj(url)
main = bsobj.find('ol', {'class': 'grid_view'})
# 标题及链接信息
title_objs = main.findAll('div', {'class': 'hd'})
titles = [i.find('span').text for i in title_objs]
links = [i.find('a')['href'] for i in title_objs]
# 评分信息
score_objs = main.findAll('div', {'class': 'star'})
scores = [i.find('span', {'class': 'rating_num'}).text for i in score_objs]
score_cnts = [i.findAll('span')[-1].text for i in score_objs]
for title, link, score, score_cnt in zip(titles, links, scores, score_cnts):
self.data[title].extend([title, link, score, score_cnt])
bsobj_more = self.get_bsobj(link)
more_data = self.get_more_info(bsobj_more)
self.data[title].extend(more_data)
print(self.data[title])
print(len(self.data))
time.sleep(1)
我们在榜单列表页面直接获取了标题、电影详情页面地址、评分、评分人数信息。
接下来,可以看到我对所有的电影详情页面进行了一个循环抓取解析,这是因为在榜单页面中信息展示不全,且这里的信息不够丰富,在详情页中,我们可以获取非常丰富的数据,包括导演、编剧、演员、上映时间和地区、语言、别名、短评数、影评数、多少人想看、多少人看过……
获得了这么多信息之后,我们可以进行更加深入的分析,因此我们选择进入详情页进一步抓取更多信息。因此我们需要定义一个函数,用来解析详情页。
豆瓣的页面抓取难度较小,不过我们这里定义了较多的字段,因此这个函数会显得比较长。这个函数里我们使用了两个try...except...
来应对异常,这是因为有些电影没有编剧或者主演,这会导致抓取异常,针对这种情况,我们直接将该字段留空即可。
每个字段抓取的表达式都是根据返回的源码得到的,BeautifulSoup
的使用非常简单,几分钟就可以上手,半小时就可以入门。事实上我现在更喜欢使用XPath
表达式,更灵活、更强大,对XPath
的使用不了解的同学可以去看我的另一篇抓取网易云音乐的文章。在这个例子中,有些BeautifulSoup
不太容易实现的部分,我们结合了re
正则表达式来完成。
关于BeautifulSoup
的使用,可以参考这份教程:https://docs.pythontab.com/beautifulsoup4/。至于学习程度,仍然是按照我们的二八法则,不需深究,学习最少的内容,覆盖最多的应用即可,剩下的在实战中遇到了再去检索学习即可。
def get_more_info(self, bsobj):
# 榜单排名
top_no = bsobj.find('span', {'class': 'top250-no'}).text.split('.')[1]
# 更多信息
main = bsobj.find('div', {'id': 'info'})
# 导演
dire_obj = main.findAll('a', {'rel': 'v:directedBy'})
director = [i.text for i in dire_obj]
# 编剧
try:
writer_obj = main.findAll('span', {'class': 'attrs'})[1]
writers = [i.text for i in writer_obj.findAll('a')]
except Exception as e:
writers = []
print(e)
# 主演
try:
actor_obj = main.findAll('a', {'rel': 'v:starring'})
actors = [i.text for i in actor_obj]
except Exception as e:
actors = []
print(e)
# 类型
type_obj = main.findAll('span', {'property': 'v:genre'})
types = [i.text for i in type_obj]
# 制片地区
pattern = re.compile('地区: (.*?)\n语言', re.S)
edit_location = re.findall(pattern, main.text)[0]
# 语言
pattern2 = re.compile('语言: (.*?)\n上映日期')
language = re.findall(pattern2, main.text)[0]
# 上映日期/地区
date_obj = main.findAll('span', {'property': 'v:initialReleaseDate'})
dates = [i.text.split('(')[0][:4] for i in date_obj]
play_location = [i.text.split('(')[1][:-1] for i in date_obj]
# 片长
length = main.find('span', {'property': 'v:runtime'})['content']
# 5星到1星比例
rating_obj = bsobj.findAll('span', {'class': 'rating_per'})
rating_per = [i.text for i in rating_obj]
# 好于
better_obj = bsobj.find('div', {'class': 'rating_betterthan'})
betters = [i.text for i in better_obj.findAll('a')]
# 想看/看过
watch_obj = bsobj.find('div', {'class': 'subject-others-interests-ft'})
had_seen = watch_obj.find('a').text[:-3]
want_see = watch_obj.findAll('a')[-1].text[:-3]
# 标签
tag_obj = bsobj.find('div', {'class': 'tags-body'}).findAll('a')
tags = [i.text for i in tag_obj]
# 短评
short_obj = bsobj.find('div', {'id': 'comments-section'})
short_review = short_obj.find('div').find('span', {'class': 'pl'}).find('a').text.split(' ')[1]
# 影评
review = bsobj.find('a', {'href': 'reviews'}).text.split(' ')[1]
# 问题
ask_obj = bsobj.find('div', {'id': 'askmatrix'})
ask = ask_obj.find('h2').find('a').text.strip()[2:-1]
# 讨论
discuss_obj = bsobj.find('p', {'class': 'pl', 'align': 'right'}).find('a')
discussion = discuss_obj.text.strip().split('(')[1][2:-2]
more_data = [top_no, director, writers, actors, types, edit_location, language, dates, play_location,
length, rating_per, betters, had_seen, want_see, tags, short_review, review, ask, discussion]
return more_data
成功抓取之后,我们还需要定义一个函数,用来将数据缓存到本地或其他途径(比如数据库),用于后续分析。
def dump_data(self):
data = []
for title, value in self.data.items():
data.append(value)
self.df = pd.DataFrame(data, columns=self.columns)
self.df.to_csv('douban_top250.csv', index=False)
好了,一个针对豆瓣电影TOP250的爬虫就写完了,接下来我们执行抓取。
if __name__ == '__main__':
douban = DoubanMovieTop()
douban.get_info()
douban.dump_data()
抓取完成后,我们就可以看到我们的数据了。
那么有人可能会问,我们抓取到数据就结束了吗?
当然没有,在下一篇文章中,我们会实战演练如何对我们得到的数据进行数据清洗和分析挖掘。