☰
Python豆瓣电影数据分析与可视化:Flask+ECharts完整实战
2026/10/2 19:53:55 网站建设 项目流程

简介:这是一份面向Python学习者与毕业设计开发者的完整项目资源,以豆瓣电影为数据源,串联爬虫采集、Pandas清洗处理、Flask服务搭建及ECharts可视化展示全流程,解决从零搭建数据可视化系统的常见问题。压缩包内共1054个文件,其中688个py源码与157个pyc编译文件为系统核心,配套15个html页面、11个js脚本及CSS样式实现前端交互,另有SQL数据库脚本(MySQL)、docx论文文档及运行所需的exe、wheel依赖等,整体约30.07MB。已有255人下载学习。资源包含完整可运行的Flask+ECharts系统、数据库初始化脚本和论文文稿,目录覆盖爬虫模块、数据处理、接口服务与可视化图表,既可作为毕业设计直接参考,也能帮助初学者掌握从数据获取到展示的完整工程思路,适合进阶实践。

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'] = 'mysql+pymysql://root:123456@localhost:3306/douban_movie?charset=utf8' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = SQLAlchemy(app)

这里mysql+pymysql是驱动格式,root:123456是数据库的用户名和密码,localhost:3306是本机 MySQL 的默认地址,douban_movie是刚才 SQL 脚本建好的库名,最后的charset=utf8必须带上,否则中文数据写进去会乱码。我见过不少同学把注意力全放在爬虫代码上,结果程序卡在这里——连接超时、密码不对、库不存在,统统都是这个字符串的问题。

改好配置后启动 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 = f'https://movie.douban.com/top250?start={page_start}&filter=' try: resp = requests.get(url, headers=headers, timeout=10) 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( r'<span class="title">(.*?)</span>.*?' r'<span class="rating_num".*?>(.*?)</span>.*?' r'<span>(.*?)人评价</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', encoding='utf-8-sig') # 数据清洗:去重、填充缺失值、转换字段类型 df = df.drop_duplicates(subset='movie_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('mysql+pymysql://root:123456@localhost:3306/douban_movie?charset=utf8') df.to_sql('movie_info', con=engine, if_exists='append', index=False)

encoding='utf-8-sig'这个参数容易被忽略,它的作用是读取带 BOM 的 UTF-8 文件,用普通utf-8读取会把 BOM 头当成不可见字符拼到第一列列名上,后面查询时列名对不上就报错。drop_duplicates(subset='movie_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(how='all') # 2. 评分转浮点,无法转换的置为 NaN 再删掉 df['rating_score'] = pd.to_numeric(df['rating_score'], errors='coerce') 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 df

pd.to_numeric(errors='coerce')是处理脏数据的利器,字符串转数字失败时不会抛异常而是置为 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: 'pie',radius: ['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 编码。解决方法分两步:连接字符串里补上?charset=utf8,并在建库时执行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(host='127.0.0.1', port=5000, debug=True)。端口被占用时换一个端口,比如port=5001,同时修改前端main.js里所有接口请求的 URL 前缀。

6.4 ECharts 图表不显示但页面无报错

浏览器控制台没有 JS 报错,但图表区域是空白的。

原因一般是容器div没有明确高度,ECharts 初始化时找不到可用的渲染区域。解决方法是给放置图表的div设置固定高度,比如style="height: 500px; width: 100%"。另一个常见原因是echarts.init()在 DOM 元素渲染之前执行,把初始化代码放到window.onload回调里。

6.5 CSV 导入数据库后中文乱码

pandas 的read_csv读出来中文正常,但写入 MySQL 后中文变乱码。

原因是连接字符串缺少charset=utf8,这和 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 轴——这些细节比任何宏大的技术描述都更能说明你是真的做过这个项目的人。从那以后我每次拿到新的毕设项目源码,都强制自己走一遍“读懂数据流 → 手动重写核心逻辑 → 复现运行 → 补充自己的功能”这个过程,这次拆解豆瓣电影系统也不例外。这套资源完整度确实够高,省去了很多从零搭框架的时间,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询