豆瓣Top250爬虫与数据分析全链路实战
2026/9/11 1:52:20 网站建设 项目流程

简介:本资源是一套完整的豆瓣电影Top250数据采集、分析与可视化实战项目,面向计算机、数学及电子信息类专业的本科生与毕设学生,适用于课程设计、期末大作业及毕业设计参考。项目基于Python实现全流程:从Requests+BeautifulSoup爬取动态渲染数据,到Pandas清洗与统计分析,再通过Matplotlib/Seaborn绘图、WordCloud生成词云,并集成Flask构建轻量Web展示界面,结合ECharts实现交互式图表。压缩包含2000个文件,主体为1800个Python源码(含爬虫、分析、Web后端及前端逻辑)、70个说明类txt、22个JSON配置与数据文件、11个HTML/ECharts模板页,以及PDF项目文档和Markdown说明,总大小144.04MB,结构分层清晰,模块解耦明确。已有370人学习下载,提供开箱即用的完整工程、详细注释、可复现的数据处理链路及典型排错提示,助读者深入理解网络爬虫、数据分析与前后端协同开发实践。

1. 为什么爬豆瓣电影Top250不能只靠“requests+BeautifulSoup”硬刚?——一个真实落地的数据分析项目起点

你打开豆瓣电影Top250页面,右键“查看网页源码”,发现标题、评分、导演都明文在HTML里,心里一松:“Python爬虫?小菜一碟。”但真正动手时,第3页就卡住:返回403 Forbidden,headers加了User-Agent还是被拦截;换IP后又触发验证码;好不容易存下250条数据,却发现“主演”字段有的含括号备注、有的带换行、有的是“张艺谋 / 陈凯歌 / 冯小刚”这种斜杠分隔——后续清洗直接崩盘。这不是代码写得不对,而是没把豆瓣反爬机制、数据语义结构、分析目标三者对齐。本项目不是教你怎么“绕过封禁”,而是用合法、可持续、可复现的方式,完整走通「请求→解析→清洗→建模→可视化」全链路。适合已掌握基础Python语法、能写函数但没做过端到端数据分析的新手,也适合想快速验证某类爬虫+分析组合技的中级工程师。核心不在于“爬到”,而在于“爬得稳、理得清、看得懂”。

2. 用requests+fake_useragent+time.sleep构建抗干扰爬取层:绕过基础反爬的最小可行方案

豆瓣对高频、无头浏览器特征的请求会返回403或空响应。单纯伪造User-Agent已失效,必须叠加请求间隔、随机UA、Referer和Accept-Language等字段,模拟真实用户行为节奏。关键不是“伪装得像”,而是“行为节奏合理”。

2.1 安装依赖与初始化配置

pip install requests fake-useragent beautifulsoup4 pandas matplotlib seaborn openpyxl

提示:fake-useragent会自动从在线UA库获取最新列表,避免硬编码过期UA字符串。首次运行会下载JSON缓存,若网络受限可手动下载useragents.json放入~/.fake_useragent.json

2.2 构建带重试与随机延迟的请求会话

import requests from fake_useragent import UserAgent import time import random # 初始化全局UA池 ua = UserAgent() def create_session(): session = requests.Session() # 设置默认headers,覆盖requests默认值 session.headers.update({ 'User-Agent': ua.random, 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7', 'Accept-Encoding': 'gzip, deflate', 'Connection': 'keep-alive', 'Upgrade-Insecure-Requests': '1', 'Cache-Control': 'max-age=0', }) return session def fetch_page(session, url, max_retries=3): for attempt in range(max_retries): try: # 每次请求前随机休眠1.5~3.5秒,模拟人工翻页 time.sleep(random.uniform(1.5, 3.5)) response = session.get(url, timeout=10) response.raise_for_status() # 抛出4xx/5xx异常 if response.status_code == 200: return response.text except (requests.exceptions.RequestException, Exception) as e: print(f"请求失败(第{attempt+1}次): {url}, 错误: {e}") if attempt < max_retries - 1: time.sleep(2 ** attempt) # 指数退避 else: raise return None

这段代码的核心逻辑是:用Session复用连接减少开销,用fake-useragent动态轮换UA,用指数退避应对临时网络抖动,用随机sleep打破请求节拍规律。注意timeout=10防止挂起,response.raise_for_status()确保HTTP错误被捕捉。不要省略max_retries,豆瓣偶尔返回502,重试比人工干预更可靠。

2.3 解析HTML并提取结构化字段

豆瓣Top250每页25部电影,URL形如https://movie.douban.com/top250?start=0&filter=。需循环拼接start=0,25,50,...,225共10页。

