实战:利用Python爬取豆瓣电影Top250数据并可视化分析
1. 为什么我们要爬取豆瓣电影Top250?
每次看到豆瓣电影Top250的榜单,你是不是也和我一样,心里会冒出很多问号?比如,评分最高的电影是哪部?评价人数最多的电影是不是评分也最高?哪个国家的电影在榜单里最受欢迎?这些电影都是什么类型的?以前,要回答这些问题,我们只能一部一部地手动记录,或者凭感觉猜测,既费时又不准确。
现在好了,我们有了Python这个“万能工具”。今天,我就带你一起,用Python写一个程序,自动把豆瓣电影Top250的数据“搬”到我们自己的电脑里。这可不是简单的复制粘贴,而是让程序像一个小机器人,自动浏览网页、识别信息、整理成表格。更酷的是,我们不仅要拿到数据,还要用图表把它“画”出来,让数据自己“说话”,看看里面藏着哪些我们没注意到的秘密。
整个过程,我会假设你是一个刚接触Python不久的朋友,可能对“爬虫”这个词有点陌生,甚至有点怕。别担心,我会把每一步都掰开揉碎了讲,从安装软件到运行代码,再到画出漂亮的图表,保证你能跟着做下来。你不需要是编程高手,只要有好奇心,会复制粘贴代码,就能完成这个有趣的项目。最终,你不仅能得到一份专属的电影数据分析报告,还能掌握一套非常实用的数据获取和处理技能,以后想分析其他网站的数据,思路也是一样的。
2. 动手前的准备工作:搭建你的工具箱
工欲善其事,必先利其器。在开始写代码之前,我们需要把“战场”布置好。这个过程很简单,就像给新手机安装几个必备的APP。
2.1 安装Python:获得你的“超能力”
Python是我们的核心工具,它就像一把瑞士军刀,功能强大又容易上手。首先,你需要确认电脑上是否已经安装了Python。打开你的命令行工具(Windows上是“命令提示符”或PowerShell,Mac或Linux上是“终端”),输入 python --version 或者 python3 --version 并回车。
如果屏幕上显示类似 Python 3.8.10 这样的版本号,并且数字大于等于3.6,那么恭喜你,可以直接进入下一步。如果提示“找不到命令”,那就需要去安装了。我强烈推荐去Python的官方网站下载安装包,记得勾选“Add Python to PATH”这个选项(Windows用户尤其要注意),这样系统才能在任何地方找到Python命令。安装过程就像安装普通软件一样,一路“下一步”即可。
2.2 安装必备的“武器库”
有了Python,我们还需要几个专门的库来帮忙。你可以把它们想象成游戏里的不同技能:requests 负责和网站服务器“对话”,获取网页原始代码;BeautifulSoup4 是个“解析大师”,能把杂乱无章的HTML代码整理成我们看得懂的结构;pandas 是“数据处理专家”,能轻松地对数据进行排序、筛选和计算;matplotlib 和 seaborn 则是两位“画家”,负责把枯燥的数字变成直观的图表。
安装它们只需要一行命令。再次打开你的命令行工具,输入下面的命令并回车:
pip install requests beautifulsoup4 pandas matplotlib seaborn openpyxl
这里多了一个 openpyxl,它是为了让我们能更好地生成和读写Excel文件。输入命令后,你会看到屏幕上开始滚动下载和安装的信息,稍等片刻,直到出现“Successfully installed”的字样,就表示所有“武器”都装备完毕了。如果遇到网络慢或者报错,可以尝试在命令后面加上 -i https://pypi.tuna.tsinghua.edu.cn/simple 来使用国内的镜像源加速下载。
3. 深入敌后:分析网页结构与爬取策略
写爬虫程序,有点像玩一个“找东西”的游戏。我们得先搞清楚,想要的东西藏在网页的哪个角落,有什么规律。直接打开浏览器,访问豆瓣电影Top250的页面(https://movie.douban.com/top250),然后按下F12键打开“开发者工具”,这是我们观察网页内部结构的“透视镜”。
3.1 找到数据的藏身之处
在开发者工具里,点击左上角那个鼠标箭头的图标,然后去页面上点击任意一部电影的区域。你会发现,右侧的代码区域会自动高亮显示对应的一部分HTML代码。仔细看,每一部电影的信息都被包裹在一个 <div class="item">...</div> 的标签里。这就是我们的目标单元!我们写程序就是要先找到所有的 class="item" 的div标签,然后再逐个从里面提取详细信息。
接下来,我们看看具体信息在哪。比如电影名称,它通常在一个 <span class="title"> 标签里;评分在 <span class="rating_num"> 里面;评价人数则在 class="star" 的div下的最后一个span标签里。导演、年份、国家等信息,则集中在一个 <p class=""> 的段落里,它们之间用斜杠 / 或换行符分隔。通过这样观察,我们就知道了该命令程序去“挖”哪些标签。
3.2 设计高效的爬取路线
豆瓣Top250一共有10页,每页25部电影。观察地址栏你会发现一个规律:第一页网址是 ?start=0,第二页是 ?start=25,第三页是 ?start=50……这个 start 参数就像页码指示器,每次增加25。所以,我们写一个循环,让 start 的值从0开始,每次加25,一直加到225,就能遍历所有10页了。
这里有个非常重要的细节:礼貌爬取。我们不能像轰炸机一样一秒内请求几十次页面,那样很容易被网站识别为恶意攻击,从而封锁我们的IP。因此,在爬取每一页之后,我们需要让程序“休息”一下,比如睡上1到2秒钟。这既是对网站服务器的尊重,也能保证我们稳定地拿到数据。我一般用 time.sleep(1) 来让程序暂停1秒,实测下来这个频率比较稳妥,很少会被反爬虫机制拦截。
4. 编写爬虫核心代码:一步步把数据抓回来
分析清楚了,现在开始动手写代码。我会把代码分成几个功能明确的函数,这样结构清晰,也方便你理解和修改。
4.1 打造单页数据抓取函数
我们先写一个函数,专门负责处理一页25部电影的数据抓取。这个函数是整个爬虫的“心脏”。
import requests
from bs4 import BeautifulSoup
import time
def get_movie_data(page_start):
"""
爬取豆瓣电影Top250单页数据
:param page_start: URL中的start参数,如0, 25, 50...
:return: 包含当前页25部电影信息的列表,每部电影是一个子列表
"""
# 1. 设置请求头,伪装成普通浏览器访问
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
}
# 2. 构造当前页的完整URL
url = f"https://movie.douban.com/top250?start={page_start}&filter="
# 3. 发送网络请求,获取网页内容
# 这里增加了异常处理,网络不稳定时程序不会直接崩溃
try:
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status() # 如果状态码不是200,会抛出异常
response.encoding = 'utf-8' # 设置编码,防止中文乱码
except requests.RequestException as e:
print(f"请求页面失败 (start={page_start}): {e}")
return [] # 返回空列表,不影响其他页的抓取
# 4. 使用BeautifulSoup解析HTML
soup = BeautifulSoup(response.text, 'html.parser')
# 5. 找到当前页所有电影项目
movie_items = soup.find_all('div', class_='item')
page_data = [] # 用来存储这一页的所有电影数据
# 6. 遍历每一个电影项目,提取详细信息
for item in movie_items:
try:
# 电影中文名(注意,有些电影可能有多个名字,这里取第一个)
name = item.find('span', class_='title').text.strip()
# 评分
score = item.find('span', class_='rating_num').text.strip()
# 评价人数,需要处理“xx万”这种格式
comment_tag = item.find('div', class_='star').find_all('span')[-1]
comment_text = comment_tag.text.replace('人评价', '').strip()
if '万' in comment_text:
comment_num = int(float(comment_text.replace('万', '')) * 10000)
else:
comment_num = int(comment_text)
# 导演、主演、年份、地区、类型等信息都在一个p标签里
bd = item.find('div', class_='bd')
info_paragraph = bd.find('p', class_='').text.strip()
# 这个段落信息比较乱,我们用换行符分割成多行处理
info_lines = info_paragraph.split('\n')
info_lines = [line.strip() for line in info_lines if line.strip()] # 清理空行和空格
# 通常第一行是导演和主演信息
director_actor_line = info_lines[0].replace('\xa0', ' ') # 替换掉网页中的特殊空格
# 分割导演和主演
if '主演:' in director_actor_line:
director_part, actor_part = director_actor_line.split('主演:', 1)
director = director_part.replace('导演:', '').strip()
actor = actor_part.strip()
else:
# 有些电影可能没有列出主演
director = director_actor_line.replace('导演:', '').strip()
actor = '暂无'
# 通常第二行是年份、地区、类型,用 / 分割
if len(info_lines) > 1:
details = info_lines[1].split('/')
details = [d.strip() for d in details]
# 确保有足够的信息,有些条目可能不完整
year = details[0] if len(details) > 0 else '未知'
area = details[1] if len(details) > 1 else '未知'
genre = details[2] if len(details) > 2 else '未知'
else:
year, area, genre = '未知', '未知', '未知'
# 把一部电影的所有信息打包成一个列表,添加到当前页数据中
page_data.append([name, score, comment_num, director, actor, year, area, genre])
except AttributeError as e:
# 如果某个标签找不到,跳过这部电影,打印错误但不中断程序
print(f"解析一部电影时出错,已跳过。错误信息: {e}")
continue
return page_data
这个函数里我加了 try...except 异常处理。因为网页结构可能会微调,或者某部电影信息缺失,有了异常处理,即使遇到一点小问题,程序也不会整个崩溃,只会跳过有问题的电影继续运行, robustness(健壮性)大大增强。
4.2 循环抓取所有页面
单页抓取函数写好之后,抓取全部10页就非常简单了,只需要一个循环。
def crawl_all_movies():
"""
爬取所有10页,共250部电影的数据
:return: 包含250部电影数据的二维列表
"""
all_data = []
total_pages = 10
for page in range(total_pages):
start = page * 25 # 计算每一页的start参数
print(f'正在爬取第 {page + 1} 页 (start={start})...')
one_page_data = get_movie_data(start)
all_data.extend(one_page_data) # 将本页数据添加到总列表中
time.sleep(1.5) # 每爬完一页,礼貌地等待1.5秒
print(f'所有页面爬取完成!共获取 {len(all_data)} 条电影数据。')
return all_data
运行 crawl_all_movies() 函数,你会看到控制台一页一页地打印进度。等待一两分钟,所有数据就静静地躺在 all_data 这个变量里了。这个过程是不是有点像在看一个机器人帮你勤奋地收集资料?
5. 数据清洗与保存:让数据变得规整可用
爬取下来的原始数据可能有些小瑕疵,比如前后空格、格式不统一等。直接使用可能会影响后续分析,所以我们先做个简单的“大扫除”,然后保存到文件里。
5.1 使用Pandas进行数据清洗
Pandas是处理表格数据的利器。我们先把列表数据转换成Pandas的DataFrame,它看起来就像一个Excel表格,操作起来非常方便。
import pandas as pd
def clean_and_save_data(movie_list, excel_filename='douban_top250.xlsx'):
"""
清洗数据并保存到Excel
:param movie_list: crawl_all_movies()返回的原始数据列表
:param excel_filename: 要保存的Excel文件名
:return: 清洗后的DataFrame
"""
# 定义表格的列名
columns = ['电影名称', '评分', '评价人数', '导演', '主演', '上映年份', '国家/地区', '类型']
# 将列表转换为DataFrame
df = pd.DataFrame(movie_list, columns=columns)
# 数据清洗
print("正在进行数据清洗...")
# 1. 确保评分是数值类型
df['评分'] = pd.to_numeric(df['评分'], errors='coerce')
# 2. 确保评价人数是整数类型
df['评价人数'] = pd.to_numeric(df['评价人数'], errors='coerce').astype('Int64')
# 3. 处理“上映年份”列,只提取数字年份(有些条目可能包含月份)
df['上映年份'] = df['上映年份'].str.extract(r'(\d{4})').fillna('未知')
# 4. 去除“电影名称”列可能存在的首尾空格
df['电影名称'] = df['电影名称'].str.strip()
# 保存到Excel
with pd.ExcelWriter(excel_filename, engine='openpyxl') as writer:
df.to_excel(writer, sheet_name='Top250', index=False)
# 自动调整列宽,让Excel看起来更舒服
worksheet = writer.sheets['Top250']
for column in worksheet.columns:
max_length = 0
column_letter = column[0].column_letter
for cell in column:
try:
if len(str(cell.value)) > max_length:
max_length = len(str(cell.value))
except:
pass
adjusted_width = min(max_length + 2, 50) # 设置最大宽度为50
worksheet.column_dimensions[column_letter].width = adjusted_width
print(f"数据已清洗并保存至文件:{excel_filename}")
return df
# 使用示例
# all_movies = crawl_all_movies()
# df_movies = clean_and_save_data(all_movies)
清洗完成后,你的目录下会生成一个 douban_top250.xlsx 文件,用Excel或WPS打开它,一份干净整齐的电影榜单就诞生了!但这只是开始,好戏还在后头。
6. 让数据开口说话:多维度可视化分析
拿到数据表格,我们可以用Matplotlib和Seaborn来画图。图表能瞬间揭示数字背后的规律,比单纯看表格直观得多。我们来问数据几个问题,并用图表来回答。
6.1 评分分布:高分电影有多“卷”?
首先,我们好奇这250部顶尖电影的评分都集中在哪个区间。是9分以上神仙打架,还是8.5分左右扎堆?
import matplotlib.pyplot as plt
import seaborn as sns
# 设置中文字体,防止图表出现乱码(根据你的系统调整字体路径)
plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
def plot_rating_distribution(df):
"""
绘制评分分布直方图与密度曲线
"""
plt.figure(figsize=(12, 6))
# 绘制直方图
plt.subplot(1, 2, 1)
plt.hist(df['评分'], bins=20, edgecolor='black', alpha=0.7, color='skyblue')
plt.title('豆瓣Top250电影评分分布直方图', fontsize=14)
plt.xlabel('评分', fontsize=12)
plt.ylabel('电影数量', fontsize=12)
plt.grid(axis='y', alpha=0.3)
# 绘制核密度估计曲线
plt.subplot(1, 2, 2)
sns.kdeplot(df['评分'], fill=True, color='orange', alpha=0.6)
plt.title('评分密度曲线', fontsize=14)
plt.xlabel('评分', fontsize=12)
plt.ylabel('密度', fontsize=12)
plt.grid(alpha=0.3)
plt.tight_layout()
plt.savefig('rating_distribution.png', dpi=300, bbox_inches='tight')
plt.show()
# 打印一些统计信息
print("评分描述性统计:")
print(df['评分'].describe())
print(f"\n评分高于9.0的电影有 {len(df[df['评分'] >= 9.0])} 部")
运行这个函数,你会看到两张并排的图。直方图能清晰显示在各个分数段有多少部电影,而密度曲线则平滑地展示了评分的集中趋势。从我的分析结果来看,绝大多数电影评分集中在8.5到9.2分之间,9分以上的“神作”并不多,符合“金字塔尖”的规律。
6.2 评价人数 vs 评分:叫好和叫座的关系
我们常听说“叫好不叫座”或者“票房冠军口碑一般”。在豆瓣Top250里,评分和评价人数(可以近似看作“热度”或“知名度”)有什么关系呢?我们用散点图来看看。
def plot_rating_vs_comments(df):
"""
绘制评分与评价人数的散点图,观察相关性
"""
plt.figure(figsize=(10, 6))
# 为了散点图更清晰,我们对评价人数取对数处理,因为人数差异可能非常大(从几万到几百万)
plt.scatter(df['评分'], np.log10(df['评价人数']), alpha=0.6, c='green', edgecolors='w', s=50)
# 添加趋势线
z = np.polyfit(df['评分'], np.log10(df['评价人数']), 1)
p = np.poly1d(z)
plt.plot(df['评分'], p(df['评分']), "r--", linewidth=2, label='趋势线')
plt.title('电影评分 vs 评价人数(对数)', fontsize=14)
plt.xlabel('评分', fontsize=12)
plt.ylabel('评价人数(对数尺度)', fontsize=12)
plt.legend()
plt.grid(alpha=0.3)
plt.tight_layout()
plt.savefig('rating_vs_popularity.png', dpi=300, bbox_inches='tight')
plt.show()
# 计算相关系数
correlation = df['评分'].corr(np.log10(df['评价人数']))
print(f"评分与评价人数(对数)的相关系数为: {correlation:.3f}")
if correlation > 0:
print("趋势:总体上看,评分越高的电影,评价人数也倾向于更多(正相关)。")
这个图非常有意思。你会发现,虽然整体趋势是评分高的电影评价人数也多(右上角密集),但左下角也存在一些评分相对较低(但也在8.5以上)且评价人数极少的“冷门佳作”。同时,右上角那些评分和热度都极高的,就是真正的“经典中的经典”了。
6.3 国家/地区与电影类型分析:谁才是榜单霸主?
Top250里,哪个国家贡献了最多的经典电影?哪种类型的电影最受评委青睐?我们用条形图和饼图来揭示。
def plot_country_and_genre_analysis(df):
"""
分析电影的国家/地区和类型分布
"""
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
# 1. 国家/地区分布(取前10)
country_counts = df['国家/地区'].str.split(' ').explode().value_counts().head(10)
axes[0].barh(country_counts.index, country_counts.values, color='lightcoral')
axes[0].set_title('电影数量最多的国家/地区(Top10)', fontsize=14)
axes[0].set_xlabel('电影数量', fontsize=12)
for i, v in enumerate(country_counts.values):
axes[0].text(v + 0.5, i, str(v), va='center')
# 2. 电影类型分布(取前10)
# 一部电影可能有多个类型,用explode方法拆分
genre_counts = df['类型'].str.split(' ').explode().value_counts().head(10)
axes[1].pie(genre_counts.values, labels=genre_counts.index, autopct='%1.1f%%', startangle=90)
axes[1].set_title('电影类型分布(Top10)', fontsize=14)
plt.tight_layout()
plt.savefig('country_genre_analysis.png', dpi=300, bbox_inches='tight')
plt.show()
print("\n国家/地区分布洞察:")
print(f" 榜单中电影涉及的国家/地区数量: {df['国家/地区'].str.split(' ').explode().nunique()}")
print(f" 排名第一的国家是: {country_counts.index[0]},共有 {country_counts.values[0]} 部电影上榜。")
print("\n电影类型洞察:")
print(f" 所有电影共涉及 {df['类型'].str.split(' ').explode().nunique()} 种类型标签。")
print(f" 最常见的类型是: {genre_counts.index[0]},出现了 {genre_counts.values[0]} 次。")
从分析结果来看,美国电影在数量上通常占据显著优势,这反映了其全球电影工业的影响力。而在类型上,“剧情”片毫无疑问是绝对的主流,这与Top250偏向艺术性和思想深度的评选标准是吻合的。“爱情”、“喜剧”、“犯罪”、“动画”等类型也紧随其后,构成了榜单的多样性。
6.4 导演作品数量排行:谁是“霸榜”之王?
最后,我们看看哪些导演是这份榜单的“常客”,他们的多部作品都获得了认可。
def plot_director_analysis(df):
"""
分析导演上榜作品数量
"""
# 有些电影有多个导演,这里进行拆分并计数
director_series = df['导演'].str.split('/').explode().str.strip()
director_counts = director_series.value_counts().head(15) # 取前15位
plt.figure(figsize=(10, 8))
bars = plt.barh(director_counts.index, director_counts.values, color='mediumseagreen')
plt.title('导演上榜作品数量排行(Top15)', fontsize=16)
plt.xlabel('上榜电影数量', fontsize=12)
plt.gca().invert_yaxis() # 让最多的显示在最上面
# 在条形末端添加数量标签
for bar in bars:
width = bar.get_width()
plt.text(width + 0.1, bar.get_y() + bar.get_height()/2, f'{int(width)}', ha='left', va='center')
plt.tight_layout()
plt.savefig('director_ranking.png', dpi=300, bbox_inches='tight')
plt.show()
print("导演洞察:")
print(f" 共有 {director_series.nunique()} 位导演的作品上榜。")
print(f" 上榜作品最多的导演是: {director_counts.index[0]},共有 {director_counts.values[0]} 部作品。")
这个排行榜往往能揭示那些被影史铭记的大师,比如宫崎骏、克里斯托弗·诺兰、史蒂文·斯皮尔伯格等,他们常常有多部作品入选。这对于影迷来说,也是一个绝佳的“补片指南”。
7. 把分析整合成一个完整项目
到这里,爬取、清洗、分析、可视化的核心环节我们都完成了。在实际操作中,我习惯把这些功能封装在一个主函数里,并添加一些交互和错误处理,让整个脚本更友好、更健壮。
import numpy as np
def main():
"""主函数,串联整个流程"""
print("=" * 50)
print("豆瓣电影Top250数据爬取与分析系统")
print("=" * 50)
choice = input("是否重新爬取数据?(y/n, 直接回车使用现有数据): ").lower()
if choice == 'y':
print("开始爬取数据,请耐心等待...")
raw_data = crawl_all_movies()
df = clean_and_save_data(raw_data)
print("数据爬取与保存完成!")
else:
try:
# 尝试读取之前保存的Excel文件
df = pd.read_excel('douban_top250.xlsx')
print("已加载本地数据文件。")
except FileNotFoundError:
print("未找到本地数据文件,开始爬取新数据...")
raw_data = crawl_all_movies()
df = clean_and_save_data(raw_data)
print(f"\n数据概览:")
print(f" 总记录数: {len(df)}")
print(f" 数据列: {', '.join(df.columns)}")
print(f" 评分范围: {df['评分'].min():.1f} - {df['评分'].max():.1f}")
print(f" 评价人数范围: {df['评价人数'].min():,} - {df['评价人数'].max():,}")
# 开始可视化分析
input("\n按回车键开始生成评分分布图...")
plot_rating_distribution(df)
input("\n按回车键开始生成评分与热度关系图...")
plot_rating_vs_comments(df)
input("\n按回车键开始生成国家与类型分析图...")
plot_country_and_genre_analysis(df)
input("\n按回车键开始生成导演排行榜...")
plot_director_analysis(df)
print("\n" + "=" * 50)
print("所有分析已完成!图表已保存为PNG图片。")
print("你可以打开 'douban_top250.xlsx' 查看详细数据。")
print("=" * 50)
if __name__ == '__main__':
main()
运行这个 main() 函数,程序会引导你选择是重新爬取数据还是使用已有的数据文件,然后一步步生成所有图表。整个过程就像在和一个数据分析助手对话,非常直观。
写完这个项目,我最深的体会是,技术最大的魅力在于它能将繁琐重复的工作自动化,并赋予我们洞察世界的全新视角。爬虫和数据分析本身并不复杂,关键在于耐心和细心:耐心分析网页结构,细心处理数据中的各种边界情况。这个豆瓣Top250项目是一个绝佳的起点,它涉及的请求、解析、清洗、分析、可视化,是数据科学中最核心的流程。你可以举一反三,用类似的思路去分析其他你感兴趣的榜单,比如豆瓣读书Top250、音乐Top榜,甚至是电商网站的商品评论。当你掌握了这套方法,互联网上的公开数据,就成了一座等待你挖掘的宝库。
更多推荐
所有评论(0)