ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

豆瓣Top250爬虫与数据分析全链路实战

豆瓣Top250爬虫与数据分析全链路实战 简介本资源是一套完整的豆瓣电影Top250数据采集、分析与可视化实战项目面向计算机、数学及电子信息类专业的本科生与毕设学生适用于课程设计、期末大作业及毕业设计参考。项目基于Python实现全流程从RequestsBeautifulSoup爬取动态渲染数据到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不能只靠“requestsBeautifulSoup”硬刚——一个真实落地的数据分析项目起点你打开豆瓣电影Top250页面右键“查看网页源码”发现标题、评分、导演都明文在HTML里心里一松“Python爬虫小菜一碟。”但真正动手时第3页就卡住返回403 Forbiddenheaders加了User-Agent还是被拦截换IP后又触发验证码好不容易存下250条数据却发现“主演”字段有的含括号备注、有的带换行、有的是“张艺谋 / 陈凯歌 / 冯小刚”这种斜杠分隔——后续清洗直接崩盘。这不是代码写得不对而是没把豆瓣反爬机制、数据语义结构、分析目标三者对齐。本项目不是教你怎么“绕过封禁”而是用合法、可持续、可复现的方式完整走通「请求→解析→清洗→建模→可视化」全链路。适合已掌握基础Python语法、能写函数但没做过端到端数据分析的新手也适合想快速验证某类爬虫分析组合技的中级工程师。核心不在于“爬到”而在于“爬得稳、理得清、看得懂”。2. 用requestsfake_useragenttime.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/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en-US;q0.8,en;q0.7, Accept-Encoding: gzip, deflate, Connection: keep-alive, Upgrade-Insecure-Requests: 1, Cache-Control: max-age0, }) return session def fetch_page(session, url, max_retries3): for attempt in range(max_retries): try: # 每次请求前随机休眠1.5~3.5秒模拟人工翻页 time.sleep(random.uniform(1.5, 3.5)) response session.get(url, timeout10) response.raise_for_status() # 抛出4xx/5xx异常 if response.status_code 200: return response.text except (requests.exceptions.RequestException, Exception) as e: print(f请求失败第{attempt1}次: {url}, 错误: {e}) if attempt max_retries - 1: time.sleep(2 ** attempt) # 指数退避 else: raise return None这段代码的核心逻辑是用Session复用连接减少开销用fake-useragent动态轮换UA用指数退避应对临时网络抖动用随机sleep打破请求节拍规律。注意timeout10防止挂起response.raise_for_status()确保HTTP错误被捕捉。不要省略max_retries豆瓣偶尔返回502重试比人工干预更可靠。2.3 解析HTML并提取结构化字段豆瓣Top250每页25部电影URL形如https://movie.douban.com/top250?start0filter。需循环拼接start0,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(stripTrue) if title_tag else # 评分转为float rating_tag item.find(span, class_rating_num) rating float(rating_tag.get_text(stripTrue)) 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(stripTrue) 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(stripTrue) 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 fhttps://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_actors用re.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], bins20, kdeTrue) 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], bins20, kdeTrue) 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. 用matplotlibseaborn实现四类核心可视化从单变量分布到导演-演员合作网络可视化不是“把数据画出来”而是用图形语言回答具体问题哪类导演更受高分青睐主演阵容是否影响评价人数哪些演员横跨最多Top250电影本节提供可直接运行的代码每张图对应一个明确分析目标。4.1 评分与评价人数的双变量散点图识别“高口碑低热度”与“高热度稳口碑”象限plt.figure(figsize(10, 6)) scatter plt.scatter( df[votes_log10], df[rating], cdf[rating], cmapviridis, sdf[votes_log10]*20, # size映射log10人数避免过大 alpha0.7, edgecolorsw, linewidth0.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(ymean_rating, colorr, linestyle--, alpha0.6, labelf平均评分: {mean_rating:.2f}) plt.axvline(xmean_votes_log, colorb, linestyle--, alpha0.6, labelf平均热度: {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, keylambda x: x[1], reverseTrue)[0] # 按评分排序 plt.annotate(top_movie[0], xy(np.log10(top_movie[2]1), top_movie[1]), xytext(5, 5), textcoordsoffset points, fontsize9, bboxdict(boxstyleround,pad0.3, fcyellow, alpha0.7)) plt.grid(True, alpha0.3) plt.show()图表解读逻辑X轴用log10(评价人数)而非原始值使《肖申克的救赎》200万与《小城之春》2万在图上距离合理。点大小sdf[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(datadf, xdirector_region, yrating, paletteSet2) plt.title(华语导演 vs 国际导演作品评分分布对比) plt.xlabel(导演地区) plt.ylabel(评分) plt.grid(True, alpha0.3) # 添加均值点 for region in df[director_region].unique(): region_data df[df[director_region] region] plt.plot([], [], , labelf{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, seed42, k3) # k控制节点间距 nx.draw_networkx_nodes(G, pos, node_size[actor_freq.get(node, 1)*300 for node in G.nodes()], node_colorlightblue, alpha0.8) nx.draw_networkx_edges(G, pos, width1.5, edge_colorgray, alpha0.6) nx.draw_networkx_labels(G, pos, font_size10, font_weightbold) plt.title(Top10高频演员合作网络基于共同出演Top250电影, fontsize14) 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_layout的k3参数增大节点排斥力避免密集重叠。合作边仅统计“同一部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(row1, columncol, valueheader) cell.font Font(boldTrue, colorFFFFFF) cell.fill PatternFill(start_color4F81BD, end_color4F81BD, fill_typesolid) cell.alignment Alignment(horizontalcenter) # 写入数据清洗后字段 for idx, (_, row) in enumerate(df.iterrows(), 2): ws_data.cell(rowidx, column1, valueidx-1) ws_data.cell(rowidx, column2, valuerow[title]) ws_data.cell(rowidx, column3, valuerow[rating]) ws_data.cell(rowidx, column4, valuerow[votes]) ws_data.cell(rowidx, column5, valuerow[director_clean]) ws_data.cell(rowidx, column6, value / .join(row[actors_list]) if row[actors_list] else ) ws_data.cell(rowidx, column7, valuerow[quote]) # 自动调整列宽 for col in range(1, len(headers)1): ws_data.column_dimensions[get_column_letter(col)].width 185.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(rowr_idx, columnc_idx, valuevalue) if c_idx 1: # 指标名称列 cell.font Font(boldTrue) else: # 数值列 cell.alignment Alignment(horizontalright) # 添加评分分布直方图 chart BarChart() chart.title 评分分布每0.1分区间 chart.x_axis.title 评分区间 chart.y_axis.title 电影数量 # 构建数据引用用openpyxl的Reference data_range Reference(ws_data, min_col3, max_col3, min_row2, max_rowlen(df)1) chart.add_data(data_range, titles_from_dataFalse) # 设置X轴为分类轴需手动定义区间 cats [f{i/10:.1f}-{(i1)/10:.1f} for i in range(70, 96)] # 7.0~9.5 chart.set_categories(Reference(ws_summary, min_col1, min_row1, max_rowlen(cats))) ws_summary.add_chart(chart, A10) # 对“原始数据”Sheet的评分列添加条件格式绿色渐变 from openpyxl.formatting.rule import ColorScaleRule rule ColorScaleRule( start_typenum, start_value7.0, start_colorFF6384, mid_typenum, mid_value8.5, mid_colorFFCE79, end_typenum, end_value9.7, end_colorFF6B6B ) ws_data.conditional_formatting.add(fC2:C{len(df)1}, rule) wb.save(豆瓣电影Top250分析报告.xlsx) print(Excel报告已生成豆瓣电影Top250分析报告.xlsx)Excel导出关键技巧ColorScaleRule为评分列添加红-黄-绿渐变色一眼识别高低分。BarChart的set_categories必须显式指定X轴标签openpyxl不自动推断数值列的区间。ws_data.column_dimensions[...].width 18统一列宽避免中文换行混乱。提示此Excel文件可在Windows/Mac/Linux的Excel或WPS中直接打开图表随数据更新条件格式实时生效完全脱离Python环境。若需进一步自动化可封装为命令行工具python douban_analyzer.py --output excel。本文还有配套的精品资源点击获取
返回列表