from bs4 import BeautifulSoup import re def parse_movie_list(html): soup = BeautifulSoup(html, 'html.parser') movie_items = soup.find_all('div', class_='item') movies = [] for item in movie_items: try: # 片名(去除序号和空格) title_tag = item.find('span', class_='title') title = title_tag.get_text(strip=True) if title_tag else '' # 评分(转为float) rating_tag = item.find('span', class_='rating_num') rating = float(rating_tag.get_text(strip=True)) if rating_tag else 0.0 # 评价人数(提取数字,如"2042604人评价" → 2042604) votes_tag = item.find('div', class_='star').find_next_sibling('span', class_='rating_num') if votes_tag and '人评价' in votes_tag.get_text(): votes_text = votes_tag.get_text() votes = int(re.search(r'(\d+)', votes_text).group(1)) if re.search(r'(\d+)', votes_text) else 0 else: votes = 0 # 导演与主演(处理多行文本,合并为字符串) info_tag = item.find('div', class_='bd') if info_tag: p_text = info_tag.find('p', class_='').get_text(strip=True) if info_tag.find('p', class_='') else '' # 提取导演(中文名+冒号后内容,直到换行或斜杠) director_match = re.search(r'导演:\s*([^/\n]+)', p_text) director = director_match.group(1).strip() if director_match else '' # 提取主演(“主演:”后内容,截断到下一个换行) actors_match = re.search(r'主演:\s*([^/\n]+)', p_text) actors = actors_match.group(1).strip() if actors_match else '' else: director, actors = '', '' # 简介(短评,可能为空) quote_tag = item.find('span', class_='inq') quote = quote_tag.get_text(strip=True) if quote_tag else '' movies.append({ 'title': title, 'rating': rating, 'votes': votes, 'director': director, 'actors': actors, 'quote': quote }) except Exception as e: print(f"解析单条电影失败: {e}") continue return movies # 主爬取流程 session = create_session() all_movies = [] for start in range(0, 250, 25): url = f'https://movie.douban.com/top250?start={start}&filter=' print(f"正在抓取第{start//25 + 1}页...") html = fetch_page(session, url) if html: movies = parse_movie_list(html) all_movies.extend(movies) print(f"第{start//25 + 1}页抓取完成,新增{len(movies)}部") else: print(f"第{start//25 + 1}页抓取失败,跳过") print(f"总计抓取{len(all_movies)}部电影")
关键参数说明:
  • re.search(r'导演:\s*([^/\n]+)', p_text):正则匹配“导演:”后非斜杠非换行的连续字符,避免捕获到“编剧”或“类型”字段。
  • votes提取用re.search(r'(\d+)', ...)而非int(...)直接转换,因原始文本含逗号(如“1,234,567人评价”),正则先提纯数字再转int。
  • parse_movie_list中每个字段都加try/except,单条失败不影响整体,符合生产级鲁棒性要求。

3. 用pandas清洗与结构化:解决豆瓣数据特有的“人名分隔混乱”与“评分分布偏态”问题

爬取的原始数据存在三大典型脏数据:主演字段用“/”、“、”、“,”甚至空格分隔;导演字段含“(美)”“(韩)”等国籍标注;评分虽标称0-10,但Top250实际集中在8.0-9.5区间,直接画直方图会严重失真。清洗不是“删掉异常值”,而是按业务逻辑重建字段。

3.1 加载数据并识别脏字段模式

import pandas as pd import numpy as np df = pd.DataFrame(all_movies) print("原始数据形状:", df.shape) print("\n主演字段前5条示例:") print(df['actors'].head().tolist())

输出示例:

['张译 / 徐峥 / 王俊凯', '蒂莫西·柴勒梅德 / 丽莉·莱丝莉', '周冬雨 / 刘昊然 / 张国立', '黄渤 / 王宝强 / 徐峥', '张涵予 / 范伟 / 董勇']

可见分隔符不统一(/为主,但有空格、顿号混用),且部分人名含空格(如“丽莉·莱丝莉”),直接split('/')会错切。

3.2 标准化主演与导演字段:用正则统一分隔符并去重

