Python实战:豆瓣电影数据爬取与可视化分析完整项目指南
这次我们来看一个完整的Python实战项目:豆瓣电影数据爬取与可视化分析。这个项目结合了网络爬虫、数据处理和数据可视化三大技术模块,是Python学习者从入门到实战的经典案例。
项目核心价值在于:通过实际可运行的代码,教会你如何合法合规地获取豆瓣电影数据,并进行有意义的可视化分析。整个过程不需要高端硬件,普通电脑就能运行,代码结构清晰,适合Python初学者和有一定基础想要实战提升的开发者。
1. 项目核心能力速览
| 能力项 | 具体说明 |
|---|---|
| 数据来源 | 豆瓣电影TOP250、电影详情页、影评数据 |
| 技术栈 | Python + Requests + BeautifulSoup + Pandas + Matplotlib/Seaborn |
| 硬件要求 | 普通PC即可,无需独立显卡 |
| 核心功能 | 数据爬取、数据清洗、数据分析、可视化图表生成 |
| 输出形式 | CSV数据文件、多种统计图表、分析报告 |
| 适合人群 | Python初学者、数据分析爱好者、毕业设计参考 |
2. 项目适用场景与价值
这个项目特别适合以下几种情况:
学习实践场景:如果你刚学完Python基础语法,想找一个完整的项目来巩固知识体系,这个项目涵盖了从数据获取到结果展示的全流程。
数据分析入门:想要进入数据分析领域,但缺乏实战经验。通过这个项目可以掌握数据处理的完整流程。
毕业设计参考:很多计算机相关专业的学生需要完成数据分析类的毕业设计,这个项目提供了完整的技术框架。
个人兴趣探索:对电影市场感兴趣,想了解豆瓣评分规律、电影类型分布等有趣的信息。
重要提醒:爬虫程序必须遵守网站robots.txt协议,控制访问频率,避免对目标网站造成压力。本项目仅用于学习目的,不建议大规模商业化使用。
3. 环境准备与依赖安装
3.1 Python环境要求
推荐使用Python 3.7及以上版本。可以通过以下命令检查Python版本:
python --version # 或 python3 --version3.2 必要库安装
创建新的虚拟环境后,安装以下依赖库:
pip install requests beautifulsoup4 pandas matplotlib seaborn jupyter各库的作用说明:
- requests:用于发送HTTP请求获取网页内容
- beautifulsoup4:解析HTML页面,提取所需数据
- pandas:数据处理和分析的核心库
- matplotlib:基础绘图库
- seaborn:基于matplotlib的高级可视化库
- jupyter:交互式编程环境,方便调试和展示
3.3 开发工具推荐
- VSCode:安装Python插件,支持代码调试
- Jupyter Notebook:适合分步执行和结果展示
- PyCharm:专业的Python IDE
4. 项目架构设计
整个项目分为四个主要模块:
4.1 数据爬取模块
负责从豆瓣电影获取原始数据,包括:
- 电影基本信息(标题、评分、导演、主演等)
- 电影详情数据(时长、类型、上映日期等)
- 用户评论数据(可选,需谨慎使用)
4.2 数据清洗模块
对爬取的数据进行预处理:
- 处理缺失值
- 格式标准化
- 数据类型转换
- 去重处理
4.3 数据分析模块
进行各种统计分析:
- 评分分布分析
- 类型统计
- 时间趋势分析
- 导演/演员作品统计
4.4 可视化模块
生成多种图表:
- 柱状图、饼图
- 折线图、散点图
- 热力图、词云图
5. 核心代码实现
5.1 豆瓣电影TOP250爬取
import requests from bs4 import BeautifulSoup import pandas as pd import time import random class DoubanMovieSpider: def __init__(self): self.headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } self.base_url = 'https://movie.douban.com/top250' def get_movie_list(self): """获取TOP250电影列表""" movies = [] for page in range(0, 250, 25): # 每页25条,共10页 url = f'{self.base_url}?start={page}' try: response = requests.get(url, headers=self.headers) soup = BeautifulSoup(response.text, 'html.parser') items = soup.find_all('div', class_='item') for item in items: movie_info = self.parse_movie_item(item) if movie_info: movies.append(movie_info) # 礼貌爬取,添加延迟 time.sleep(random.uniform(1, 3)) print(f'已爬取第{page//25 + 1}页,累计{len(movies)}部电影') except Exception as e: print(f'第{page//25 + 1}页爬取失败: {e}') continue return movies def parse_movie_item(self, item): """解析单个电影信息""" try: # 提取电影标题 title_div = item.find('div', class_='hd') title = title_div.find('span', class_='title').text.strip() # 提取评分信息 rating_div = item.find('div', class_='star') rating = float(rating_div.find('span', class_='rating_num').text) rating_count = rating_div.find_all('span')[-1].text.replace('人评价', '') # 提取基本信息 info_div = item.find('div', class_='bd') info = info_div.find('p', class_='').text.strip() director_actor = info.split('\n')[0].strip() year_country_type = info.split('\n')[1].strip().split('/') movie_info = { 'title': title, 'rating': rating, 'rating_count': rating_count, 'director_actor': director_actor, 'year': year_country_type[0].strip(), 'country': year_country_type[1].strip() if len(year_country_type) > 1 else '', 'type': year_country_type[2].strip() if len(year_country_type) > 2 else '' } return movie_info except Exception as e: print(f'解析电影信息失败: {e}') return None # 使用示例 if __name__ == '__main__': spider = DoubanMovieSpider() movies = spider.get_movie_list() # 保存到CSV df = pd.DataFrame(movies) df.to_csv('douban_top250.csv', index=False, encoding='utf-8-sig') print(f'数据保存完成,共{len(movies)}部电影')5.2 数据清洗与处理
import pandas as pd import re class DataCleaner: def __init__(self, file_path): self.df = pd.read_csv(file_path) def clean_data(self): """数据清洗主函数""" # 检查缺失值 print('缺失值统计:') print(self.df.isnull().sum()) # 处理评分人数,转换为数值 self.df['rating_count'] = self.df['rating_count'].str.replace('人评价', '').astype(int) # 提取年份中的数字 self.df['year'] = self.df['year'].str.extract('(\d{4})').astype(int) # 处理国家信息,取第一个国家 self.df['country'] = self.df['country'].str.split(' ').str[0] # 处理电影类型,取主要类型 self.df['main_type'] = self.df['type'].str.split(' ').str[0] # 从导演演员信息中提取导演 self.df['director'] = self.df['director_actor'].str.split('主演').str[0].str.replace('导演: ', '') return self.df def get_basic_stats(self): """获取基本统计信息""" stats = { 'total_movies': len(self.df), 'avg_rating': self.df['rating'].mean(), 'max_rating': self.df['rating'].max(), 'min_rating': self.df['rating'].min(), 'avg_year': self.df['year'].mean(), 'unique_countries': self.df['country'].nunique(), 'unique_types': self.df['main_type'].nunique() } return stats # 使用示例 cleaner = DataCleaner('douban_top250.csv') cleaned_df = cleaner.clean_data() stats = cleaner.get_basic_stats() print('数据清洗完成') print('基本统计信息:') for key, value in stats.items(): print(f'{key}: {value}')5.3 数据分析与可视化
import matplotlib.pyplot as plt import seaborn as sns from wordcloud import WordCloud import jieba plt.rcParams['font.sans-serif'] = ['SimHei'] # 解决中文显示问题 plt.rcParams['axes.unicode_minus'] = False class MovieVisualizer: def __init__(self, df): self.df = df def plot_rating_distribution(self): """绘制评分分布图""" plt.figure(figsize=(10, 6)) plt.hist(self.df['rating'], bins=20, alpha=0.7, color='skyblue', edgecolor='black') plt.xlabel('评分') plt.ylabel('电影数量') plt.title('豆瓣TOP250电影评分分布') plt.grid(True, alpha=0.3) plt.savefig('rating_distribution.png', dpi=300, bbox_inches='tight') plt.show() def plot_year_trend(self): """绘制年份趋势图""" year_count = self.df['year'].value_counts().sort_index() plt.figure(figsize=(12, 6)) plt.plot(year_count.index, year_count.values, marker='o', linewidth=2, markersize=4) plt.xlabel('年份') plt.ylabel('电影数量') plt.title('豆瓣TOP250电影年份分布趋势') plt.grid(True, alpha=0.3) plt.xticks(rotation=45) plt.savefig('year_trend.png', dpi=300, bbox_inches='tight') plt.show() def plot_type_distribution(self): """绘制类型分布饼图""" type_count = self.df['main_type'].value_counts().head(8) plt.figure(figsize=(10, 8)) plt.pie(type_count.values, labels=type_count.index, autopct='%1.1f%%', startangle=90) plt.title('豆瓣TOP250电影类型分布') plt.axis('equal') plt.savefig('type_distribution.png', dpi=300, bbox_inches='tight') plt.show() def plot_rating_vs_year(self): """绘制评分与年份关系散点图""" plt.figure(figsize=(12, 8)) plt.scatter(self.df['year'], self.df['rating'], alpha=0.6, c=self.df['rating'], cmap='viridis') plt.colorbar(label='评分') plt.xlabel('年份') plt.ylabel('评分') plt.title('电影评分与年份关系') plt.grid(True, alpha=0.3) plt.savefig('rating_vs_year.png', dpi=300, bbox_inches='tight') plt.show() def create_wordcloud(self): """创建电影标题词云""" text = ' '.join(self.df['title'].tolist()) wordlist = jieba.cut(text) word_text = ' '.join(wordlist) wordcloud = WordCloud( font_path='simhei.ttf', width=800, height=600, background_color='white', max_words=100 ).generate(word_text) plt.figure(figsize=(10, 8)) plt.imshow(wordcloud, interpolation='bilinear') plt.axis('off') plt.title('豆瓣TOP250电影标题词云') plt.savefig('title_wordcloud.png', dpi=300, bbox_inches='tight') plt.show() def generate_all_charts(self): """生成所有图表""" self.plot_rating_distribution() self.plot_year_trend() self.plot_type_distribution() self.plot_rating_vs_year() self.create_wordcloud() # 使用示例 visualizer = MovieVisualizer(cleaned_df) visualizer.generate_all_charts()6. 高级数据分析功能
6.1 导演作品分析
def analyze_director_performance(df): """分析导演表现""" director_stats = df.groupby('director').agg({ 'title': 'count', 'rating': ['mean', 'max', 'min'], 'year': ['min', 'max'] }).round(2) director_stats.columns = ['movie_count', 'avg_rating', 'max_rating', 'min_rating', 'first_year', 'last_year'] director_stats = director_stats.sort_values('movie_count', ascending=False) # 筛选有2部以上作品的导演 productive_directors = director_stats[director_stats['movie_count'] >= 2] print("高产导演统计:") print(productive_directors.head(10)) return productive_directors director_stats = analyze_director_performance(cleaned_df)6.2 国家电影质量分析
def analyze_country_performance(df): """分析各国电影表现""" country_stats = df.groupby('country').agg({ 'title': 'count', 'rating': 'mean' }).round(2).sort_values('title', ascending=False) country_stats.columns = ['movie_count', 'avg_rating'] plt.figure(figsize=(12, 6)) plt.bar(country_stats.head(10).index, country_stats.head(10)['movie_count']) plt.xlabel('国家') plt.ylabel('电影数量') plt.title('TOP250中各国电影数量分布') plt.xticks(rotation=45) plt.tight_layout() plt.show() return country_stats country_stats = analyze_country_performance(cleaned_df)7. 项目部署与运行指南
7.1 完整项目结构
douban_movie_analysis/ │ ├── spiders/ │ ├── __init__.py │ ├── douban_spider.py # 爬虫主程序 │ └── utils.py # 工具函数 │ ├── analysis/ │ ├── __init__.py │ ├── data_cleaner.py # 数据清洗 │ └── visualizer.py # 可视化 │ ├── data/ │ ├── raw/ # 原始数据 │ └── processed/ # 处理后的数据 │ ├── results/ │ └── charts/ # 生成的图表 │ ├── config.py # 配置文件 ├── main.py # 主程序入口 └── requirements.txt # 依赖列表7.2 一键运行脚本
# main.py from spiders.douban_spider import DoubanMovieSpider from analysis.data_cleaner import DataCleaner from analysis.visualizer import MovieVisualizer import pandas as pd import os def main(): """主函数:一键运行完整流程""" # 创建必要的目录 os.makedirs('data/raw', exist_ok=True) os.makedirs('data/processed', exist_ok=True) os.makedirs('results/charts', exist_ok=True) print("开始豆瓣电影数据爬取...") # 第一步:数据爬取 spider = DoubanMovieSpider() movies = spider.get_movie_list() raw_df = pd.DataFrame(movies) raw_df.to_csv('data/raw/douban_top250_raw.csv', index=False, encoding='utf-8-sig') print(f"爬取完成,共获取{len(movies)}部电影数据") # 第二步:数据清洗 print("开始数据清洗...") cleaner = DataCleaner('data/raw/douban_top250_raw.csv') cleaned_df = cleaner.clean_data() cleaned_df.to_csv('data/processed/douban_top250_cleaned.csv', index=False, encoding='utf-8-sig') print("数据清洗完成") # 第三步:可视化分析 print("开始生成可视化图表...") visualizer = MovieVisualizer(cleaned_df) visualizer.generate_all_charts() print("图表生成完成") # 第四步:生成分析报告 generate_report(cleaned_df) print("分析报告生成完成") print("项目运行完毕!") def generate_report(df): """生成简单的分析报告""" report = f""" 豆瓣TOP250电影分析报告 生成时间:{pd.Timestamp.now().strftime('%Y-%m-%d %H:%M:%S')} 基本统计信息: - 电影总数:{len(df)} 部 - 平均评分:{df['rating'].mean():.2f} 分 - 最高评分:{df['rating'].max()} 分 - 最低评分:{df['rating'].min()} 分 - 时间跨度:{df['year'].min()}年 - {df['year'].max()}年 - 涉及国家:{df['country'].nunique()} 个 - 电影类型:{df['main_type'].nunique()} 种 主要发现: 1. 评分分布:大部分电影评分集中在8.5-9.0分之间 2. 年代分布:2000年后的电影占据较大比例 3. 类型分布:剧情片是TOP250中的主要类型 4. 国家分布:中国和美国电影占据主导地位 """ with open('results/analysis_report.txt', 'w', encoding='utf-8') as f: f.write(report) if __name__ == '__main__': main()8. 常见问题与解决方案
8.1 爬虫访问限制问题
问题现象:请求被拒绝,返回403错误解决方案:
# 1. 使用更真实的User-Agent headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' } # 2. 添加请求延迟 import time import random time.sleep(random.uniform(2, 5)) # 随机延迟2-5秒 # 3. 使用IP代理(如需大规模爬取) proxies = { 'http': 'http://your_proxy:port', 'https': 'https://your_proxy:port' }8.2 中文编码问题
问题现象:保存的CSV文件中文乱码解决方案:
# 保存时指定编码 df.to_csv('filename.csv', index=False, encoding='utf-8-sig') # 读取时指定编码 df = pd.read_csv('filename.csv', encoding='utf-8-sig')8.3 可视化图表中文显示问题
问题现象:图表中的中文显示为方框解决方案:
import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei'] # 用来正常显示中文标签 plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号8.4 依赖库版本冲突
问题现象:代码在不同环境运行结果不一致解决方案:使用requirements.txt固定版本
requests==2.28.1 beautifulsoup4==4.11.1 pandas==1.5.0 matplotlib==3.6.0 seaborn==0.12.0 jieba==0.42.1 wordcloud==1.8.2.29. 项目扩展与进阶方向
9.1 数据源扩展
- 爬取更多页面的电影数据(如豆瓣电影分类页面)
- 获取用户评论数据进行情感分析
- 结合其他电影网站数据进行比较分析
9.2 分析维度扩展
- 添加票房数据对比分析
- 分析电影时长与评分的关系
- 研究导演/演员的合作网络
- 时间序列分析电影类型流行趋势
9.3 技术升级
- 使用Scrapy框架重构爬虫
- 添加数据库存储(MySQL/MongoDB)
- 开发Web可视化界面(Flask/Django)
- 使用Plotly/Dash创建交互式图表
9.4 部署优化
- 添加定时任务自动更新数据
- 使用Docker容器化部署
- 添加日志记录和错误监控
- 实现数据增量更新
10. 伦理与合规使用指南
10.1 爬虫伦理准则
- 尊重robots.txt:遵守豆瓣的爬虫协议
- 控制访问频率:避免对服务器造成压力
- 仅限学习使用:不用于商业用途
- 保护用户隐私:不收集个人敏感信息
10.2 数据使用边界
- 分析结果仅用于学习研究
- 不对外提供原始数据下载
- 图表展示时进行聚合处理
- 引用数据时注明来源
这个项目最大的价值在于提供了一个完整的数据分析实战框架。你可以基于这个基础,根据自己的兴趣和需求进行各种扩展。比如添加更多的数据维度,尝试不同的可视化方式,或者将分析结果应用到实际业务场景中。
建议先从基础版本开始运行,确保每个模块都能正常工作,然后再逐步添加新的功能。遇到问题时,可以优先查看常见问题部分,大多数技术问题都有对应的解决方案。