
简介这份资源是面向计算机专业学生与Python初学者的毕业设计完整方案围绕豆瓣电影数据爬取、分析与可视化展开可解决选题落地、代码实现与论文撰写的一站式需求。包内共1054个文件以688个py源码、157个pyc编译文件为主体辅以html、js、css等前端页面与ECharts图表资源另有sql数据库脚本、docx论文文档及csv数据样例压缩包约30.07MB目录结构完整便于按模块查阅。项目基于Flask搭建后端、pandas完成数据清洗处理、爬虫采集豆瓣电影信息并通过ECharts实现多角度动态图表展示帮助用户直观识别影片数据规律。已有257人学习下载适合需要参考完整赛题实现、复用爬虫与可视化代码、对照论文框架查漏补缺的读者。1. 从一份能跑起来的毕设说起豆瓣电影数据从爬到看的一条龙每年到了毕设季后台被问得最多的一类问题就是「有没有能直接跑起来的 Python 数据分析项目」。这套基于 Flask ECharts 爬虫 pandas 的豆瓣电影数据分析与可视化系统正好踩在这个需求点上它把「爬数据 → 清洗入库 → 后端接口 → 前端图表」整条链路都做完了还附带 SQL 脚本和论文属于拿到手就能跑、跑完还能照着改的那一类。适合谁一是赶毕设需要完整闭环的同学二是想练手 Flask 全栈 pandas 分析的中级开发者。它不追求高并发、不上微服务但胜在链路完整、代码直白你能清楚看到一条数据从网页到图表的全过程。2. 技术栈与目录结构先看清这套系统由哪几块拼起来2.1 四层技术选型各管一段这套系统的技术栈没有花活全是毕设场景下最稳的组合拆开看是四层层次技术承担职责数据采集requests BeautifulSoup / lxml抓取电影列表、评分、评论等页面数据数据处理pandas numpy清洗、去重、类型转换、分组聚合数据存储MySQL / SQLite SQL 脚本建表、落库、供后端查询展示层Flask ECharts Jinja2提供接口与页面前端渲染图表选 Flask 而不是 Django是因为这个项目接口数量有限、不需要自带 admin 和 ORM 全家桶Flask 轻、路由清晰新手改起来不容易迷路。ECharts 负责可视化柱状图、饼图、词云、折线趋势都能覆盖配置项写 JSON 就行比手撸 canvas 省事太多。pandas 在这里是绝对主力清洗和聚合基本靠它SQL 只做存储和简单查询复杂统计尽量在 pandas 里完成这样逻辑集中、调试方便。2.2 目录结构长什么样一个典型的工程目录大致是这样组织的不同版本可能略有出入但主干一致douban_movie_analysis/ ├── app.py # Flask 入口注册路由与蓝图 ├── config.py # 数据库连接、密钥等配置 ├── requirements.txt # 依赖清单 ├── spider/ │ ├── movie_spider.py # 电影列表与详情爬取 │ └── comment_spider.py # 短评爬取 ├── analysis/ │ ├── clean.py # 数据清洗脚本 │ └── stats.py # 统计分析函数 ├── models/ │ └── movie.py # 数据表映射与查询封装 ├── static/ │ ├── js/ # ECharts 及页面脚本 │ └── css/ ├── templates/ │ ├── index.html # 首页概览 │ └── detail.html # 单部电影详情 ├── sql/ │ └── init.sql # 建表与初始数据脚本 └── data/ └── movies.csv # 爬取后的中间数据这个结构的好处是职责边界清楚spider只管抓analysis只管算models只管查app.py只做路由分发。二次开发时你想加一个「按年份统计票房」的图基本只需要动analysis/stats.py加一个函数、app.py加一条路由、前端加一个图表容器三处改动互不干扰。2.3 数据表设计三张表撑起整个分析SQL 脚本里核心就三张表字段设计直接决定了后面能做什么分析-- 电影主表 CREATE TABLE movie ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(128) NOT NULL, -- 电影名 year INT, -- 上映年份 country VARCHAR(64), -- 国家/地区 genre VARCHAR(128), -- 类型多个用 / 分隔 rating FLOAT, -- 评分 votes INT, -- 评价人数 director VARCHAR(64) -- 导演 ); -- 短评表 CREATE TABLE comment ( id INT PRIMARY KEY AUTO_INCREMENT, movie_id INT, content TEXT, star INT, -- 星级 1-5 FOREIGN KEY (movie_id) REFERENCES movie(id) ); -- 类型维度表可选用于多类型拆分统计 CREATE TABLE genre ( id INT PRIMARY KEY AUTO_INCREMENT, movie_id INT, genre_name VARCHAR(32) );genre字段用/分隔多个类型是为了爬取时省事但做「各类型电影数量」统计时就得先拆开这也是后面清洗环节要处理的重点。votes存评价人数而不是字符串「12万人看过」是为了能直接做数值排序和分桶这类「存数值不存展示文本」的习惯在数据分析项目里能省掉大量后期麻烦。3. 爬虫与数据清洗一条数据从网页到 DataFrame 的完整链路3.1 爬虫主流程请求、解析、翻页、落盘爬虫部分的核心逻辑不复杂但几个细节决定了它能不能稳定跑完。下面这段是电影列表爬取的骨架import requests from bs4 import BeautifulSoup import pandas as pd import time import random HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 } def fetch_page(url, retry3): 带重试的页面请求失败返回 None for i in range(retry): try: resp requests.get(url, headersHEADERS, timeout10) if resp.status_code 200: return resp.text except requests.RequestException: pass time.sleep(random.uniform(1, 3)) # 随机间隔降低被封风险 return None def parse_movies(html): 解析单页电影列表返回字典列表 soup BeautifulSoup(html, lxml) rows [] for item in soup.select(div.item): title_tag item.select_one(span.title) rating_tag item.select_one(span.rating_num) votes_tag item.select_one(span.star span:last-child) if not title_tag or not rating_tag: continue rows.append({ title: title_tag.get_text(stripTrue), rating: float(rating_tag.get_text(stripTrue)), votes: int(votes_tag.get_text(stripTrue).replace(人评价, )) if votes_tag else 0, }) return rows def crawl(total_pages10): all_rows [] for page in range(total_pages): url fhttps://example.com/top250?start{page * 25} html fetch_page(url) if not html: print(f第 {page} 页抓取失败跳过) continue all_rows.extend(parse_movies(html)) time.sleep(random.uniform(2, 5)) df pd.DataFrame(all_rows) df.to_csv(data/movies.csv, indexFalse, encodingutf-8-sig) return df逐段说清楚fetch_page里加了重试和随机 sleep这是爬虫能不能跑完的关键很多新手直接requests.get一把梭跑到一半被限流就前功尽弃。parse_movies用 CSS 选择器定位select_one找不到返回 None所以每个字段都要判空否则一条脏数据就能让整个循环崩掉。votes字段从「12345人评价」里剥出数字用replace去掉中文后缀再转 int这一步不做后面 pandas 里这列就是 object 类型排序和分桶全废。crawl里翻页参数start每次加 25是列表页的固定步长total_pages按你需要的数据量调抓太多既慢又容易触发风控。提示爬取频率和总量要克制time.sleep的随机区间别省这是血泪经验——省掉它省下的时间最后都会花在重跑上。3.2 清洗把「能看」的数据变成「能算」的数据爬下来的原始数据通常有一堆问题类型字段带斜杠、年份缺失、评分是字符串、重复条目。清洗脚本要逐项处理import pandas as pd import numpy as np def clean_movies(pathdata/movies.csv): df pd.read_csv(path) # 1. 去重同名同年的视为重复 df df.drop_duplicates(subset[title, year], keepfirst) # 2. 数值列强制转换非法值变 NaN 后剔除 df[rating] pd.to_numeric(df[rating], errorscoerce) df[votes] pd.to_numeric(df[votes], errorscoerce).fillna(0).astype(int) df df.dropna(subset[rating]) # 3. 年份缺失用中位数填充并转 int df[year] df[year].fillna(df[year].median()).astype(int) # 4. 类型字段拆分剧情/爱情 - [剧情, 爱情] df[genre_list] df[genre].fillna(未知).str.split(/) # 5. 派生列评分等级分桶方便做分布图 bins [0, 6, 7, 8, 9, 10] labels [6分以下, 6-7分, 7-8分, 8-9分, 9分以上] df[rating_level] pd.cut(df[rating], binsbins, labelslabels, rightFalse) return df几个参数值得展开drop_duplicates的subset选title year而不是只选title是因为同名翻拍电影很常见只按标题去重会误删。pd.to_numeric(errorscoerce)把无法转换的值变成 NaN 而不是抛异常这是批量清洗的标准姿势。pd.cut的rightFalse表示左闭右开区间[6,7)归到「6-7分」边界归属要提前想清楚否则统计结果会和预期差一截。genre_list这列是给后面「类型统计」用的explode一下就能把一部多类型电影拆成多行这是 pandas 做维度展开的常用手法。3.3 统计分析函数把聚合逻辑收在一处分析层不直接写在路由里而是抽成独立函数返回前端能直接用的结构def genre_count(df): 各类型电影数量 Top10 exploded df.explode(genre_list) counts exploded[genre_list].value_counts().head(10) return {genres: counts.index.tolist(), counts: counts.values.tolist()} def year_rating_trend(df): 按年份统计平均评分趋势 trend df.groupby(year)[rating].mean().round(2) trend trend[trend.index 1990] # 过滤过早年份样本太少 return {years: trend.index.tolist(), ratings: trend.values.tolist()} def rating_distribution(df): 评分等级分布用于饼图 dist df[rating_level].value_counts().sort_index() return [{name: k, value: int(v)} for k, v in dist.items()]explode是这里的关键它把genre_list里的列表展开成多行一部「剧情/爱情」的电影会变成两行这样value_counts统计出来的才是「类型出现次数」而不是「电影数」。year_rating_trend里过滤 1990 年以前的数据是因为早期样本量太小平均分波动大画到折线图上就是一根乱跳的线反而干扰阅读。返回结构统一成{维度: [...], 数值: [...]}或 ECharts 直接能吃的[{name, value}]前端拿到就能setOption不用再做二次转换。4. Flask 接口与 ECharts 渲染图表是怎么被喂出来的4.1 路由设计一个接口对应一张图后端路由保持「一图一接口」的粒度简单直接from flask import Flask, jsonify, render_template from analysis.clean import clean_movies from analysis import stats app Flask(__name__) DF clean_movies() # 启动时加载一次避免每次请求重复清洗 app.route(/) def index(): return render_template(index.html) app.route(/api/genre) def api_genre(): return jsonify(stats.genre_count(DF)) app.route(/api/trend) def api_trend(): return jsonify(stats.year_rating_trend(DF)) app.route(/api/distribution) def api_distribution(): return jsonify(stats.rating_distribution(DF)) if __name__ __main__: app.run(debugTrue, port5000)DF在模块加载时算一次并常驻内存是这个数据量级下的合理选择——几万条电影数据放内存毫无压力每次请求重新读 CSV 再清洗纯属浪费。jsonify会自动设置Content-Type: application/json并序列化注意 numpy 的int64、float64默认不能被 json 序列化所以stats里返回前都做了int()、round()或.tolist()转换这一步漏了就会报TypeError: Object of type int64 is not JSON serializable是 Flask pandas 组合里最常见的翻车点之一。4.2 前端渲染ECharts 初始化与数据对接页面里每个图表容器对应一段初始化脚本以类型柱状图为例// 类型分布柱状图 fetch(/api/genre) .then(res res.json()) .then(data { const chart echarts.init(document.getElementById(genreChart)); chart.setOption({ title: { text: 各类型电影数量 Top10 }, tooltip: { trigger: axis }, xAxis: { type: category, data: data.genres, axisLabel: { rotate: 30 } // 类型名较长倾斜避免重叠 }, yAxis: { type: value, name: 数量 }, series: [{ type: bar, data: data.counts, itemStyle: { color: #5470c6 } }] }); });echarts.init绑定 DOM 容器setOption一次性把配置灌进去。axisLabel.rotate是处理长标签的实用参数类型名像「剧情」「喜剧」还好遇到「动画/奇幻/冒险」这种不倾斜就会糊成一片。tooltip.trigger: axis让鼠标悬停时按整列显示比单点触发更适合柱状图。如果图表在容器隐藏比如 tab 切换时初始化会出现宽度为 0 的空白图常见做法是切 tab 后调一次chart.resize()这个坑几乎每个用 ECharts 的人都踩过。4.3 页面组织Jinja2 模板与静态资源templates/index.html用 Jinja2 语法引入静态资源结构上就是「容器 脚本」!DOCTYPE html html langzh-CN head meta charsetUTF-8 title电影数据分析看板/title script src{{ url_for(static, filenamejs/echarts.min.js) }}/script /head body div idgenreChart stylewidth: 100%; height: 400px;/div div idtrendChart stylewidth: 100%; height: 400px;/div script src{{ url_for(static, filenamejs/charts.js) }}/script /body /htmlurl_for(static, filename...)是 Flask 生成静态资源路径的标准写法别硬编码/static/js/...否则部署到子路径下就全 404。图表容器必须给明确高度ECharts 靠容器尺寸计算画布高度为 0 时图直接不显示这是新手最常问的「为什么页面一片空白」的头号原因。5. 跑起来与避坑从零启动这套系统的实操记录5.1 环境准备与启动步骤按顺序走基本不会出岔子# 1. 建虚拟环境Python 3.8 均可 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 2. 装依赖 pip install -r requirements.txt # 3. 初始化数据库MySQL 示例 mysql -u root -p your_db sql/init.sql # 4. 改配置 # 编辑 config.py填数据库地址、账号、密码 # 5. 先跑爬虫拿数据也可直接用附带的 csv python spider/movie_spider.py # 6. 启动服务 python app.py启动后访问http://127.0.0.1:5000就能看到看板。如果只想看效果不想等爬虫直接用data/movies.csv里的现成数据把clean_movies的路径指过去即可。requirements.txt里通常包含 flask、pandas、numpy、requests、beautifulsoup4、lxml、pymysql 这几项lxml在部分环境需要编译装不上就换html.parser作为 BeautifulSoup 的解析器功能一样只是慢一点。5.2 常见问题排查五个真实踩过的坑现象一启动报ModuleNotFoundError: No module named analysis。原因是从项目根目录以外的地方启动Python 找不到包路径。解决确保在项目根目录执行python app.py或在入口文件顶部加sys.path.append(os.path.dirname(__file__))。现象二接口返回 500日志里是Object of type int64 is not JSON serializable。原因是 pandas 聚合结果里的 numpy 类型没转成原生 Python 类型。解决在stats函数返回前统一.tolist()或int()别指望 Flask 帮你兜底。现象三图表容器有但页面空白无图。原因多半是容器高度为 0或 ECharts 在容器隐藏时初始化。解决给容器写死heighttab 切换场景在显示后调chart.resize()。现象四爬虫跑几十条就中断报连接超时或 403。原因是请求频率过高被限流。解决加大time.sleep随机区间、补全User-Agent、失败重试必要时减少total_pages分批跑。现象五中文在图表或 CSV 里乱码。原因是编码不统一。解决to_csv用encodingutf-8-sigHTML 里声明meta charsetUTF-8数据库连接串加charsetutf8mb4三处对齐就不会乱。注意改完config.py记得重启服务Flask 的debugTrue会自动重载代码但配置模块的改动有时不触发重载别对着旧配置调试半天。6. 二次开发加一张图、换一个数据源的改造思路拿到这套代码最有价值的改造方向是「加维度」和「换数据源」而不是推倒重来。加一张图的标准动作是三处联动analysis/stats.py里写一个返回{维度, 数值}的聚合函数app.py里挂一条/api/xxx路由前端加一个容器和一段setOption。比如想加「导演作品数 Top10」函数体就是df[director].value_counts().head(10)路由和图表照抄现有模式十分钟能跑通。这种「函数 → 接口 → 图表」的固定套路是这个项目结构最大的红利。换数据源时关键是把清洗层当成适配器。不管你换成哪个电影数据来源只要最终产出和clean_movies返回的 DataFrame 列名一致title、year、rating、votes、genre_list、rating_level上层分析和接口一行都不用改。我一般会先写一个to_standard(df)函数做列名映射和类型对齐再让所有下游只认标准列这样换源就是换一个适配函数的事。验证改造是否成功别只看页面出没出图要拿几个已知数据点对一遍比如某类型电影总数、某年份平均分用 SQL 单独查一次和图表数字比对对得上才算真跑通。下面这张表是我常用的自检清单检查项验证方法通过标准数据完整性df.shape与源数据条数比对差异在去重合理范围内聚合正确性SQL 手查 vs 接口返回数值一致接口可用性浏览器直接访问/api/xxx返回合法 JSON图表渲染页面实际显示有图、有数据、可交互最后说个习惯每次改完分析函数我都会先用python -c在命令行里单独调一次确认返回结构对了再接前端。直接改完就刷页面出了问题你分不清是数据错、接口错还是图表配置错排查成本翻倍。这套项目不大但链路完整把这条链路走顺一遍比看十篇教程都实在。希望帮到你。本文还有配套的精品资源点击获取