# 清洗主演:统一用'/'分隔,去除多余空格,去重(同一人名在不同电影重复出现属正常,此处不去重) def clean_actors(text): if not isinstance(text, str) or not text.strip(): return [] # 替换所有分隔符为空格,再用空格分割,最后过滤空字符串 normalized = re.sub(r'[\/、,\s]+', ' ', text.strip()).strip() names = [name.strip() for name in normalized.split(' ') if name.strip()] return names # 清洗导演:移除国籍括号,如"张艺谋(中国大陆)" → "张艺谋" def clean_director(text): if not isinstance(text, str) or not text.strip(): return '' # 移除括号及内部内容(如(中国大陆)、(美)) cleaned = re.sub(r'\([^)]*\)', '', text).strip() return cleaned df['actors_list'] = df['actors'].apply(clean_actors) df['director_clean'] = df['director'].apply(clean_director) # 展开主演列表,生成“电影-演员”关系表(用于后续频次统计) actors_exploded = df.explode('actors_list') actors_exploded = actors_exploded[actors_exploded['actors_list'].notna()] actors_exploded = actors_exploded.rename(columns={'actors_list': 'actor'})
参数设计逻辑:
  • clean_actorsre.sub(r'[\/、,\s]+', ' ', ...)将所有分隔符统一替换为空格,再split(' '),比逐个replace更健壮,能处理/、,混合场景。
  • clean_director的正则r'\([^)]*\)'精准匹配最内层括号,避免误删电影名中的括号(如《阿凡达(重制版)》)。
  • explode是pandas 1.3+特性,将list列展开为多行,是构建关系型分析的基础操作。

3.3 处理评分与评价人数的分布偏态:引入对数变换与箱线图离群值标记

import matplotlib.pyplot as plt import seaborn as sns # 检查评分分布 plt.figure(figsize=(12, 4)) plt.subplot(1, 2, 1) sns.histplot(df['rating'], bins=20, kde=True) plt.title('原始评分分布(未处理)') plt.xlabel('评分') plt.subplot(1, 2, 2) # 对评价人数取log10,缓解长尾效应 df['votes_log10'] = np.log10(df['votes'] + 1) # +1避免log(0) sns.histplot(df['votes_log10'], bins=20, kde=True) plt.title('评价人数(log10变换后)') plt.xlabel('log10(评价人数)') plt.tight_layout() plt.show() # 用箱线图识别评分离群值(非删除,仅标记) Q1 = df['rating'].quantile(0.25) Q3 = df['rating'].quantile(0.75) IQR = Q3 - Q1 lower_bound = Q1 - 1.5 * IQR upper_bound = Q3 + 1.5 * IQR df['is_outlier_rating'] = ((df['rating'] < lower_bound) | (df['rating'] > upper_bound)) print(f"评分离群值数量: {df['is_outlier_rating'].sum()}(阈值: {lower_bound:.2f} ~ {upper_bound:.2f})")

注意:豆瓣Top250本身是人工筛选榜单,评分天然集中,此处离群值(如《肖申克的救赎》9.7 vs 《这个杀手不太冷》9.4)更多反映用户偏好差异,而非数据错误。清洗阶段保留全部数据,后续可视化时用颜色/大小编码区分即可。

4. 用matplotlib+seaborn实现四类核心可视化:从单变量分布到导演-演员合作网络

可视化不是“把数据画出来”,而是用图形语言回答具体问题:哪类导演更受高分青睐?主演阵容是否影响评价人数?哪些演员横跨最多Top250电影?本节提供可直接运行的代码,每张图对应一个明确分析目标。

4.1 评分与评价人数的双变量散点图:识别“高口碑低热度”与“高热度稳口碑”象限

