简介这套基于Python的豆瓣电影数据分析及可视化系统是一份完整的毕业设计项目主要面向计算机相关专业的学生以及希望系统学习爬虫和可视化开发的技术人员。系统以Flask作为后端Web框架使用爬虫从豆瓣电影页面抓取评分、类型、导演等数据再借助pandas进行清洗与统计分析最终采用ECharts绘制交互式图表帮助用户直观理解电影市场的整体趋势和分布规律。资源包共包含1054个文件其中Python源码是核心还有编译生成的pyc文件、用于前端展示的html/js/css文件、数据库sql脚本、以及完整的论文文档另外也包含exe可执行文件等辅助内容压缩包大小约30MB文件目录划分清晰便于按源码、页面、数据库、文档等模块分别查阅。目前该项目已有255人学习下载。这套资料除了可以直接运行演示外还提供了详细的设计文档和数据库脚本既能作为毕业设计或课程设计的完整方案参考也能从中学习从网络爬虫、数据处理到可视化呈现的全流程开发技巧。1. 豆瓣电影数据可视化一套能写进简历的Python毕业设计长什么样每年毕业季都会看到大批“豆瓣电影爬虫”题目但多数人做到爬完 Top250、存进 CSV 就停了。真正能上答辩桌的版本是爬虫、pandas 清洗、SQL 存储、Flask 接口、echarts 图表串成一条完整链路评委追问任何一个环节你都能讲出取舍。这个标题之所以值得做是因为它把 Python 生态里最常用的五件套——requests、pandas、SQL、Flask、echarts——全部覆盖数据量不大但五脏俱全改一改就能变成图书、音乐、招聘等其他领域分析项目。适合正在选题的本科生也适合想拿一个完整项目充实简历、但不想卷算法的初学者。下面按我实际搭建这套系统的顺序把设计思路、可复制代码和踩过的坑一次讲清。2. 技术选型与数据流为什么是 Flask pandas echarts数据怎么流动2.1 选型逻辑Flask 定后端echarts 定前端pandas 居中处理数据常见做法是 Flask 提供接口、pandas 做数据处理、echarts 做浏览器端图表SQL 脚本负责把爬虫结果固化到数据库。先解释为什么是这套组合而不是 Django matplotlib。Flask 是微框架一个 app.py 就能把路由、接口、静态页全部托管毕业设计粒度正好Django 自带 ORM、Admin、迁移工具功能强但对这个体量来说太重答辩时被追问“为什么不用 Django”也可以坦然回答“系统定位是轻量数据展示Flask 的请求生命周期短、模板与接口分离更直观”。echarts 与 matplotlib 的选择更关键matplotlib 输出静态图片没法交互评委点一下图想看详细数值做不到echarts 是纯前端渲染、自带 tooltip、dataZoom、地图等组件展示效果天然超出预期。pandas 在这里不是展示工具而是清洗和聚合的中枢。爬虫抓到的是嵌套、缺失、类型混乱的原始记录不能直接塞进 echarts先用 pandas 去重、补缺失、转换日期与评分数值类型再把聚合结果写入 SQLite 或 MySQLFlask 从数据库读出来转 JSON。整条链路里 pandas 的位置在“爬虫之后、数据库之前”这也是为什么标题把 pandas 排在 echarts 和 flask 中间——它确实是承上启下的那个角色。2.2 项目目录与数据流架构一条命令从爬虫跑到可视化大屏我习惯把系统拆成五层采集层、清洗层、存储层、接口层、展示层。下面是推荐的项目布局这个结构可以直接照搬douban_movie/ ├── app.py # Flask 入口注册路由与静态页 ├── config.py # 数据库路径、请求头、flask 端口等配置 ├── spider/ │ ├── douban_spider.py # requests 爬虫输出原始 CSV │ └── run_spider.py # 调度入口带断点续爬 ├── data_process/ │ ├── cleaner.py # pandas 清洗去重、补缺、类型转换 │ └── stats.py # 聚合统计生成图表需要的中间数据 ├── sql/ │ ├── movie.sql # 建表脚本与基础统计视图 │ └── init_db.py # 把清洗结果导入数据库 ├── templates/ │ └── index.html # 可视化大屏页面引入 echarts └── static/ ├── css/ └── js/数据流的走向是这样的爬虫把豆瓣列表页解析成 DataFrame落盘成 raw_movies.csvcleaner.py 读 CSV清洗后交给 init_db.py 执行 movie.sql 里的建表语句并插入数据Flask 的 app.py 中每个路由对应一个统计查询SQL 结果转成 Python 列表再 jsonify 返回前端 index.html 用 fetch 请求 /api/movie/top_rated 这类接口拿到数据后直接塞进 echarts 的 option.series。这个分层的好处是每一层都能单独调试。爬虫挂了不影响已经落库的数据清洗逻辑错了只需重跑 cleaner.py前端调接口时后端改完重启 Flask 就能看到效果。答辩时按“数据从哪来、怎么变干净、存到哪里、怎么展示”这条线讲逻辑非常顺。3. 爬虫与数据清洗把豆瓣电影数据变成 pandas 认识的样子3.1 用 requests 抓取豆瓣 Top250请求头与限速是活下来的关键豆瓣列表页结构稳定Top250 是经典入门目标每页 25 条翻页参数是 start0、25、50…… 一直到 225。抓取时最核心的不是解析而是让请求看起来像真实用户。下面这段爬虫我一直在用短小但够稳import time import random import requests import pandas as pd from bs4 import BeautifulSoup HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36, Accept-Language: zh-CN,zh;q0.9, Referer: https://movie.douban.com/top250, } ALL_MOVIES [] for start in range(0, 250, 25): url fhttps://movie.douban.com/top250?start{start}filter resp requests.get(url, headersHEADERS, timeout10) if resp.status_code ! 200: print(f[跳过] start{start}, 状态码{resp.status_code}) time.sleep(30) continue soup BeautifulSoup(resp.text, html.parser) for item in soup.select(ol.grid_view li): title_tag item.select_one(span.title) rating_tag item.select_one(span.rating_num) quote_tag item.select_one(span.inq) if not title_tag or not rating_tag: continue ALL_MOVIES.append({ title: title_tag.get_text(), rating: float(rating_tag.get_text()), quote: quote_tag.get_text() if quote_tag else , page: start // 25 1, }) print(f[完成] start{start}, 累计 {len(ALL_MOVIES)} 条) time.sleep(random.uniform(1.5, 3.0)) df pd.DataFrame(ALL_MOVIES) df.to_csv(raw_movies.csv, indexFalse, encodingutf-8-sig)逻辑说明循环里通过 start 参数控制翻页每次请求后 time.sleep(random.uniform(1.5, 3.0)) 制造随机间隔避免固定频率请求触发频率限制。状态码不是 200 时直接等 30 秒重试而不是立刻重试给服务端一个“冷静期”。解析时用 BeautifulSoup 的 select 直接定位 ol.grid_view 下的 li 元素再分别取标题、评分、一句话短评quote 可能为空所以用条件表达式兜底。参数说明里最值得注意的是 HEADERS 中的 User-Agent 和 Referer。很多爬虫失败不是被封锁而是缺少 Referer——豆瓣部分接口会校验来源页面。timeout10 也要写否则某个请求卡住会让整个爬虫停在原地。这里用 utf-8-sig 保存 CSV 是刻意的后面清洗阶段再讲原因。3.2 pandas 清洗规则缺失、重复、类型转换哪个都不能跳过爬虫直出的 CSV 看着能用但直接导入数据库或送进 echarts 会在三个地方翻车重复记录、空值、类型隐患。清洗脚本 clea​​ner.py 做的事就是把这三件事摁死import pandas as pd df pd.read_csv(raw_movies.csv, encodingutf-8-sig) # 1. 去重同一部电影因为翻页叠加可能被抓两次 df df.drop_duplicates(subset[title], keepfirst) # 2. 缺失处理quote 为空时填充占位rating 为空时丢弃 df[quote] df[quote].fillna(暂无短评) df df.dropna(subset[rating]) # 3. 类型转换rating 确保是浮点数title 统一去掉空白 df[rating] df[rating].astype(float) df[title] df[title].str.strip() # 4. 新特征按评分区间打标签方便后面做饼图 df[rating_level] pd.cut( df[rating], bins[0, 7, 8.5, 10], labels[一般, 推荐, 高分], ) print(df.info()) df.to_csv(cleaned_movies.csv, indexFalse, encodingutf-8-sig)逻辑说明drop_duplicates 的 subset[title] 是因为豆瓣电影名不会重复用标题做唯一键最可靠keepfirst 保留第一次抓到的记录。fillna 处理短评空值避免可视化时出现 NaN 字样。rating 若为空直接丢弃因为它是后续所有图表的数值基础不能用捏造数据补。这里要专门讲 pd.cut 的作用。它的 bins 参数把 0 到 10 的评分切成三段labels 给每段名字产出新的类别列 rating_level后面 echarts 饼图可以直接用这列做分类统计。这是 pandas 给可视化铺路的最典型操作——不是在前端现算分类而是在数据处理阶段就把分类结果备好。类型转换是新手最容易忽略的一步。raw CSV 里 rating 读进来可能是浮点数也可能被读成 object取决于是否混入空字符串。astype(float) 会直接抛异常暴露问题这正是想要的——早失败比前端图表白屏好。print(df.info()) 这行很关键永远在清洗后看一眼 Non-Null Count 和数据类型确认每个字段都符合预期。4. 从 SQL 脚本到 echarts 大屏Flask 接口与可视化渲染全链路4.1 SQL 脚本的结构设计建表、导数据、统计视图一次到位清洗后的 CSV 是中间产物真正要交给 Flask 的是数据库。SQL 脚本我通常包含三部分建表、索引、统计分析视图。以 SQLite 为例movie.sql 的核心结构如下-- 电影主表存储清洗后的基础信息 CREATE TABLE IF NOT EXISTS movie ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL UNIQUE, rating REAL NOT NULL, quote TEXT DEFAULT , rating_level TEXT DEFAULT 推荐, page INTEGER DEFAULT 0 ); -- 评分索引评分筛选与排序都走这个索引 CREATE INDEX IF NOT EXISTS idx_movie_rating ON movie(rating); -- 统计视图按评分区间统计数量直接供 echarts 饼图使用 CREATE VIEW IF NOT EXISTS v_rating_dist AS SELECT rating_level AS name, COUNT(*) AS value FROM movie GROUP BY rating_level;逻辑说明title 加了 UNIQUE 约束这是数据库层面的第二道去重防线即使 CSV 里混入重复记录也会被拒之门外。rating 用 REAL 存储浮点评分而不是 TEXT保证 ORDER BY rating DESC 是数值排序而不是字典排序。索引 idx_movie_rating 在数据量几百条时感觉不到差别但答辩时被问到“查询性能怎么保证”就能拿它当论据。CREATE VIEW 是经常被低估的技巧。v_rating_dist 把 GROUP BY 统计固化成一个视图Flask 端只需 SELECT * FROM v_rating_dist 就能拿到饼图需要的 name-value 结构不需要在 Python 里再写分组逻辑。视图的意义是把数据准备尽量下沉到数据库层Flask 代码更简洁也更好维护。4.2 Flask 路由返回 JSON注意 pandas 类型不能直接塞进 jsonifyFlask 端的设计原则是“每个路由只干一件事”一个路由给饼图数据一个路由给柱状图数据一个路由给 TOP10 榜单。下面这段是 app.py 里最核心的接口写法from flask import Flask, jsonify, render_template import sqlite3 app Flask(__name__) DB_PATH movie.db def query_db(sql: str): conn sqlite3.connect(DB_PATH) conn.row_factory sqlite3.Row cur conn.execute(sql) rows [dict(r) for r in cur.fetchall()] conn.close() return rows # 换成你的真实库表结构 app.route(/api/movie/top_rated) def top_rated(): rows query_db( SELECT title, rating FROM movie ORDER BY rating DESC LIMIT 10 ) return jsonify({ ok: True, data: rows }) app.route(/api/movie/rating_dist) def rating_dist(): rows query_db(SELECT * FROM v_rating_dist) return jsonify({ ok: True, data: rows }) app.route(/) def index(): return render_template(index.html) if __name__ __main__: app.run(debugTrue, port5000)逻辑说明query_db 里 conn.row_factory sqlite3.Row 是关键它让查询结果可以用 dict(r) 转成字典否则 sqlite3 返回的是元组前端拿到后不知道字段名。每个接口包一层 { ok: True, data: rows } 的结构前端统一判断 ok 再取 data比直接返回数组更工程化。这里提醒一个血泪经验绝对不要把 pandas 的 DataFrame 直接传给 jsonify。DataFrame 转成 JSON 时会出现 int64、float64 类型不被 jsonify 识别的问题报错 TypeError: Object of type int64 is not JSON serializable。常见做法是先用 df.to_dict(orientrecords) 转成 Python 字典列表再交给 jsonify。我的习惯是让 Flask 走 SQL 查询而不是 pandas 直连数据库——SQL 查出的是原生 int/float天然规避这个问题。4.3 echarts 图表对接柱状图、饼图的数据格式与渲染写法前端页面 templates/index.html 里引入 echarts用 fetch 异步请求接口拿到数据后填充 option。下面是 TOP10 柱状图和评分分布饼图的完整示例!DOCTYPE html html langzh-CN head meta charsetUTF-8 title豆瓣电影数据分析/title !-- 使用 echarts 5 -- script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script style .chart-box { width: 45%; height: 400px; display: inline-block; } /style /head body div idbarChart classchart-box/div div idpieChart classchart-box/div script const barChart echarts.init(document.getElementById(barChart)); const pieChart echarts.init(document.getElementById(pieChart)); fetch(/api/movie/top_rated) .then(res res.json()) .then(json { if (!json.ok) return alert(接口返回异常); const titles json.data.map(item item.title); const ratings json.data.map(item item.rating); barChart.setOption({ title: { text: 豆瓣电影评分 TOP10, left: center }, tooltip: { trigger: axis }, xAxis: { type: category, data: titles, axisLabel: { rotate: 30 } }, yAxis: { type: value, max: 10, name: 评分 }, series: [{ type: bar, data: ratings, itemStyle: { color: function(params) { return params.value 9 ? #e74c3c : #3498db; } } }] }); }); fetch(/api/movie/rating_dist) .then(r r.json()) .then(json { if (!json.ok) return; pieChart.setOption({ title: { text: 评分区间分布, left: center }, tooltip: { trigger: item }, legend: { bottom: 0 }, series: [{ type: pie, radius: 60%, data: json.data, label: { formatter: {b}: {c} } }] }); }); /script /body /html这段代码值得注意的点有三个。axisLabel.rotate(30) 是解决电影名称过长互相遮挡的常规手段不旋转的话《肖申克的救赎》这类长标题会叠成一团。柱状图 itemStyle.color 用了回调函数评分大于 9 的柱子显示红色突出高分电影这个视觉细节答辩时很加分。饼图直接吃后端返回的 [{ name, value }] 结构这就是 SQL 视图 v_rating_dist 的作用——数据格式在数据库层已经对齐 echarts 的 series.data 要求。如果图表加载失败先按这个顺序排查打开浏览器 F12 的 Network看 /api/movie/top_rated 状态码是不是 200、返回的 JSON 是否符合预期再看 Console 里有没有 echarts 找不到 DOM 的警告最后确认 JS 执行顺序——init 必须在 DOM 渲染后调用所以 script 放在 body 底部。4.4 ECharts 的进阶配置渐变色与 dataZoom 让大屏更像样很多同学做完基础图表就停了,这个可视化是能做出差异化的。下面这段代码演示两个高频配置柱状图渐变色和折线图 dataZoom,都是面试和答辩常被问到的点:// 柱状图渐变色 series: [{ type: bar, data: ratings, itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: #83bff6 }, { offset: 1, color: #2f89fc } ]) } }] // 折线图缩放数据多的时候只显示窗口内的一部分 // xAxis 数据很长时加 dataZoom 后拖拽滚动 dataZoom: [{ type: slider, start: 0, end: 60, height: 18, bottom: 10 }]渐变色用 echarts.graphic.LinearGradient 声明纵向渐变从浅蓝过渡到深蓝视觉层次比纯色好一个档次。dataZoom 的 slider 组件让用户拖动查看不同区间的数据图表看起来专业很多。这两个配置都不需要后端配合纯前端改动是“花了最少力气提升完成度”的典型做法。5. 毕业设计避坑指南环境、部署与答辩翻车现场5.1 豆瓣反爬猝死请求头不全或者请求过快现象爬虫跑到第二三页就开始返回 418 或 403页面内容变成验证页面。很多同学第一反应是“需要更牛逼的代理”实际上绝大多数情况是请求头太朴素或频率过快。原因豆瓣对缺少 Referer、User-Agent 或单位时间内请求过多的客户端做拦截。有的请求返回正常的 HTML 但里面没有电影条目这种情况更隐蔽解析后一条数据都拿不到。解决第一headers 里必须带完整的 User-Agent、Accept-Language、RefererReferer 不要省略第二每页之间 sleep 1.5 到 3 秒随机值不要固定 2 秒——固定间隔的流量特征远比随机间隔明显第三状态码异常时退避 30 秒再继续而不是立刻重试同一个地址。我曾经把 sleep 改成 0.1 秒测试极限结果 IP 直接被限制到第二天才恢复,建议不要拿自己环境做这种压力尝试。5.2 pandas 的 int64 / float64 导致 jsonify 报错现象Flask 接口直接返回 DataFrame 或在 JSON 序列化时报错 TypeError: Object of type int64 is not JSON serializable图表全部白屏。原因pandas 的数值类型是 numpy 的 int64、float64Python 标准 json 库不认识。如果沿用 SQL 查询SQLite 返回的是 Python 原生 int/float不会踩这个坑一旦路径切换成 pandas 直出就会炸。解决不要硬碰硬两条路任选——要么像第 4 章那样用 sqlite3 查询并 dict(r) 转换要么洗干净后用 df.to_dict(orientrecords) 把 DataFrame 变成 Python 列表字典。我一般选前者因为 SQL 视图还能承担聚合逻辑。排查时看到 int64 报错第一反应就该是“这是 pandas 类型问题去转换处而不是前端找原因”。5.3 Windows 下 CSV 与数据库中文乱码现象raw_movies.csv 用 Excel 打开是乱码或 SQLite 里查询显示 但终端 print 完全正常。原因Windows 的 Excel 默认用 GBK 解析 CSV而 Python 默认写 UTF-8没有 BOM 时 Excel 就会误读。SQLite 本身存 UTF-8 没问题乱码通常发生在从 CSV 手动导入、编码声明不一致的时候。解决写 CSV 时用 encodingutf-8-sig这个编码会在文件头加 BOMExcel 识别为 UTF-8。如果已经写错了可以 pandas 读进来再重新存一次。SQLite 这边统一在 connect 后执行 PRAGMA encoding UTF-8; 并在建表语句里给 TEXT 字段明确 CHARSET——虽然 SQLite 无视 CHARSET,但写上能让阅读脚本的人立刻知道意图。这个问题 90% 出现在 Windows 环境macOS/Linux 用户基本不用管。5.4 Flask debug 模式导致爬虫被重复执行现象爬虫模块写进 flask 启动入口后每次保存文件控制台就出现两条爬虫同时跑的日志数据库出现重复数据。原因Flask 的 debugTrue 会启动 reloader,它同时运行两份进程来监听文件变化。文档里没细说但这个双进程机制对自动化采集脚本是致命伤害。解决开发时把爬虫单独用一个脚本 run_spider.py 执行不要在 Flask 启动流程里触发爬虫非要耦合也是用锁文件或时间戳判断但最省心的就是分层。提交演示或收入论文时确保 app.run(debugFalse)此时单进程运行接口行为更稳定。这条坑不是报错型坑是会“悄悄给你制造混乱数据”的坑答辩演示时数据库里冒出一堆重复行会很尴尬。5.5 echarts 图表不显示x轴标签拥挤与数据格式隐藏问题现象柱状图渲染出来横轴十几部电影片名全部重叠连成一个长条或饼图完全空白F12 里能看到接口正常返回但图表不动。原因前者是 xAxis 的 axisLabel 没有加 rotate 或 interval长文本互相遮盖后者是数据格式不对echarts 的饼图 series.data 需要 [{ name, value }] 结构接口返回了 name-value 数组但键名拼错比如 name 写成 names前端不会报错但就是画不出来。解决x 轴长文本统一 axisLabel.rotate(30) 或设置 interval: 0 配合 formatter 换行。饼图数据在 Flask 接口返回前用 Python 侧打印 json.dumps(rows, ensure_asciiFalse) 人肉核对一次键名。echarts 的隐晦之处在于数据不合理往往不报错只表现为白屏或少图形前端不要只盯着 Console要把接口返回的 JSON 贴到编辑器里对照 echarts 文档的 data 结构逐字段检查。6. 超出预期加一个电影推荐接口让系统不止于可视化如果答辩想拉开差距在现有 DataFrame 和 SQLite 基础上加一个“相似影片推荐”接口成本很低但效果明显。思路是选定一部电影后按两个维度打分——评分差值小于 0.5 且属于同一评分区间 rating_level 的影片优先叠加年份区间可自行在爬虫中补充年份字段算出近似分最后返回 TOP5。SQL 里用窗口函数或直接 Python 排序均可我倾向于在 Flask 层做因为推荐逻辑便于动态调参app.route(/api/movie/recommend/title) def recommend(title: str): target query_db( SELECT title, rating, rating_level FROM movie WHERE title ?, [title] ) if not target: return jsonify({ok: False, msg: 影片不存在}), 404 t target[0] rows query_db( SELECT title, rating, rating_level FROM movie WHERE title ! ?, [title] ) scored [] for row in rows: score 0.0 if row[rating_level] t[rating_level]: score 2 if abs(row[rating] - t[rating]) 0.5: score 1 scored.append({**row, score: score}) scored.sort(keylambda x: (-x[score], x[rating]), reverseTrue) return jsonify({ok: True, data: scored[:5]})验证方法在浏览器直接访问 /api/movie/recommend/肖申克的救赎观察返回列表里是否包含同评分区间的高分片目再换一部冷门电影确认接口不会因为数据少而报错。这个推荐虽然不涉及协同过滤但已经能说明“从静态展示走向基础算法”的能力。回到这套系统本身我最深的教训是毕业设计质量的差距不在单个技术点——requests 谁都会写pandas 也没难度真正拉开差距的是把数据流理清楚每一层都留下可验证的产物。源码、SQL 脚本、论文三件套如果只是堆砌文件答辩一追问就露怯但当你把清洗前后对比、SQL 视图结构、接口返回格式这三样东西一点点讲明白基本上已经可以预判答辩老师会在哪个方向点头。做到这一步这个题目的价值就真正落地了。希望这篇笔记能帮当时跟我一样对着豆瓣页面发愁的同学少走一段弯路做出一套不仅能跑、还能讲清楚来龙去脉的系统。本文还有配套的精品资源点击获取