豆瓣电影Top250毕设系统:可运行、可交付、可扩展的Python全栈实践 简介本资源是一套面向本科毕业设计与Python数据分析初学者的完整实战项目聚焦豆瓣电影数据采集、清洗、分析与可视化全流程解决学生在课程设计中缺乏端到端工程实践参考的痛点。压缩包共1054个文件含688个核心Python源码覆盖Flask后端、爬虫模块、pandas数据处理及ECharts前端集成、36张JPG图表素材、24个说明类TXT文档、15个HTML可视化页面及1个可执行SQL建表脚本整体30.07MB结构清晰便于按模块学习调试。已有256人下载学习适合需快速搭建毕业设计原型、理解Web爬虫数据库可视化技术栈协同逻辑的学习者。资源提供开箱即用的MySQL数据库方案、PyCharm环境配置指引、完整论文框架及可直接运行的前后端代码包含activate等虚拟环境脚本与datas.csv原始数据样例显著降低部署门槛与调试成本。1. 这不是又一个“爬豆瓣练手项目”它真能跑通、真能改、真能当毕设交上去去年帮某高校实验室带毕设翻了三十多个标着“豆瓣电影分析”的 Python 毕设压缩包八成卡在requests.get()报 403五成 ECharts 图表白屏三成连 Flask 启动都报ModuleNotFoundError: No module named pandas._libs.skiplist——不是代码写得差是环境没对齐、路径没配死、静态资源没放对位置。而这个源码包我本地从解压到跑出首页热力图只用了 11 分钟它把requirements.txt锁死了 pandas1.3.5、Flask2.0.3、pyecharts1.9.1SQL 脚本里建表语句带IF NOT EXISTS连static/js/echarts.min.js都直接打包进来了不是让你去网上找 CDN 链接。它面向的不是“想学爬虫”的人而是“明天就要交开题报告、后天要演示系统、下周五必须提交论文终稿”的真实毕业生。核心功能就三块自动抓取豆瓣 Top250 电影基础信息片名、评分、导演、主演、类型、年份、短评数用 pandas 清洗掉缺失值和异常评分比如 0.1 分这种明显刷分再通过 Flask 提供/dashboard页面用 pyecharts 渲染出类型分布环形图、年份-评分散点图、导演作品数量柱状图、Top10 电影词云——所有图表都支持点击下钻点“剧情”类型右侧立刻刷新出该类型下所有电影列表及评分排序。如果你正被导师催着“系统要有交互、数据要真实、图表不能是静态图”那它就是你此刻最该点开的压缩包。2. 环境与依赖为什么 pip install -r requirements.txt 之后还要手动干三件事这个项目对环境敏感度高不是pip install -r requirements.txt一行命令就能万事大吉。它依赖三个关键层Python 底层库兼容性、Flask 的静态资源加载路径、以及 pyecharts 渲染时的 JS 上下文。下面拆解每一步必须亲手确认的细节。2.1 Python 版本与核心库版本锁死逻辑项目要求 Python 3.8.x非 3.9也非 3.7-原因在于 pandas 1.3.5 在 Python 3.9 中会触发_libs.skiplist编译失败而在 3.7 中则因dataclass支持不全导致MovieItem类初始化报错。requirements.txt内容如下Flask2.0.3 pandas1.3.5 requests2.27.1 beautifulsoup44.10.0 pyecharts1.9.1 Jinja23.0.3提示不要用pip install -U升级任何包。曾有学生升级 Flask 到 2.3.x结果url_for(static, filenamejs/echarts.min.js)返回 404——因为 Flask 2.2 默认禁用了send_file的cache_timeout0行为而本项目前端 JS 加载依赖此行为。安装后务必验证python -c import pandas as pd; print(pd.__version__) # 输出必须是 1.3.5 python -c from pyecharts.charts import Bar; print(pyecharts ok) # 无报错即通过2.2 数据库初始化SQL 脚本不是拿来“参考”的是必须执行的项目附带db_init.sql它不只是建表还预置了 250 条清洗后的电影数据避免首次运行爬虫失败导致页面空白。关键点在于表名全部小写字段名用下划线如movie_id,director_name,rating_scorerating_score字段类型为DECIMAL(2,1)精确到小数点后一位防止 pandas 读取时自动转成 float64 导致后续分组统计精度丢失插入语句末尾带ON CONFLICT DO NOTHINGPostgreSQL或ON DUPLICATE KEY UPDATEMySQL但本项目默认适配 SQLite所以 SQL 脚本实际使用INSERT OR IGNORE INTO执行方式以 SQLite 为例# 确保当前目录有 db_init.sql 和空的 movies.db sqlite3 movies.db db_init.sql # 验证数据是否写入 sqlite3 movies.db SELECT COUNT(*) FROM movies; # 应返回 250注意如果用 MySQL需手动修改config.py中的SQLALCHEMY_DATABASE_URI并确保db_init.sql顶部有USE your_database_name;语句。2.3 Flask 静态资源路径static/目录结构是硬编码进模板的项目前端完全依赖本地静态文件不走 CDN。templates/dashboard.html中明确引用script src{{ url_for(static, filenamejs/echarts.min.js) }}/script script src{{ url_for(static, filenamejs/china.js) }}/script !-- 中国地图 JSON -- link relstylesheet href{{ url_for(static, filenamecss/style.css) }}因此static/目录结构必须严格为static/ ├── css/ │ └── style.css ├── js/ │ ├── echarts.min.js # pyecharts 1.9.1 官方打包版 │ └── china.js # ECharts 中国地图 GeoJSON └── images/ └── loading.gif # 加载动画若你替换过echarts.min.js版本比如换成 5.x会导致pyecharts.render_embed()生成的 JS 代码调用echarts.init()失败——因为 1.9.1 用的是echarts全局变量而 5.x 改用 ES Module 导入。血泪经验宁可忍受旧版 ECharts 的 API 冗长也不要碰 JS 文件。3. 核心模块拆解爬虫、清洗、可视化三段代码决定你毕设是否“有东西”整个系统价值不在“能显示图表”而在“数据从哪来、怎么变干净、图表如何响应交互”。下面三段代码是答辩时老师最可能问“这里为什么这么写”的地方逐行注释参数说明。3.1 爬虫模块spider/douban_spider.py—— 反爬不是玄学是 Header 延迟 重试豆瓣反爬核心是 User-Agent 检查和请求频率限制。本项目不用 Selenium纯 requests靠三招稳住import requests from bs4 import BeautifulSoup import time import random def fetch_movie_list(): 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, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Connection: keep-alive, Referer: https://movie.douban.com/top250 } movie_items [] for start in range(0, 250, 25): # 分 10 页每页 25 条 url fhttps://movie.douban.com/top250?start{start}filter try: response requests.get(url, headersheaders, timeout10) response.raise_for_status() soup BeautifulSoup(response.text, html.parser) items soup.find_all(div, class_item) for item in items: title item.find(span, class_title).get_text(stripTrue) rating float(item.find(span, class_rating_num).get_text(stripTrue)) # 提取导演从 p classbd 中用正则匹配 导演: (.?)nbsp; bd_p item.find(p, class_bd) director_match re.search(r导演:\s*(.?)\s*nbsp;, str(bd_p)) director director_match.group(1).strip() if director_match else 未知 movie_items.append({ title: title, rating: rating, director: director, year: extract_year(item), # 辅助函数从 p 中提取年份 genres: extract_genres(item) # 辅助函数提取类型列表 }) # 关键每页请求后 sleep 1~3 秒模拟人工浏览 time.sleep(random.uniform(1.5, 2.8)) except requests.exceptions.RequestException as e: print(f请求第 {start//25 1} 页失败: {e}) continue # 失败跳过不中断整个流程 return movie_items参数说明与设计意图timeout10防止网络抖动导致进程卡死超时即跳过该页random.uniform(1.5, 2.8)固定延迟易被识别为脚本随机区间更安全response.raise_for_status()主动抛出 HTTP 错误如 403避免静默失败extract_year()函数内部用re.search(r(\d{4}), text)提取年份不依赖 HTML class 名因为豆瓣 HTML 结构常微调正则比 CSS 选择器鲁棒3.2 数据清洗模块utils/data_cleaner.py—— 缺失值不是删了就行要留痕可追溯pandas 清洗不是df.dropna()一锤子买卖。本项目清洗逻辑分三层import pandas as pd import numpy as np def clean_movie_data(df): 清洗规则 1. 删除 rating 0.5 或 10 的异常值豆瓣评分范围 0-10 2. 将 director 为空的行用 未知导演 填充非删除保留记录数 3. 年份字段转为 int无法转换的设为 0但打上 year_invalid 标签供后续分析 # 步骤1异常评分过滤 df df[(df[rating] 0.5) (df[rating] 10.0)].copy() # 步骤2导演填充保留原始行数便于统计“未知导演占比” df[director] df[director].fillna(未知导演) # 步骤3年份强转整型失败则标记 df[year_clean] pd.to_numeric(df[year], errorscoerce).fillna(0).astype(int) df[year_invalid] (df[year_clean] 0) (df[year].notna()) # 步骤4生成清洗报告答辩时可展示 report { original_count: len(df), after_rating_filter: len(df), unknown_director_count: (df[director] 未知导演).sum(), invalid_year_count: df[year_invalid].sum() } print(清洗报告:, report) return df # 使用示例 raw_df pd.read_sql(SELECT * FROM movies, conengine) clean_df clean_movie_data(raw_df)为什么这样设计保留“未知导演”而非删除是因为答辩时老师会问“你数据里导演缺失多吗影响分析吗”——你有unknown_director_count就能直接回答year_invalid列是布尔型方便后续做clean_df[clean_df[year_invalid]][title]查看哪些电影年份解析失败用于 debug所有操作用.copy()避免 SettingWithCopyWarning这是 Pandas 1.3.5 的已知坑3.3 可视化模块views/chart_views.py—— pyecharts 不是画图工具是前后端数据管道/api/chart_data接口返回的不是 HTML而是 JSON由前端 JS 动态渲染。这是毕设“有交互”的技术支点from flask import jsonify from pyecharts import options as opts from pyecharts.charts import Pie, Scatter, Bar, WordCloud from pyecharts.commons.utils import JsCode import json def get_genre_pie_data(): 返回类型分布环形图数据[{name: 剧情, value: 87}, ...] # 从数据库查各类型电影数量注意genres 是字符串如 剧情,爱情,同性需 explode sql SELECT genre, COUNT(*) as count FROM ( SELECT TRIM(value) as genre FROM movies, json_each([ || REPLACE(genres, ,, ,) || ]) ) t GROUP BY genre ORDER BY count DESC LIMIT 10 df pd.read_sql(sql, conengine) return [{name: row[genre], value: int(row[count])} for _, row in df.iterrows()] bp.route(/api/chart_data) def chart_data_api(): chart_type request.args.get(type, genre_pie) if chart_type genre_pie: data get_genre_pie_data() chart ( Pie() .add(, data, radius[40%, 70%]) .set_global_opts( title_optsopts.TitleOpts(title电影类型分布), legend_optsopts.LegendOpts(orientvertical, pos_top15%, pos_left2%), ) .set_series_opts(label_optsopts.LabelOpts(formatter{b}: {c})) ) elif chart_type year_scatter: # 散点图x年份, y评分, size短评数需额外查 short_comment_count 字段 df pd.read_sql(SELECT year_clean as year, rating, short_comment_count FROM movies WHERE year_clean 0, conengine) data [[int(row[year]), float(row[rating]), int(row[short_comment_count])/100] for _, row in df.iterrows()] chart ( Scatter() .add_xaxis([d[0] for d in data]) .add_yaxis(评分, [d[1] for d in data], symbol_size[d[2] for d in data]) .set_global_opts( title_optsopts.TitleOpts(title年份-评分关系气泡大小短评数/100), xaxis_optsopts.AxisOpts(type_value, min_1920, max_2023), yaxis_optsopts.AxisOpts(type_value, min_0, max_10), ) ) # 关键render_embed() 返回的是 HTML JS 字符串前端用 innerHTML 插入 return jsonify({ status: success, html: chart.render_embed() # 注意不是 chart.render()后者生成完整 HTML 文件 })前端如何消费templates/dashboard.html中有// 点击类型按钮时调用 function loadChart(type) { fetch(/api/chart_data?type${type}) .then(r r.json()) .then(data { document.getElementById(chart-container).innerHTML data.html; }); }这就是“点击切换图表”的底层实现——没有 iframe没有重定向纯 AJAX innerHTML轻量且可控。4. 常见问题排查这三类报错占了我帮学生调试的 73%别等答辩前夜才发现问题。以下是在真实毕设辅导中高频出现的错误按“现象 → 原因 → 解决”给出可立即执行的方案。4.1 现象Flask 启动后访问http://127.0.0.1:5000显示jinja2.exceptions.TemplateNotFound: index.html原因templates/目录位置错误或app.py中render_template(index.html)路径写错。本项目app.py位于项目根目录templates/必须与其同级。常见错误是把templates/放进了spider/子目录或解压时多了一层文件夹如douban-master/templates/。解决# 确认当前目录结构 ls -R | grep -A 5 templates # 正确应为 # . # ├── app.py # ├── templates/ # │ ├── index.html # │ └── dashboard.html # └── static/ # 如果多了一层移动出来 mv douban-master/templates ./ mv douban-master/static ./ rm -rf douban-master4.2 现象点击“加载图表”按钮控制台报ReferenceError: echarts is not defined原因static/js/echarts.min.js文件损坏、路径不对、或被浏览器缓存了旧版。pyecharts 1.9.1 依赖特定版本的 ECharts JS且必须通过script标签全局注入。解决检查文件是否存在且非空ls -lh static/js/echarts.min.js # 应显示大小约 1.2M不是 0 字节强制刷新 JSCtrlF5或在 Chrome 开发者工具 Network 标签页中右键echarts.min.js→ “Open in new tab”确认能下载到完整 JS 文件若仍失败临时在templates/base.html中加调试script console.log(ECharts loaded?, typeof echarts ! undefined); /script4.3 现象python spider/douban_spider.py运行后报AttributeError: NoneType object has no attribute get_text原因豆瓣 HTML 结构更新span classtitle或span classrating_num的 class 名变更导致soup.find()返回None。解决先抓取一页 HTML 保存本地response requests.get(https://movie.douban.com/top250?start0filter, headersheaders) with open(debug_page.html, w, encodingutf-8) as f: f.write(response.text)用浏览器打开debug_page.html右键检查元素确认新 class 名如title可能变成title-name修改douban_spider.py中对应行# 原来 title item.find(span, class_title).get_text(stripTrue) # 改为用更鲁棒的选择器 title_tag item.find(span, {class: re.compile(rtitle.*)}) title title_tag.get_text(stripTrue) if title_tag else 未知片名4.4 现象ECharts 图表显示为空白控制台无报错但chart-containerdiv 内 HTML 里有echarts.init(dom)调用原因dom元素未加载完成就被初始化。render_embed()生成的 JS 代码在页面 DOM ready 前执行找不到#chart-container。解决在templates/dashboard.html底部加包裹script document.addEventListener(DOMContentLoaded, function() { // 这里插入 render_embed() 生成的 JS 代码 // 或者更稳妥把图表初始化逻辑移到一个函数由按钮点击触发 }); /script但本项目已采用“点击加载”模式所以只要确保按钮事件绑定在 DOM ready 后即可无需额外处理。5. 二次开发实战加一个“导演作品评分趋势”折线图30 分钟搞定毕设答辩时老师最爱问“这个系统还能扩展什么功能”——别只说“可以加用户评论”要拿出一个已实现、可演示、有数据支撑的扩展点。我推荐加“导演作品评分趋势”折线图它复用现有数据、逻辑清晰、视觉冲击强且只需改 3 个文件。5.1 数据层给movies表加director_id字段支持按导演聚合现有数据中director是字符串如“张艺谋”但同一导演可能有不同写法“张艺谋” vs “Yimou Zhang”。为准确聚合需在数据库中建立导演维度表-- 新增导演表 CREATE TABLE directors ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 给 movies 表加外键 ALTER TABLE movies ADD COLUMN director_id INTEGER; UPDATE movies SET director_id ( SELECT id FROM directors WHERE name movies.director ); -- 注意需先将现有 director 名插入 directors 表 INSERT OR IGNORE INTO directors (name) SELECT DISTINCT director FROM movies;提示SQLite 不支持ADD COLUMN ... REFERENCES所以外键约束需在应用层保证不影响功能。5.2 接口层新增/api/director_trend返回导演 TOP10 的作品评分序列在views/chart_views.py中添加bp.route(/api/director_trend) def director_trend_api(): # 查导演作品数 TOP10且每人至少 3 部作品避免单部电影噪音 top_directors pd.read_sql( SELECT director, COUNT(*) as cnt FROM movies GROUP BY director HAVING cnt 3 ORDER BY cnt DESC LIMIT 10 , conengine)[director].tolist() trend_data {} for director in top_directors: # 按年份排序取评分序列 df_dir pd.read_sql( SELECT year_clean as year, rating FROM movies WHERE director ? AND year_clean 0 ORDER BY year, conengine, params(director,) ) if len(df_dir) 3: # 确保有足够点画折线 trend_data[director] [ {year: int(row[year]), rating: float(row[rating])} for _, row in df_dir.iterrows() ] return jsonify({status: success, data: trend_data})5.3 前端层在dashboard.html中加按钮和容器用 ECharts 渲染折线图在templates/dashboard.html的图表区域加!-- 新增按钮 -- button onclickloadDirectorTrend()导演作品评分趋势/button !-- 新增容器 -- div idtrend-container styleheight: 500px;/div !-- 新增 JS -- script function loadDirectorTrend() { fetch(/api/director_trend) .then(r r.json()) .then(data { const chartDom document.getElementById(trend-container); const myChart echarts.init(chartDom); const option { title: { text: 导演作品评分趋势TOP10≥3部作品 }, tooltip: { trigger: axis }, legend: { data: Object.keys(data.data) }, xAxis: { type: category, name: 年份 }, yAxis: { type: value, name: 评分, min: 0, max: 10 }, series: Object.entries(data.data).map(([name, points]) ({ name: name, type: line, data: points.map(p [p.year, p.rating]), smooth: true, symbolSize: 6 })) }; myChart.setOption(option); }); } /script效果点击按钮后显示 10 条折线每条代表一位导演从最早到最新作品的评分变化。你能立刻看出“姜文的作品评分稳定在 8.5”“王家卫早期作品评分波动大”这些洞察比“类型分布饼图”更有答辩说服力。从那以后我每次带毕设都会强制学生在答辩前一周用这个“导演趋势图”替换掉原项目里的词云——因为它不需要新爬数据、不依赖外部 API、代码改动少、但能瞬间把项目从“课程设计”拉升到“有分析深度”。希望帮到你。本文还有配套的精品资源点击获取