plt.figure(figsize=(10, 6)) scatter = plt.scatter( df['votes_log10'], df['rating'], c=df['rating'], cmap='viridis', s=df['votes_log10']*20, # size映射log10人数,避免过大 alpha=0.7, edgecolors='w', linewidth=0.5 ) plt.colorbar(scatter, label='评分') plt.xlabel('log10(评价人数)') plt.ylabel('评分') plt.title('豆瓣Top250:评分 vs 热度(评价人数)') # 添加参考线:平均评分与平均热度 mean_rating = df['rating'].mean() mean_votes_log = df['votes_log10'].mean() plt.axhline(y=mean_rating, color='r', linestyle='--', alpha=0.6, label=f'平均评分: {mean_rating:.2f}') plt.axvline(x=mean_votes_log, color='b', linestyle='--', alpha=0.6, label=f'平均热度: {mean_votes_log:.2f}') plt.legend() # 标注四个象限代表作(取各象限top3) quadrant_movies = {} for idx, row in df.iterrows(): if row['votes_log10'] < mean_votes_log and row['rating'] > mean_rating: quadrant_movies.setdefault('高口碑低热度', []).append((row['title'], row['rating'], row['votes'])) elif row['votes_log10'] > mean_votes_log and row['rating'] > mean_rating: quadrant_movies.setdefault('高口碑高热度', []).append((row['title'], row['rating'], row['votes'])) elif row['votes_log10'] < mean_votes_log and row['rating'] < mean_rating: quadrant_movies.setdefault('低口碑低热度', []).append((row['title'], row['rating'], row['votes'])) else: quadrant_movies.setdefault('低口碑高热度', []).append((row['title'], row['rating'], row['votes'])) # 在图上标注每个象限1部代表作 for quad, movies in quadrant_movies.items(): if movies: top_movie = sorted(movies, key=lambda x: x[1], reverse=True)[0] # 按评分排序 plt.annotate(top_movie[0], xy=(np.log10(top_movie[2]+1), top_movie[1]), xytext=(5, 5), textcoords='offset points', fontsize=9, bbox=dict(boxstyle='round,pad=0.3', fc='yellow', alpha=0.7)) plt.grid(True, alpha=0.3) plt.show()
图表解读逻辑:
  • X轴用log10(评价人数)而非原始值,使《肖申克的救赎》(200万+)与《小城之春》(2万+)在图上距离合理。
  • 点大小s=df['votes_log10']*20,让高热度电影视觉权重更高,但避免遮盖小点。
  • 四象限标注选取“各象限内评分最高者”,直接回答“哪个象限有最强代表作”。

4.2 导演作品评分箱线图:比较华语导演与国际导演的口碑稳定性

# 提取导演国籍标签(简化版:含“中国”“大陆”“香港”“台湾”为华语,其余为国际) df['director_region'] = df['director_clean'].apply( lambda x: '华语' if any(kw in x for kw in ['中国', '大陆', '香港', '台湾']) else '国际' ) plt.figure(figsize=(10, 6)) sns.boxplot(data=df, x='director_region', y='rating', palette='Set2') plt.title('华语导演 vs 国际导演:作品评分分布对比') plt.xlabel('导演地区') plt.ylabel('评分') plt.grid(True, alpha=0.3) # 添加均值点 for region in df['director_region'].unique(): region_data = df[df['director_region'] == region] plt.plot([], [], ' ', label=f'{region}均值: {region_data["rating"].mean():.2f}') plt.legend() # 显示统计摘要 print("导演地区评分统计:") print(df.groupby('director_region')['rating'].agg(['count', 'mean', 'std', 'min', 'max']).round(3)) plt.show()
关键处理点:
  • 国籍判断用any(kw in x for kw in [...])而非正则,因导演名中“中国”可能出现在人名里(如“中国张艺谋”不存在,但“张艺谋(中国)”已清洗),此处仅依赖清洗后的director_clean字段。
  • boxplot自动显示中位数、四分位距、离群值,比柱状图更能体现分布稳定性。

4.3 演员合作网络图:用networkx绘制Top10高频演员的合作关系

import networkx as nx # 统计演员出现频次 actor_freq = actors_exploded['actor'].value_counts().head(10) top_actors = set(actor_freq.index) # 构建电影-演员关联矩阵 movie_actor_matrix = actors_exploded[actors_exploded['actor'].isin(top_actors)] # 每部电影的主演列表去重后两两组合,形成合作边 edges = [] for _, group in movie_actor_matrix.groupby('title'): actors_in_movie = list(set(group['actor'])) # 去重,避免同一电影内重复计算 if len(actors_in_movie) >= 2: from itertools import combinations edges.extend(combinations(actors_in_movie, 2)) # 构建图 G = nx.Graph() G.add_edges_from(edges) plt.figure(figsize=(12, 8)) pos = nx.spring_layout(G, seed=42, k=3) # k控制节点间距 nx.draw_networkx_nodes(G, pos, node_size=[actor_freq.get(node, 1)*300 for node in G.nodes()], node_color='lightblue', alpha=0.8) nx.draw_networkx_edges(G, pos, width=1.5, edge_color='gray', alpha=0.6) nx.draw_networkx_labels(G, pos, font_size=10, font_weight='bold') plt.title('Top10高频演员合作网络(基于共同出演Top250电影)', fontsize=14) plt.axis('off') plt.show() # 输出合作频次最高的3对 edge_counts = pd.Series(edges).value_counts().head(3) print("\n合作最频繁的演员对:") for (a, b), count in edge_counts.items(): print(f"{a} & {b}: {count}次")
网络图设计要点:
  • node_size映射演员出现频次,直观体现核心人物。
  • spring_layoutk=3参数增大节点排斥力,避免密集重叠。
  • 合作边仅统计“同一部Top250电影中共同出演”,不包含时间序列或导演关联,聚焦纯粹共演关系。

5. 用openpyxl导出交互式Excel报告:一键生成含图表、数据透视与条件格式的本地分析包

最终交付物不是Jupyter Notebook,而是可发给同事、无需Python环境即可查看的Excel文件。openpyxl支持嵌入图表、设置单元格样式、创建数据透视表,比pandas.to_excel更可控。

5.1 创建多Sheet工作簿:数据源、统计摘要、导演分析、演员分析

from openpyxl import Workbook from openpyxl.styles import Font, PatternFill, Alignment, Border, Side from openpyxl.chart import BarChart, Reference, Series from openpyxl.utils import get_column_letter wb = Workbook() ws_data = wb.active ws_data.title = "原始数据" # 写入表头 headers = ['序号', '片名', '评分', '评价人数', '导演', '主演', '短评'] for col, header in enumerate(headers, 1): cell = ws_data.cell(row=1, column=col, value=header) cell.font = Font(bold=True, color="FFFFFF") cell.fill = PatternFill(start_color="4F81BD", end_color="4F81BD", fill_type="solid") cell.alignment = Alignment(horizontal="center") # 写入数据(清洗后字段) for idx, (_, row) in enumerate(df.iterrows(), 2): ws_data.cell(row=idx, column=1, value=idx-1) ws_data.cell(row=idx, column=2, value=row['title']) ws_data.cell(row=idx, column=3, value=row['rating']) ws_data.cell(row=idx, column=4, value=row['votes']) ws_data.cell(row=idx, column=5, value=row['director_clean']) ws_data.cell(row=idx, column=6, value=' / '.join(row['actors_list']) if row['actors_list'] else '') ws_data.cell(row=idx, column=7, value=row['quote']) # 自动调整列宽 for col in range(1, len(headers)+1): ws_data.column_dimensions[get_column_letter(col)].width = 18

5.2 在“统计摘要”Sheet添加动态图表与条件格式

ws_summary = wb.create_sheet(title="统计摘要") # 写入汇总指标 summary_data = [ ["总电影数", len(df)], ["平均评分", f"{df['rating'].mean():.2f}"], ["最高评分", f"{df['rating'].max():.1f}"], ["最低评分", f"{df['rating'].min():.1f}"], ["平均评价人数", f"{df['votes'].mean():,.0f}"], ["华语导演作品数", len(df[df['director_region']=='华语'])], ["国际导演作品数", len(df[df['director_region']=='国际'])], ] for r_idx, row_data in enumerate(summary_data, 1): for c_idx, value in enumerate(row_data, 1): cell = ws_summary.cell(row=r_idx, column=c_idx, value=value) if c_idx == 1: # 指标名称列 cell.font = Font(bold=True) else: # 数值列 cell.alignment = Alignment(horizontal="right") # 添加评分分布直方图 chart = BarChart() chart.title = "评分分布(每0.1分区间)" chart.x_axis.title = "评分区间" chart.y_axis.title = "电影数量" # 构建数据引用(用openpyxl的Reference) data_range = Reference(ws_data, min_col=3, max_col=3, min_row=2, max_row=len(df)+1) chart.add_data(data_range, titles_from_data=False) # 设置X轴为分类轴(需手动定义区间) cats = [f"{i/10:.1f}-{(i+1)/10:.1f}" for i in range(70, 96)] # 7.0~9.5 chart.set_categories(Reference(ws_summary, min_col=1, min_row=1, max_row=len(cats))) ws_summary.add_chart(chart, "A10") # 对“原始数据”Sheet的评分列添加条件格式(绿色渐变) from openpyxl.formatting.rule import ColorScaleRule rule = ColorScaleRule( start_type='num', start_value=7.0, start_color='FF6384', mid_type='num', mid_value=8.5, mid_color='FFCE79', end_type='num', end_value=9.7, end_color='FF6B6B' ) ws_data.conditional_formatting.add(f'C2:C{len(df)+1}', rule) wb.save("豆瓣电影Top250分析报告.xlsx") print("Excel报告已生成:豆瓣电影Top250分析报告.xlsx")
Excel导出关键技巧:
  • ColorScaleRule为评分列添加红-黄-绿渐变色,一眼识别高低分。
  • BarChartset_categories必须显式指定X轴标签,openpyxl不自动推断数值列的区间。
  • ws_data.column_dimensions[...].width = 18统一列宽,避免中文换行混乱。

提示:此Excel文件可在Windows/Mac/Linux的Excel或WPS中直接打开,图表随数据更新,条件格式实时生效,完全脱离Python环境。若需进一步自动化,可封装为命令行工具:python douban_analyzer.py --output excel

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询