简介这是一份面向Python方向毕业设计学生的豆瓣电影数据分析与可视化系统完整源码包。项目以Flask为Web框架结合爬虫获取豆瓣电影数据经pandas清洗处理后使用ECharts生成多角度可视化图表后端数据存储采用MySQL并随包附带SQL脚本与论文文档。压缩包共1054个文件体积约30MB其中以688个Python源码文件和157个字节码文件为核心辅以HTML/CSS/JS前端页面、CSV数据文件、可执行依赖以及环境配置等能够直接支撑项目运行、修改与文档撰写。目前已有256人学习/下载特别适合缺少项目经验、希望参考完整数据采集到可视化流程的毕业设计用户。资源覆盖从Pycharm环境搭建、网络爬虫、数据预处理到ECharts动态视图展示的完整链路并配有论文可辅助快速理解技术选型与数据库设计能显著缩短二次开发与答辩准备的周期。1. 豆瓣电影分析系统一个爬虫到可视化的一站式毕设框架之前有个学弟拿豆瓣电影当毕设选题最初是在 Excel 里看评分接着被导师要求「做成网页系统」最后落地成了这套基于 Python 的 Flask 应用自己抓数据、清洗入库、写查询接口、前端用 echarts 出图整个链路走通就是一个完整的大数据可视化毕设。这套源码包拿到手里你会看到爬虫脚本、pandas 处理逻辑、SQL 建表脚本、Flask 路由和前端图表模板以及配套的毕业设计论文属于「后端、数据、前端」三件套齐全的工程型项目。适合两类人一类是还没定题、想把数据分析类毕设做成「能演示的东西」的同学——照着这套改一改就能跑另一类是新手开发者想搞明白「爬虫抓下来的数据怎么经过清洗变成一张能看的图表」的完整过程。整个项目技术栈很典型Flask 做 Web 服务、requests 抓页面、pandas 处理数据、echarts 出图没有太重型的框架只要会 Python 基础就能握住它。2. 技术栈与整体架构Flask 怎么串联爬虫、pandas 和 echarts2.1 工程目录源码包里每层文件在干什么拆开压缩包后建议先按目录按功能过一遍不要急着运行。整个工程的典型结构如下douban-movie-analysis/ ├── spiders/ │ ├── movie_spider.py # 爬虫翻页采集豆瓣电影列表 │ └── user_agents.py # 请求头池避免 UA 被识别 ├── analysis/ │ ├── data_clean.py # pandas 清洗去重、补缺失、类型转换 │ └── stats_builder.py # 统计分组聚合生成图表要用的 JSON ├── sql/ │ └── init_db.sql # 建库建表脚本SQLite 数据表 ├── app.py # Flask 主入口定义 /api/* 接口 ├── templates/ │ └── index.html # 可视化大屏页面 ├── static/ │ ├── js/echarts.min.js # echarts 库文件 │ └── css/style.css # 页面样式 ├── data/ │ └── movies.db # SQLite 数据库初始化后生成 ├── requirements.txt # 依赖清单 └── 论文/ └── 正文.doc # 毕业设计论文文档拆这个目录的时候我注意到一个很实用的设计爬虫、清洗、Flask 三层拆分得很干净。spiders/管采集analysis/管加工app.py只管把 SQL 查询包装成 HTTP 接口——后面想换数据源只改第一个目录想换图表类型只动templates/即可不牵连别的模块。这正是毕设答辩时导师最爱问的「模块划分」问题这套结构可以直接正面回答。2.2 数据层设计两张表搞定常规查询这套系统的数据库用的是 SQLite建库脚本在sql/init_db.sql里。核心是两张表movies存电影元数据movie_ratings存评分明细。CREATE TABLE movies ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, genres TEXT, -- 类型多个用 | 分隔 year INTEGER, region TEXT, directors TEXT, actors TEXT, rating REAL, votes INTEGER, crawl_time DATETIME ); CREATE TABLE movie_ratings ( id INTEGER PRIMARY KEY AUTOINCREMENT, movie_id INTEGER NOT NULL, rating_value REAL, rating_count INTEGER, star_level INTEGER, -- 1~5 星档位 create_time DATETIME );这里说三个容易被新手忽略的设计点。第一genres用|分隔存字符串而不是建一张关联表因为偏好分析只需要「包含/不包含」不需要严格的范式关系这样做可以少掉两次 JOIN查询更快。第二rating用 REAL 类型评分肯定又要浮点但注意在 pandas 清洗阶段要先转成 float再写库。第三crawl_time字段是给「增量更新」准备的毕设如果写「每日抓取一次」这个字段就是证据答辩时能解释为什么要留它。实际跑通之后这套表设计应对「年度评分趋势」「类型分布饼图」「地区 Top10 柱状图」这几个典型图表毫无压力。你没看错常规可视化需求的查询用一张主表就能完成完全不碰明细表只有做「评分数区间分布」时才GROUP BY star_level查movie_ratings。3. 核心链路拆解一部电影的评分从互联网落到折线图的全过程3.1 爬虫设计翻页采集与请求头伪装这套系统的数据从公开网页获取写法和普通爬虫工程一致requests 发 GET用 lxml 或正则解析列表页翻页循环。采集脚本movie_spider.py的核心逻辑如下import requests from lxml import etree import time from user_agents import get_random_ua def crawl_movie_list(start0, limit50): 按豆瓣列表页格式翻页抓取 start: 起始偏移每页 25 条翻页 25 limit: 期望抓取总数默认 50 条做演示 movies [] headers {User-Agent: get_random_ua()} while start limit: url fhttps://movie.douban.com/j/search_subjects?typemovietag热门sortrecommendpage_limit25page_start{start} resp requests.get(url, headersheaders, timeout10) items resp.json().get(subjects, []) if not items: break for item in items: movies.append({ title: item[title], rating: item[rate], cover: item[cover], url: item[url], }) start 25 time.sleep(2) # 频率控制避免请求过密 return movies这段代码里最关键的是page_start{start}和time.sleep(2)。翻页不是拼接page2那种而是用偏移量start按 25 条递增这是该页面本身的接口约定不按它的参数规范写翻到第二页就会拿空数据。sleep 的 2 秒是频率控制临时把项目当学习样本跑做一个带礼貌延时。抓完列表页只是拿到一个要素评分详情要进详情页再取。常见做法是列表页收集url字段再写一个crawl_detail(movie_url)函数逐条抓directors、genres、votes同样注意 header 伪装和超时设置。原型验证阶段抓 50 条够了别一上来抓 2000 条爬太快容易被反爬手段盯上这是这篇源码里最值得保留的经验。3.2 pandas 清洗与入库从 CSV 到 SQLite 的过程裸爬出来的数据直接写库会出问题有的字段是空字符串评分在网页里可能是9.2字符串年份可能混进「中国大陆 / 2018」这种脏值。所以data_clean.py这层不能省我拆解时觉得它写得比较克制的部分如下import pandas as pd import sqlite3 def clean_and_store(csv_path, db_pathdata/movies.db): df pd.read_csv(csv_path, encodingutf-8-sig) # 去掉完全重复的行只保留第一次出现的记录 df df.drop_duplicates(subset[title, year], keepfirst) # 把空评分填为 0再转成 float避免后续聚合报错 df[rating] pd.to_numeric(df[rating], errorscoerce).fillna(0.0) # 年份字段抽数字保留 4 位年份 df[year] df[year].astype(str).str.extract(r(\d{4})).fillna(0).astype(int) # 类型列如果为空填 未知并转成字符串 df[genres] df[genres].fillna(未知).astype(str) conn sqlite3.connect(db_path) df.to_sql(movies, conn, if_existsappend, indexFalse) conn.close() return len(df)给初次看这段代码的人解释三个地方。drop_duplicates(subset[title, year])是防列表页多次抓取重复入库的关键毕设系统重复数据一多图表里会出现诡异的「炒作」去重这一步是在写库前最后一道防线。pd.to_numeric(errorscoerce)是清洗脏数字的标准手法遇到解析失败的值会变NaN然后再统一fillna(0)比直接astype(float)崩给你看要温和得多。最容易被忽略的是encodingutf-8-sig这个细节Windows 下写 CSV 不带这个存进 SQLite 的中文大概率乱码后面图表标题就变成「锟斤拷」。df.to_sql(movies, if_existsappend)的语义是「追加」适合多轮增量采集。如果想清库重建先执行DROP TABLE再跑这段即可不建议改成replace否则会把表结构重建导致 sql 建表脚本里的约束丢失。3.3 Flask 路由把 SQL 查询包装成 JSON 接口pandas 洗完的数据进库下一步是让前端能按条件查询。app.py里每个接口模式几乎相同从request.args拿参数拼 SQLsqlite3查询组装成列表嵌套字典再jsonify返回。年度评分趋势接口就是最典型的一个app.route(/api/year_trend) def year_trend(): conn sqlite3.connect(data/movies.db) conn.row_factory sqlite3.Row start_year request.args.get(start_year, default2000, typeint) end_year request.args.get(end_year, default2024, typeint) sql SELECT year, COUNT(*) AS movie_count, ROUND(AVG(rating), 1) AS avg_rating FROM movies WHERE year BETWEEN ? AND ? GROUP BY year ORDER BY year ASC rows conn.execute(sql, (start_year, end_year)).fetchall() conn.close() data { categories: [row[year] for row in rows], series: [ {name: 影片数量, data: [row[movie_count] for row in rows]}, {name: 平均评分, data: [row[avg_rating] for row in rows]}, ], } return jsonify({code: 0, data: data})这段代码抓住了 Flask SQLite 轻量应用的几个习惯。conn.row_factory sqlite3.Row这行一加查询出来的行就可以用row[year]按列名取值否则只能按下标取可读性差一截。SQL 里BETWEEN ? AND ?用的是参数化写法值由 Flask 的typeint解析后的变量传入不拼字符串既防注入也省去手动转类型。两个接口参数start_year和end_year都给了默认值前端不清参也能出全量图这个「有默认值」的习惯很适合演示场景——评委访问首页时不用配置任何参数就能看到图。我额外关注了返回结构它设计成{code:0, data:{categories: [...], series: [...]}}code 为 0 表示成功。这个结构恰好和 echarts 的 option 一一对应前端几乎不用做二次加工拿来就能直接塞进图表配置——后面第 4 章你会看到这种「后端直接出图表数据」的配合方式。3.4 数据流向一次前端请求的完整链路为了便于理解我梳理一下从浏览器到数据表的完整链路这是缩写为「前端发起 → 接口拼装 → 驱动查询 → 规整返回」的过程。用户打开 index.html - echarts 初始化折线图容器 - fetch(/api/year_trend?start_year2000end_year2024) - Flask 路由接收参数拼参数化 SQL - sqlite3 执行 GROUP BY 查询 - 返回 {code:0,data:{categories:[2000,2001,...],series:[...]}} - 前端把 data.categories 绑到 xAxis把 series 绑到 series - echarts 渲染出图接口层没有掺杂「先查 pandas DataFrame 再转 JSON」的复杂操作直接面向 SQLite 查询。这样做的优势是直观——任何 SQL 能算出的统计量都可以人工核对比如用 Navicat 查一次确认接口数据是对的再让图表去展示它排查问题时就清晰多了。大多数函数库做这个项目都是这个套路所以这份代码的阅读门槛不高正适合二次开发。4. 可视化层实现echarts 图表如何从接口拿到数据并渲染4.1 大屏布局五个图表组合如何排布整个templates/index.html做的是经典的数据大屏布局顶部标题条下面用 CSS Grid 或 flex 切出五个区间——左上评分年度趋势折线图、右上类型占比饼图、中间地区 Top10 柱状图、左下评分区间分布散点或条形、右下人气榜表格列表。这种布局在答辩时很吃香因为评委站在两米外就能看清一张屏上的多维度数据。前端引 echarts 的方式值得留意。源码里把echarts.min.js放在了static/js/目录而不是用 CDN 链接。跑这套系统的主力环境可能是没外网条件的教室或机房CDN 会在演示现场掉链子本地文件是保证演示不断网的核心前提。后面避坑章节我会专门再讲一次这个问题因为我见过太多次现场翻车都是因为内网环境加载不了 CDN。五个图表里年度趋势图的数据对应上一章的/api/year_trend区域分布对应/api/region_top类型占比对应/api/genre_pie评分区间对应/api/rating_dist。每个接口都返回categories series结构前端写图表时基本是「复制一段 option换接口名调整颜色」的工作没有难啃的地方。4.2 前端取数与渲染fetch option 的固定套路接着看前端核心段代码所有图表统一采用一个模式fetch请求数据把返回值填充进option设置到已初始化的图表实例上。async function loadYearTrend() { const resp await fetch(/api/year_trend?start_year2000end_year2024); const result await resp.json(); if (result.code ! 0) { console.error(接口返回异常, result); return; } const data result.data; const chart echarts.init(document.getElementById(yearTrendChart)); const option { tooltip: { trigger: axis }, legend: { data: [影片数量, 平均评分] }, xAxis: { type: category, data: data.categories }, yAxis: [ { type: value, name: 数量, minInterval: 1 }, { type: value, name: 评分, min: 0, max: 10 } ], series: data.series.map(item { return { name: item.name, type: line, smooth: true, data: item.data }; }) }; chart.setOption(option); }这段代码里有个细节很值得玩味yAxis配置了两条左轴管数量右轴管评分。因为评分总是在 0~10 之间而电影数量可能是几十上百共用一个轴会出现评分曲线几乎贴地的视觉效果。双轴是处理「不同量纲数据同图比较」的标准解法。第二个细节是data.series.map(...)后端返回的 series 结构直接映射成 echarts 的 series 数组图表类型由前端决定后端只管给数据这种「数据与表现分离」的方式主要在于改图灵活——今天想用折线明天想改柱状后端一行不用动。还有一个没写在这段代码里但值得提的窗口缩放监听。演示的时候如果投影仪分辨率不同图表可能出现显示不全或挤压。常见做法是加一行window.addEventListener(resize, () chart.resize())这个细节代表着较好的工程素质在答辩点评里也经常被当成项目亮点。4.3 图表的响应式与配色让大屏不显得廉价源码里的图表配色以深色背景蓝绿色系为主这和 demo 数据的气质匹配。我不建议你在一开始就花大量时间调样式优先把图表的数据正确性弄对——先让折线图按接口返回的数据绘制成功再调整color调色板。echarts 的color项配一组数组颜色会被循环使用配一套 5 色的柔和渐变视觉上就有「数据大屏」感不需要额外引入 UI 框架。响应式这块大屏页面明确建议用vw/vh或 flex 自适应而不是写死百分比然后填充 padding。五个图表容器建议统一用styleheight: 40vh这种视口高度写法在不同投影比例下相对稳定调试成本低。大部分毕设系统的可视化页其实功力都花在这但上手的切入点是先把数据跑通。5. 启动运行与常见问题排查建库、装依赖、跑通一次5.1 环境准备与启动步骤这份源码运行门槛不高前置条件只有 Python 3.7 及以上版本建议 3.9 左右兼容性好无需再去配置 MySQL 或其他数据库。依赖清单requirements.txt大概长这样flask2.2.5 requests2.31.0 pandas1.5.3 lxml4.9.2第一次运行的完整流程分四步。第一步创建虚拟环境并装依赖Windows 与 macOS 的命令略有差异但核心都是python -m venv venv建隔离环境第二步初始化数据库在项目根目录执行python sql/init_db.py该脚本读init_db.sql建表如果先用爬虫采集则会写入基础数据第三步启动 Flask 服务python app.py回车后看到默认端口提示第四步浏览器访问http://127.0.0.1:5000看到图表即跑通。# 建议在项目目录下执行 python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate pip install -r requirements.txt python sql/init_db.py # 建库建表并导入初始数据 python app.py # 启动服务默认 5000 端口这里解释一下为什么需要虚拟环境pandas 是一套依赖较多的数据科学库全局装容易和系统的其他 Python 包版本冲突用venv隔离后整套环境的依赖随便装看到不会影响其他项目Python 开发者通常默认这么做。init_db.py在和 root 启动时执行 create all所以跑完建库脚本后能连库确认一下movies表有没有数据再进前端页面不要省这一步。如果中途想重来最简单的方法是删除data/movies.db文件重新跑一次 init 脚本比写一个卸载脚本省事得多。这套系统的表结构简单库文件删了重建成本极低放心操作。5.2 接入数据与常见问题排查第一个坑跟着教程写了一遍爬虫能跑但数据库里「并没有数据」页面图表全是空白。现象控制台打印「已采集 50 条」但打开页面一张图都不显示。原因init_db.py建表成功后爬虫的数据清洗入库前可能因为年份字段全变成 0导致WHERE year BETWEEN 2000 AND 2024一张图都匹配不上。解决在管理端跑插件库连接工具查一句SELECT year, COUNT(*) FROM movies GROUP BY year;看到大量year 0的行就明白了——清洗层的正则抽取数字时抽不到内容。此时回到data_clean.py把year字段抽数年复式检查一遍处理完之后重复建库。第二个坑Flask 启动时提示Address already in use/ 端口被占用。现象python app.py报OSError: [Errno 98]或 5000 端口打不开网页。原因上一次运行的服务没有正常退出或系统中其他程序占用了 5000 端口。解决在命令行执行lsof -i :5000macOS/Linux或netstat -ano | findstr 5000Windows查出占用进程的 PID结束进程后重新启动或者改端口app.run(port5001)一劳永逸。这个坑在毕设答辩现场非常常见经常是前一个演示者的服务还挂在后台直接导致全场只能干瞪眼。第三个坑页面上有图表但所有中文标题显示为乱码。现象标题、图例、坐标轴显示「锟斤拷」或口口口。原因读取 CSV 时没指定编码Windows 系统里 pandas 默认以gbk解码utf-8文件乱码是解码错位。解决读文件统一写encodingutf-8-sig尤其在to_csv时也指定一遍。这个坑看起来小却是最耗时的中文乱码一旦出现表格数据、图表、导出报告全军覆没排查到编码问题前可能白白折腾半天。第四个坑容易忽略没有外网时 echarts 图表不渲染。现象页面骨架完整接口也返回了 JSON但五块图表区域空白。原因index.html里引用的是 CDN 的 echarts 库内网环境下脚本加载失败echarts.init报echarts is not defined。解决把项目的static/js/echarts.min.js放到本地引用并核对脚本路径顺手检查一下templates/index.html里script src{{ url_for(static, filenamejs/echarts.min.js) }}这种写法假如源码用静态路径就改成相对路径也行。这个坑是我最想提醒的作为侧重点讲给所有准备在教室现场演示的同学。第五个坑推荐关注接口超时或返回空列表。现象爬虫脚本执行时卡住无输出或列表页接口返回[]。原因目标页面改了数据结构或请求头不足被判断为无效请求。解决在crawl_movie_list里打印调试信息打印resp.status_code和返回的 JSON 片段检查 header 是否带了User-Agent和Referer页面跳转来源必要时增加Accept-Language请求头。由于这套源码采集的是公开接口数据短期内可用但你不能假设「永远能用」爬虫层本身就应该容忍失败并按批次重试。6. 二次开发实战给系统加一个「年度关键词画像」功能毕设做完了不代表就能交差导师大概率会问「数据做深一层你能挖出什么」所以最后一个章节给出一条完整的加功能路线在现有框架上加一个「按年份看类型关键词热度」的画像功能。它的价值在于接一个确实实用的模块覆盖从 SQL 统计到接口到图表的完整闭环。第一步先造数据。分析层的stats_builder.py里加一个函数按年份拆分类别、统计出现次数。这里不依赖复杂分词直接对genres的|分割做词频统计即可。def build_genre_yearly_profile(conn): df pd.read_sql_query(SELECT year, genres FROM movies, conn) rows [] for _, row in df.iterrows(): for g in str(row[genres]).split(|): rows.append({year: row[year], genre: g.strip()}) profile pd.DataFrame(rows).groupby([year, genre]).size().reset_index(namecnt) # 只保留每年 Top5 的类型词避免小屏上标签过多 top profile.sort_values(cnt, ascendingFalse).groupby(year).head(5) return top第二步在app.py加一个新路由GET /api/year_profile按同样的{categories, series}结构输出。前端的修改更简单复制 4.2 节的loadYearTrend把接口换成新地址把type: line改成type: bar或者type: scatter就能得到一张「某一年最热类型」的年度分布图。这样一次完整的「从 SQL 到接口到图表」的增量开发核心工作量大头只有 SQL 和前端展示剩下全是复制改参数。这种加功能的思路和这套源码本身的做法保持一致后端把统计负责到底前端专心画图。按照这个套路你可以继续加「评分与票数相关性分析」「地区平均评分年度对比」——每个都是一张新图对应一个新接口工程量可控也能在论文里多写一节「系统功能扩展」。走完这个流程我对任何数据处理类的毕设系统都会强制先画一遍「数据从哪来、存在哪、谁消费」的链路图链路画通了功能加起来就快很多。整个过程最值得保留的习惯有两个一是清洗层和数据入库层永远分离开别在写库的 SQL 里顺带改数据二是接口返回结构和 echarts 的 option 一一对应能极大降低前端对接成本。做这类项目的运行经验告诉我把这两点守住后续加功能就是复制粘贴的体力活。希望这套源码和这篇笔记能帮你在毕设路上少熬夜希望帮到你。本文还有配套的精品资源点击获取