Python豆瓣电影数据分析与可视化:Flask+ECharts完整实战

发布时间:2026/10/2 19:53:55
Python豆瓣电影数据分析与可视化:Flask+ECharts完整实战 简介这是一份面向Python学习者与毕业设计开发者的完整项目资源以豆瓣电影为数据源串联爬虫采集、Pandas清洗处理、Flask服务搭建及ECharts可视化展示全流程解决从零搭建数据可视化系统的常见问题。压缩包内共1054个文件其中688个py源码与157个pyc编译文件为系统核心配套15个html页面、11个js脚本及CSS样式实现前端交互另有SQL数据库脚本MySQL、docx论文文档及运行所需的exe、wheel依赖等整体约30.07MB。已有255人下载学习。资源包含完整可运行的FlaskECharts系统、数据库初始化脚本和论文文稿目录覆盖爬虫模块、数据处理、接口服务与可视化图表既可作为毕业设计直接参考也能帮助初学者掌握从数据获取到展示的完整工程思路适合进阶实践。1. 豆瓣电影数据分析与可视化这套源码包能让你少走一个月弯路做毕设选“基于 Python 的豆瓣电影数据分析及可视化系统”这个题目的同学不少但我见过太多人卡在同一个地方爬虫写好了数据却存不进去ECharts 图表出来了但跟数据库对不上Flask 路由跑通了页面却白屏。这套基于 Flask ECharts 爬虫 pandas 的完整项目包正好把这条链路一次性打通。它不是零散的代码片段而是从豆瓣 TOP250 等真实数据源出发经过 requests 爬取、SQLAlchemy 入库、pandas 清洗聚合再到 ECharts 渲染成柱状图、折线图和饼图的整套闭环同时附带了 SQL 脚本和毕业论文文档意味着你拿到的不是“能跑的函数”而是一个可以直接复现、可以答辩的毕设骨架。对比网上那些只有单个爬虫脚本或者只有前端页面的半成品这套资源的优势在于“全套可运行”——目录结构完整、数据流清晰、图表配置与后端接口一一对应。无论你是想快速交差还是想真正读懂每个模块的工作原理它都能帮你省下大量查资料和排错的时间。2. 项目结构与运行前置先搞清楚每个文件是干嘛的再动手不迟2.1 从解压到跑通目录结构与启动顺序很多同学拿到源码包第一件事就是双击 main.py结果要么缺少依赖要么数据库没导入导致页面直接 500。我的习惯是先把整个目录结构过一遍搞清楚每个文件在整条数据链路里的位置。以这套系统为例解压后你大概会看到这样的组织方式douban_movie_analysis/ ├── app.py # Flask 主入口路由与接口定义 ├── models.py # SQLAlchemy ORM 模型对应数据库表结构 ├── spider/ │ ├── douban_spider.py # requests 爬虫模块负责抓取豆瓣电影数据 │ └── data_clean.py # pandas 清洗与聚合逻辑 ├── templates/ │ └── index.html # 前端页面ECharts 图表容器 ├── static/ │ ├── js/ │ │ ├── echarts.min.js # ECharts 库文件 │ │ └── main.js # 图表初始化和数据请求逻辑 │ └── css/ ├── sql/ │ └── douban_movie.sql # 数据库初始化脚本 └── 论文/ └── 毕业论文.docx # 可直接根据实际项目修改的论文模板这个结构本身就是一个标准的 Flask 分层项目app.py负责路由和接口models.py负责数据库映射spider目录把爬虫和数据处理拆开templates和static分别管理页面结构与资源文件。启动顺序建议先建数据库再启动 Flask否则models.py里 ORM 映射的表在数据库中不存在接口一调用就会报错。第一步是导入 SQL 脚本。用 Navicat 或者命令行执行douban_movie.sql它会帮你建好数据库和所有表结构包括电影基本信息表、评分分布表、类型统计表等。第二步是安装依赖我一般会先建一个独立的虚拟环境避免和系统 Python 环境打架。执行pip install flask flask-sqlalchemy pandas requests pymysql这一串命令即可版本上不需要刻意追求最新Flask 2.x 配 SQLAlchemy 2.x 在这套代码上跑得就很稳。2.2 数据库配置与 SQLAlchemy 连接参数这套系统的数据库连接用的是 SQLAlchemy连接字符串写在app.py或者models.py的顶部核心就是这一行# 数据库连接配置用户名/密码/主机/端口/库名按自己本地环境改 app.config[SQLALCHEMY_DATABASE_URI] mysqlpymysql://root:123456localhost:3306/douban_movie?charsetutf8 app.config[SQLALCHEMY_TRACK_MODIFICATIONS] False db SQLAlchemy(app)这里mysqlpymysql是驱动格式root:123456是数据库的用户名和密码localhost:3306是本机 MySQL 的默认地址douban_movie是刚才 SQL 脚本建好的库名最后的charsetutf8必须带上否则中文数据写进去会乱码。我见过不少同学把注意力全放在爬虫代码上结果程序卡在这里——连接超时、密码不对、库不存在统统都是这个字符串的问题。改好配置后启动 Flask 项目进入虚拟环境执行python app.py终端会显示Running on http://127.0.0.1:5000。用浏览器打开这个地址如果页面能正常渲染出图表框架说明前后端已经打通接下来就看数据是从爬虫抓进来的还是从 SQL 脚本预先导入的。3. 爬虫模块拆解豆瓣数据是怎么进到 MySQL 的3.1 requests 爬虫的核心逻辑与反爬应对豆瓣电影的数据来源主要是 TOP250 榜单和分类检索结果这套系统里爬虫模块写得很典型——用requests拿 HTML用正则或解析库提取字段。爬虫部分的核心代码长这样import requests import re import time from models import Movie, db # 请求头必须模拟真实浏览器否则豆瓣直接返回 418 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: https://movie.douban.com/ } def fetch_movie_list(page_start): 抓取豆瓣电影某一页的数据返回原始 HTML url fhttps://movie.douban.com/top250?start{page_start}filter try: resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() resp.encoding utf-8 return resp.text except requests.exceptions.RequestException as e: print(f[ERROR] 请求失败: {e}, URL: {url}) return None def parse_movie_items(html): 从 HTML 中提取电影标题、评分、评价人数、类型等信息 pattern re.compile( rspan classtitle(.*?)/span.*? rspan classrating_num.*?(.*?)/span.*? rspan(.*?)人评价/span, re.S ) return re.findall(pattern, html)这段代码有三个细节需要说明。第一headers里的User-Agent一定要保持浏览器特征删掉它豆瓣会直接拒绝请求。第二resp.encoding utf-8是显式指定解析编码豆瓣页面本身是 UTF-8 编码但 requests 有时会根据响应头猜测编码猜错就会出现中文乱码。第三parse_movie_items里的正则表达式用了re.S标志让.能匹配换行符因为 HTML 标签之间往往有换行和空格不加这个标志正则匹配会失败。爬虫模块里还会定义save_to_db之类的函数把解析出来的数据逐条插入 MySQL。这里有个常见做法是每次插入前先查询数据库里是否已存在同一部电影如果存在就跳过这样重复跑爬虫不会产生重复数据。爬虫爬完一页之后代码里会调用time.sleep(3)暂停 3 秒这个延迟是我强烈建议保留的——豆瓣对请求频率很敏感爬太快 IP 会被临时封禁。3.2 CSV 中间文件与 pandas 入库的两种方式这套系统里爬虫数据有两种入库方式一种是从爬虫直接写入 MySQL另一种是先把爬到的数据存成 CSV再用 pandas 清洗后入库。第二种方式在毕设里更好讲因为它的数据链路更清晰爬虫 → CSV → pandas → MySQL → ECharts。import pandas as pd from sqlalchemy import create_engine # 读取爬虫落地的 CSV 文件 df pd.read_csv(douban_movies.csv, encodingutf-8-sig) # 数据清洗去重、填充缺失值、转换字段类型 df df.drop_duplicates(subsetmovie_title) df df.dropna(subset[movie_title, rating_score]) df[rating_score] df[rating_score].astype(float) df[vote_count] df[vote_count].astype(int) # 通过 SQLAlchemy 引擎批量写入 MySQL engine create_engine(mysqlpymysql://root:123456localhost:3306/douban_movie?charsetutf8) df.to_sql(movie_info, conengine, if_existsappend, indexFalse)encodingutf-8-sig这个参数容易被忽略它的作用是读取带 BOM 的 UTF-8 文件用普通utf-8读取会把 BOM 头当成不可见字符拼到第一列列名上后面查询时列名对不上就报错。drop_duplicates(subsetmovie_title)按电影标题去重防止爬虫重复数据污染分析结果。astype(float)和astype(int)分别把评分和票数转成正确的数值类型因为从 CSV 读进来的都是字符串。如果你选择直接让爬虫写数据库那SQLAlchemy的事务控制就要注意——大批量插入时建议分批提交比如每 50 条db.session.commit()一次。这样即使中途断网或遇到反爬封 IP已提交的数据不会丢失再次运行时通过查重也能跳过已入库的记录。4. Flask 后端与接口设计数据是怎么从 MySQL 传到 ECharts 的4.1 路由与 JSON 接口的返回格式约定Flask 后端在这套系统里的职责很纯粹把 MySQL 里的数据查出来按 ECharts 需要的格式打包成 JSON 返回给前端。app.py里的路由设计通常是一张图表对应一个接口比如评分分布图对应/api/rating_distribution类型占比图对应/api/genre_distribution年度趋势图对应/api/year_trend。from flask import Flask, jsonify, render_template from models import Movie, db app Flask(__name__) app.route(/) def index(): 渲染主页面 return render_template(index.html) app.route(/api/rating_distribution) def rating_distribution(): 返回评分分布数据x轴是评分区间y轴是电影数量 # SQL 按评分四舍五入后分组统计 results db.session.query( db.func.round(Movie.rating_score).label(rating), db.func.count(Movie.id) ).group_by(rating).all() data [ {rating: int(rating), count: count} for rating, count in results ] return jsonify({code: 200, data: data})接口返回的 JSON 格式里code字段是给前端判断请求是否成功用的data字段才是图表要的数据本体。这个约定要和前端main.js里的处理逻辑保持一致否则后端返回结构变了前端拿不到数据图表就空白。我见过不少项目前后端各写各的后端返回{status: 1}前端却用code 200判断这种低级不一致排查起来特别费时间。这个接口的逻辑本质上就是一条分组查询语句db.func.round是 SQLAlchemy 对 MySQLROUND()函数的封装把评分小数位抹平后按整数值分组统计电影的分布情况。db.func.count则是对每组内的电影数量计数。拿到结果后遍历组装成字典列表jsonify自动转成 JSON 字符串返回前端fetch或者axios发起请求后就能拿到数组。4.2 图表联动同一个数据源支撑多个 ECharts 图表的思路这套系统里最有看头的设计是“一数多用”——同一份 MySQL 数据通过不同的聚合维度生成多张图表。年度趋势图按年份分组求平均分类型占比图按类型字段拆分计数评分分布图按分数段统计频次。这三张图表虽然长得完全不一样但底层都是从movie_info这张表来的。app.route(/api/year_trend) def year_trend(): 返回年度上映电影数量与平均评分折线图用 results db.session.query( db.func.year(Movie.publish_year).label(year), db.func.count(Movie.id).label(movie_count), db.func.avg(Movie.rating_score).label(avg_rating) ).group_by(year).order_by(year).all() years [str(r.year) for r in results] counts [r.movie_count for r in results] ratings [round(r.avg_rating, 1) for r in results] return jsonify({ code: 200, data: { years: years, counts: counts, ratings: ratings } })这里有个细节值得注意前端折线图通常需要两条线——一条是电影数量一条是平均评分但两条线的数据量级完全不同。数量可能是几十到几百评分却只有 6~9如果直接画在一个坐标系里评分线会被压成一条直线。常见的解决方案是使用 ECharts 的双 Y 轴配置左侧 Y 轴对应电影数量右侧 Y 轴对应平均评分。这个配置我在后面前端部分会详细展开这里的接口设计配合双 Y 轴是整套系统里最完整的展示效果。后端接口写完用浏览器直接访问http://127.0.0.1:5000/api/rating_distribution如果能返回 JSON 数据说明查询逻辑没问题接下来所有工作量都集中在前端图表呈现上。5. pandas 数据分析与 ECharts 可视化从数据清洗到图表配置5.1 pandas 数据清洗类型转换、去重、缺失值处理pandas 在这套系统里承担的是数据预处理和分析计算的职责。虽然 MySQL 里已经存了一份原始数据但直接拿来画图往往不行——评分字段可能是字符串、类型字段可能有多值逗号分隔、年份可能有缺失。这套系统里建议把清洗逻辑集中在data_clean.py里代码结构大致如下import pandas as pd import re def clean_movie_data(df): 统一清洗入口处理缺失、类型转换、字段拆分 # 1. 去掉完全空白的行 df df.dropna(howall) # 2. 评分转浮点无法转换的置为 NaN 再删掉 df[rating_score] pd.to_numeric(df[rating_score], errorscoerce) df df.dropna(subset[rating_score]) # 3. 年份字段只保留数字部分比如 1994(中国大陆) 提取成 1994 df[publish_year] df[publish_year].apply( lambda x: re.search(r\d{4}, str(x)).group(0) if re.search(r\d{4}, str(x)) else None ) df df.dropna(subset[publish_year]) df[publish_year] df[publish_year].astype(int) # 4. 类型字段以 / 分隔拆成列表方便后面做扁平化统计 df[genres] df[genres].apply(lambda x: [g.strip() for g in str(x).split(/)]) return dfpd.to_numeric(errorscoerce)是处理脏数据的利器字符串转数字失败时不会抛异常而是置为 NaN后面用dropna统一删除。年份字段用正则提取四位数字这一步是专门处理豆瓣页面里“1994(中国大陆)”这种格式的直接astype(int)会报错提取后就能转成整数。类型字段拆成列表后方便后面用explode()做扁平化统计——每部电影的多个类型各占一行然后groupby(genres)计数就是类型的分布了。做完清洗后可以用一行df.describe()快速检查数据的均值、标准差和分位数确认数据没清洗出错。这一步在论文里也可以截图展示说明你做了严谨的数据预处理。5.2 ECharts 配置实战柱状图、折线图、饼图的参数对照前端main.js通过fetch请求后端接口拿到数据后用 ECharts 渲染图表。这套系统里三张核心图表的配置方式很典型柱状图看评分分布、折线图看年度趋势、饼图看类型占比。柱状图的配置核心是xAxis和yAxis的数据映射// 评分分布柱状图 fetch(/api/rating_distribution) .then(res res.json()) .then(data { const chartData data.data; const chart echarts.init(document.getElementById(ratingChart)); chart.setOption({ title: { text: 豆瓣电影评分分布, left: center }, tooltip: { trigger: axis }, xAxis: { type: category, data: chartData.map(item item.rating 分) }, yAxis: { type: value, name: 电影数量 }, series: [{ name: 数量, type: bar, data: chartData.map(item item.count), barWidth: 40%, itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: #ff7e5f }, { offset: 1, color: #feb47b } ]) } }] }); });柱状图的barWidth设置为40%是为了避免柱子太粗显得笨重itemStyle.color用LinearGradient渐变填充是让图表更美观但这只是视觉层面的改动不影响数据准确性。tooltip.trigger: axis表示鼠标悬停时显示坐标轴提示这是柱状图和折线图通用的交互配置。折线图的配置和柱状图类似区别在于type: line以及双 Y 轴设置。年度趋势图需要用到 ECharts 的yAxis数组形式yAxis: [ { type: value, name: 电影数量, position: left }, { type: value, name: 平均评分, position: right, min: 0, max: 10 } ], series: [ { name: 电影数量, type: line, data: data.counts, smooth: true }, { name: 平均评分, type: line, yAxisIndex: 1, data: data.ratings, smooth: true } ]yAxisIndex: 1把评分系列绑定到右侧 Y 轴min: 0, max: 10限制评分轴的显示范围这样评分线的波动会显得明显而不是一条直线。饼图则相对简单type: pieradius: [30%, 60%]设置内径和外径形成环形效果roseType: radius可以开启南丁格尔玫瑰图模式让占比小的类型在视觉上也能看清。6. 避坑指南豆瓣数据分析和 Flask 可视化最常见的六个坑6.1 爬虫请求被拒或抓不到数据爬虫运行时requests报418错误码或者返回的 HTML 里没有电影条目数据。原因基本是请求头不够完整豆瓣的反爬机制会校验User-Agent和Referer裸请求直接拒绝。解决方法是把请求头补全User-Agent用最新 Chrome 的完整标识Referer设置为https://movie.douban.com/同时增加time.sleep(3)的延迟。如果已经用了完整请求头还是被拒检查是不是请求频率太高豆瓣针对单个 IP 的分钟级请求数量有限制把time.sleep(3)改成time.sleep(5)或者换代理源。6.2 SQLAlchemy 插入数据报编码错误pymysql执行INSERT时提示Incorrect string value数据库表里的中文内容显示乱码。原因是数据库表或连接字符串没指定 UTF-8 编码。解决方法分两步连接字符串里补上?charsetutf8并在建库时执行CREATE DATABASE douban_movie CHARACTER SET utf8mb4。utf8mb4 比 utf8 更完整支持生僻字和 emoji如果 SQL 脚本里建表语句没有显式指定字符集建议手动改一下。6.3 Flask 启动后浏览器打不开页面python app.py后终端显示Running on http://127.0.0.1:5000但浏览器访问超时或无响应。原因大多是 Flask 默认监听地址问题或者 5000 端口被占用。解决方法是检查app.run()参数改成app.run(host127.0.0.1, port5000, debugTrue)。端口被占用时换一个端口比如port5001同时修改前端main.js里所有接口请求的 URL 前缀。6.4 ECharts 图表不显示但页面无报错浏览器控制台没有 JS 报错但图表区域是空白的。原因一般是容器div没有明确高度ECharts 初始化时找不到可用的渲染区域。解决方法是给放置图表的div设置固定高度比如styleheight: 500px; width: 100%。另一个常见原因是echarts.init()在 DOM 元素渲染之前执行把初始化代码放到window.onload回调里。6.5 CSV 导入数据库后中文乱码pandas 的read_csv读出来中文正常但写入 MySQL 后中文变乱码。原因是连接字符串缺少charsetutf8这和 6.2 是同源问题但很多人单独写爬虫脚本时不带连接串直接在to_sql里传引擎就会忽略编码。解决方法是把create_engine的连接串写完整文件读写时统一用utf-8-sig编码。6.6 接口返回的 JSON 数据结构与前端对不上浏览器直接访问接口 URL 有数据但页面上图表不更新。原因是接口返回的数据结构里多了code字段前端用data.data但实际返回是{code: 200, data: [...]}或者字段名不一致——后端叫count前端取value。解决方法是打开浏览器开发者工具里的 Network 面板查看接口实际返回的 JSON 格式比照它去调整前端的.map()取值逻辑不要凭记忆写。7. 论文与答辩准备把项目讲清楚比把项目跑起来更重要毕设环节里代码能跑只是第一步论文和答辩才是决定分数的地方。这套资源附带的论文文档是一份极好的起点但直接交原版肯定不行要改造成自己的东西。我的建议是以论文为基础把每一章的论述和你的实际代码对应起来系统设计那章写数据库表结构和 Flask 路由设计关键技术那章写 requests 爬虫的正则提取和 pandas 清洗思路系统实现那章放截图——主页面、评分分布柱状图、年度趋势折线图、类型占比饼图各一张。答辩时老师最常问的问题有两个。第一个是“数据怎么保证真实性”你就讲爬虫抓取的是豆瓣公开页面数据入库前经过 pandas 清洗和去重数据库里存的是本地快照不是实时数据。第二个是“系统有什么可扩展的地方”不要回答“代码写得好”而要具体说“目前只爬了 TOP250 约 250 条数据如果要扩展到全站爬虫部分需要加入代理池和分布式调度前端可以增加电影详情弹窗和关键词搜索功能”。论文的章节结构上我见过不少拿这套资源做毕设的同学最稳妥的安排是第一章绪论写背景和意义第二章相关技术写 Flask、ECharts、pandas、爬虫的简介每种技术两三页即可第三章系统分析写需求和功能模块划分第四章系统设计写数据库表结构和接口设计第五章系统实现配合截图详细描述每个模块的代码逻辑第六章测试与总结。每一章的篇幅不需要刻意拉长答辩老师更在意的是你能否讲清楚每个模块的数据流转过程——爬虫从豆瓣拿到什么、pandas 处理了什么、接口返回了什么、图表展示了什么。我自己的习惯是拿到任何一套源码后先按自己的理解重新写一遍核心的爬虫清洗和接口逻辑哪怕最终功能和原版完全一样也坚持重写。重写过程中踩过的每一个坑都会变成答辩时你说得最自然的细节比如豆瓣反爬的User-Agent设置、CSV 读取时的 BOM 编码问题、折线图评分线被压扁需要双 Y 轴——这些细节比任何宏大的技术描述都更能说明你是真的做过这个项目的人。从那以后我每次拿到新的毕设项目源码都强制自己走一遍“读懂数据流 → 手动重写核心逻辑 → 复现运行 → 补充自己的功能”这个过程这次拆解豆瓣电影系统也不例外。这套资源完整度确实够高省去了很多从零搭框架的时间希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询