基于Python的豆瓣电影数据可视化分析项目实战解析 豆瓣电影的数据可视化分析是我接触过的 Python 入门项目里性价比最高的一个。市面上太多教程停在打印 Hello World而这个项目从爬虫、数据清洗、可视化到部署一条线走通正好覆盖了数据分析的核心链路也适合作为课程设计或者毕设的完整题目。做这个项目最舒服的一点是数据源现成、回报反馈快爬到几十部电影就能画出几张像样的图特别容易建立起“我好像真的会数据分析”的信心。我今天把从 0 到 1 做这套项目的完整思路记录下来包含源码结构、关键代码、部署文档的写法和几个我实际调试时踩过的坑希望你能少走一点弯路。1. 项目整体设计先想清楚再动手拿到一个“基于 Python 豆瓣电影数据可视化分析设计与实现”的题目第一反应不应该是直接写爬虫而是先把需求拆开。很多人做这类项目容易做成“爬了一堆数据最后发现没分析出什么”。为了避免这种尴尬我建议你先在脑子里回答三个问题数据从哪来、要分析什么、结果给谁看。1.1 项目目标与数据范围我当时的做法是聚焦豆瓣电影 Top250再去补充一部分短评文本。为什么要选 Top250因为它的页面结构稳定、数据字段齐全、反爬压力相对可控而且 Top250 本身就是一个“被大众认可的高分电影榜单”很适合做各类对比分析。分析目标可以定为四个方向评分分布Top250 的评分是不是普遍集中在 8.5 到 9 分之间年份与评分电影上映年份和评分之间有没有规律不同年代的高分电影数量变化类型分布哪个类型的电影更容易进 Top250短评关键词抓取经典台词或短评做词云直观呈现观众关注点。数据量不需要大Top250 共十个页面每页 25 个条目加上短评也就是几百条文本。这个量级用 requests 加 BeautifulSoup 完全够不需要上 Scrapy 框架。1.2 为什么选这个技术栈数据分析主流程我用的“requests BeautifulSoup pandas Matplotlib/Seaborn”最后加了一层 Flask 做展示。这套组合最大的优点是安装简单、学习曲线平缓。requests 发请求BeautifulSoup 解析 HTMLpandas 做清洗和聚合Matplotlib 和 Seaborn 画静态图Flask 把它们串成一个可演示的网页。如果你想把图表做得更炫可以换成 Pyecharts 生成交互图我后面会讲怎么嵌入。不建议一开始就用 Scrapy 和 MySQL。Scrapy 对单次几百条的数据来说太重了MySQL 也完全可以被 CSV 替代。CSV 是文本文件pandas 直接读不需要额外配置数据库服务这对交作业或者演示来说省事得多。1.3 这套源码能让你学到什么如果拆成技能点这个项目至少覆盖Python 爬虫请求与解析、正则提取、pandas 常用操作、数据可视化的基础图表、Web 框架的简单集成、Linux 服务器部署。这些内容恰好是很多招聘 JD 里“数据分析”和“后端开发”共同的底层要求。做完之后你再去学 Django、爬虫框架都会觉得轻松很多。2. 第 1 步把数据拿到手爬虫模块的设计与实现爬虫是这个项目的数据来源也是第一个容易卡住的环节。豆瓣对自动爬取有一定防护最常见的就是请求头校验和访问频率限制。很多新手一上来就写并发结果第二个请求就返回 418 或者验证码页面。这里我的建议很简单慢一点稳一点就把数据当做一个普通用户手动浏览的速度去拿。2.1 先分析页面结构打开豆瓣电影 Top250 页面用浏览器开发者工具选中一部电影的标题然后在右侧“元素”面板里往上找会看到一个 class 包含item的div每一部电影都放在这样的条目里。标题在span.title里评分在span.rating_num里评价人数在span.star下的span.pl里经典台词在span.inq里影片信息一行在p里。这些结构信息搞定后解析代码就很好写了。我写了一个简单的解析函数专门处理单个页面import requests 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.0.0 Safari/537.36, Referer: https://movie.douban.com/, } def parse_page(html): soup BeautifulSoup(html, html.parser) rows soup.select(#content .item) movies [] for item in rows: title_tag item.select_one(.title) rating_tag item.select_one(.rating_num) people_tag item.select_one(.star .pl) quote_tag item.select_one(.quote .inq) info_tag item.select_one(p) movies.append({ title: title_tag.get_text(stripTrue) if title_tag else , rating: rating_tag.get_text(stripTrue) if rating_tag else , people: people_tag.get_text(stripTrue) if people_tag else , quote: quote_tag.get_text(stripTrue) if quote_tag else , info: info_tag.get_text( , stripTrue) if info_tag else , }) return movies有人会问为什么不用正则因为 BeautifulSoup 的 CSS 选择器更容易看懂也更好维护。考虑到你后续要清洗字段解析结果直接按字典列表保存转成 DataFrame 非常方便。2.2 请求细节与反爬应对爬虫能不能稳定跑完关键在请求头。豆瓣会校验 User-Agent如果你的请求默认是python-requests很容易被识别。上面代码里我特意伪造了一个浏览器的 User-Agent并且加了 Referer。这里还有一个容易忽略的点请求频率。最简单的做法是每请求一页睡两秒import time def crawl_top250(): all_movies [] for start in range(0, 250, 25): url fhttps://movie.douban.com/top250?start{start} resp requests.get(url, headersHEADERS, timeout8) if resp.status_code 200: movies parse_page(resp.text) all_movies.extend(movies) print(f已抓取第 {start // 25 1} 页共 {len(all_movies)} 条) time.sleep(2) return all_movies注意time.sleep(2)不是随便写的。如果你只爬十页一秒钟太快三秒又太慢两秒是个比较平衡的值。如果你还是收到 418最直接的办法是登录豆瓣网页版把请求头里的 Cookie 复制下来加到 HEADERS 里。这个操作我实测后成功率非常高而且不需要登录状态保持太久因为 Top250 公开数据并不强制要求登录但带 Cookie 后请求头更像真实用户。2.3 数据入库CSV 还是数据库清洗前的原始数据我建议存成 CSV而且一定用utf-8-sig编码。直接to_csv(movies.csv, indexFalse)默认是utf-8Windows 下 Excel 打开会乱码。utf-8-sig会在文件头加 BOMExcel 就能正确识别中文。import pandas as pd def save_movies(movies): df pd.DataFrame(movies) df.to_csv(data/movies_raw.csv, indexFalse, encodingutf-8-sig)CSV 的另一个好处是方便后续排查。如果你发现某一行数据没抓全直接打开文件看比连数据库查方便得多。等数据清洗真正跑通之后如果想迁移到 MySQLpandas 一行to_sql也能搞定所以不用急着上数据库。3. 脏数据才是常态数据清洗与标准化处理爬虫抓下来的数据一定不会像教程里那么干净。最常见的问题是某部电影没有经典台词quote字段为空年份混在“导演、编剧、主演”等信息里评价人数带着“人评价”这样的单位类型字段是“剧情 / 爱情”这种用斜杠隔开的多值结构。这些不处理后面的图表全都会出问题。3.1 先看一眼数据长什么样拿到 CSV 后第一件事不是画图而是用 pandas 把数据结构和缺失情况打出来import pandas as pd df pd.read_csv(data/movies_raw.csv, encodingutf-8-sig) print(df.info()) print(df.head())info()会显示每列的非空数量。如果quote列非空数量明显少于总行数说明确实有空值。head()能看文本格式比如info列是不是把年份、地区、类型都挤在了一行里。3.2 去重、缺失值处理与类型转换解析页面时出现问题会导致重复行比如页面结构变化导致解析结果重复或者程序中断后重复执行了爬虫。先用drop_duplicates去重再处理缺失值。df df.drop_duplicates(subset[title], keepfirst) df[rating] pd.to_numeric(df[rating], errorscoerce) df df.dropna(subset[rating])pd.to_numeric遇到空字符串或无法转数字的内容会变成 NaNerrorscoerce就是为了让异常值不报错。这样处理之后评分列才真正是数字类型后面才能算平均值、画直方图。如果一列数据缺失率太高比如超过 30%我一般会考虑放弃这个字段否则填充出来的值反而会影响分析结果。年份信息藏在info字段里典型格式是“1994 / 中国大陆 / 剧情 / 犯罪”。这里有两种处理方式一是按斜杠分隔取第一个数字片段二是用正则提取四位数年份。我建议用正则因为年份一定会是四位数字不容易被其他字段干扰。df[year] df[info].str.extract(r(19\d{2}|20\d{2})) df[year] pd.to_numeric(df[year], errorscoerce)这样得到的year列可以直接用来做年份维度分析。3.3 切分多值字段为可视化准备数据电影类型通常不止一个比如“剧情 / 爱情 / 喜剧”。如果直接按整串去统计会出现几十种组合没法看。正确的做法是把每种类型拆开然后转成一列一行df[genres] df[info].str.extract(r(?:.*?/ )?(?Pg1[^/])(?: / (?Pg2[^/]))?(?: / (?Pg3[^/]))?)这种正则容易把人绕晕更直观的写法是先把info按斜杠切分再新建一个包含所有类型的数组def split_genres(text): parts str(text).split(/) parts [p.strip() for p in parts if p.strip()] # 年份通常在第一段类型一般在最后几段 genres [p for p in parts if not p.isdigit()] return genres df[genres_list] df[info].apply(split_genres) df_type df.explode(genres_list)explode会把genres_list里的每一项展开成一行一个多类型电影就变成了多行每行对应一种类型这样value_counts()就能算出每种类型出现的次数。这个操作很实用之后做类型柱状图时直接你只需要整理好的长表。4. 把图表画好可视化分析的设计与代码数据清洗完之后真正出彩的部分就是可视化。很多人以为图表只是把数据换个样子其实在图表的背后你要先想清楚想看什么结论。我一般按“分布、趋势、占比、文本”四个角度来选择图表类型。4.1 评分分布直方图与 KDE 密度图评分分布用直方图最直观。每一次画图之前先设置 Matplotlib 的中文字体和图片尺寸import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False第一行解决中文乱码第二行解决负号显示成方块的问题。如果你在 Mac 上运行把SimHei换成Arial Unicode MS或者PingFang SC也行。接着画评分分布import seaborn as sns sns.set_theme(stylewhitegrid) sns.histplot(df[rating], bins15, kdeTrue, color#2f7f93) plt.title(豆瓣电影 Top250 评分分布) plt.xlabel(评分) plt.ylabel(电影数量) plt.savefig(output/rating_dist.png, dpi200, bbox_inchestight)实际跑完之后你会发现大部分电影集中在 8.5 到 9.0 分之间这很符合预期因为 Top250 本身就是优中选优的榜单。这个图放出去比单纯说“平均分 8.8”要有说服力得多。4.2 年份与评分关系趋势图与散点图要回答“高分电影是否集中在某个年代”最简单的做法是按年份分组算平均分year_rating df.groupby(year)[rating].mean().dropna() fig, ax plt.subplots(figsize(10, 5)) ax.plot(year_rating.index, year_rating.values, markero, linewidth1.5, color#c45a4a) ax.set_xlabel(上映年份) ax.set_ylabel(平均评分) ax.set_title(各上映年份电影平均评分变化) plt.xticks(rotation45) plt.tight_layout() plt.savefig(output/year_rating.png, dpi200)这里我特意把year作为索引因为折线图的横轴必须是有序的数值型数据。如果年份列是字符串画出来的横轴顺序会混乱。如果你觉得折线波动太大可以改成滚动平均把前后三年数据取平均值曲线会平滑很多。这个细节在报告里写出来会显得你分析问题有深度。4.3 类型分布柱状图与占比类型分布我用的是清洗后的df_type。由于很多电影属于多种类型统计出现次数时要把所有类型都算进去type_counts df_type[genres_list].value_counts().head(10) plt.figure(figsize(10, 6)) type_counts.plot(kindbar, color#6a9fb5) plt.title(豆瓣电影 Top250 类型出现次数 TOP10) plt.xlabel(电影类型) plt.ylabel(出现次数) plt.xticks(rotation30) plt.tight_layout() plt.savefig(output/genre_bar.png, dpi200)柱状图适合看排名饼图适合看占比。不过如果类型超过十个饼图就会挤成一团所以我的习惯是只对前五类型画饼图其他类型归为“其他”。这些都是画图前就应该想好的细节。4.4 词云对短评做高频词分析短评文本和经典台词很适合用词云展示。核心步骤是分词、加载中文停用词、生成词云。from wordcloud import WordCloud import jieba text .join(df[quote].fillna().tolist()) words .join(jieba.lcut(text)) wc WordCloud( font_pathC:/Windows/Fonts/simhei.ttf, width800, height600, max_words200, background_colorwhite, ) wc.generate(words) wc.to_file(output/quote_wordcloud.png)jieba.lcut是分词中文如果不分词词云就会拼接成乱七八糟的整句话。font_path必须指向系统中文字体文件Windows 一般是simhei.ttfMac 可以是/System/Library/Fonts/PingFang.ttc。生成词云以后你会发现里面会有人名、电影名比如“王家卫”“阿甘”“人生”这类高频词说明这些是观众评价电影时最常提及的关键点。4.5 让图表更“能看”的通用参数如果你想让图表更像一份“作品”而不是默认样式的示例记住这几个参数figsize设定图幅比例适合报告排版dpi分辨率交给项目时建议 200bbox_inchestight保存时自动裁掉多余空白避免图片周围一圈白边color整个项目的图表尽量统一色调不要每张图都换一个彩虹色title和轴标签一定要写清楚没有标题的图等于白做。这些参数看起来不起眼但评审老师或者读者看到你的图第一眼就会对你的认真程度有个判断。5. 从静态图到交互看板基于 Flask 的展示系统做数据分析项目如果最后只给一张张 PNG 图片显得有点单薄。我建议加一层 Flask把图表动态渲染到网页上这样既能演示也能让项目结构更完整。Flask 的特点是小而简单非常适合这种轻量展示。5.1 为什么建议加一层 Web 展示课程设计和毕业设计答辩时演示环节最怕就是现场打开 Jupyter 一个个跑单元格。用 Flask 提前把图和表格集成到一个页面里观众自己打开浏览器就能看到结果观感完全不同。而且 Flask 嵌入图表的方式很成熟Pyecharts 可以直接渲染成 HTML 片段Flask 模板再把片段嵌进页面。如果你不想学 Flask也可以直接写一个静态 HTML把图片路径用img标签放进去。但这样更新数据后要手动换图不够自动化。用 Flask 动态渲染每次运行都会读取最新 CSV数据更新后图表自动刷新。5.2 项目目录与代码结构我推荐的项目目录结构如下movie_project/ ├── data/ │ └── movies_clean.csv ├── output/ │ ├── rating_dist.png │ ├── year_rating.png │ └── genre_bar.png ├── templates/ │ └── index.html ├── app.py ├── requirements.txt └── README.mddata放清洗后的数据output放静态图templates放 Flask 模板app.py是整个 Web 应用的入口。如果你用 Pyecharts它的图表也是拼成 HTML 字符串后放进模板所以目录结构不变。5.3 关键代码Flask 渲染图表下面是一段最简单的 Flask 应用它把前面生成的几张静态图展示到同一个页面from flask import Flask, render_template, url_for app Flask(__name__) app.route(/) def index(): return render_template( index.html, rating_imagerating_dist.png, year_imageyear_rating.png, genre_imagegenre_bar.png, ) if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)对应的templates/index.html核心部分!doctype html html langzh-CN head meta charsetutf-8 title豆瓣电影数据分析/title /head body h1豆瓣电影 Top250 数据分析/h1 h2评分分布/h2 img src{{ url_for(static, filenameoutput/rating_dist.png) }} alt评分分布 ... /body /html但注意 Flask 默认的static文件夹是static目录不是output。最简单的做法是把图片放进static/output下或者通过send_from_directory自定义访问路径。我用的是后者避免移动文件from flask import send_from_directory app.route(/output/path:filename) def output_images(filename): return send_from_directory(output, filename)这样模板里的图片路径就可以写/output/rating_dist.png。这个细节很容易被忽略等你部署到服务器上图片加载失败时才会意识到问题。如果你想让图表可交互启动前先安装pyecharts然后用它生成 Bar 或 Line 对象。Pyecharts 的render_embed()方法会返回包含 JS 依赖的完整 HTML 片段放到模板里就能显示from pyecharts.charts import Bar from pyecharts import options as opts def make_type_bar(): type_counts df_type[genres_list].value_counts().head(10) bar Bar() bar.add_xaxis(type_counts.index.tolist()) bar.add_yaxis(出现次数, type_counts.values.tolist()) bar.set_global_opts(title_optsopts.TitleOpts(title类型出现次数 TOP10)) return bar.render_embed()Pyecharts 生成的图表自带鼠标悬停提示和缩放比静态图更有演示效果。但要注意版本兼容问题早期 0.x 版本和 1.x 版本 API 差别很大我建议直接装最新版pip install pyecharts默认安装的就是 2.x用上面的写法没问题。6. 部署上线从本地跑通到服务器发布项目做完之后副本不能只留在自己电脑上。很多同学卡在“本地能跑别人访问不了”这一关。要解决这个问题核心就两步写一份清晰的部署文档然后在服务器上按文档跑一遍。6.1 本地运行与环境准备任何时候装依赖都建议先建虚拟环境避免污染全局 Python。在项目根目录执行python -m venv venvWindows 下激活venv\Scripts\activateMac/Linux 下激活source venv/bin/activate激活后安装依赖pip install -r requirements.txtrequirements.txt至少要包含requests beautifulsoup4 pandas numpy matplotlib seaborn wordcloud jieba flask pyecharts装完依赖先跑一遍数据准备脚本再启动 Flaskpython crawl.py python analysis.py python app.py浏览器访问http://127.0.0.1:5000能看到页面本地就算跑通了。6.2 部署到 Linux 服务器的关键步骤服务器我以 Ubuntu 为例。常见的坑是系统自带的 Python 版本比较老我一般用python3指令先确认版本至少需要 Python 3.8。服务器上安装依赖前记得先升级 pipsudo apt update sudo apt install python3-venv python3-pip python3 -m venv venv source venv/bin/activate pip install --upgrade pip pip install -r requirements.txtFlask 自带的开发服务器只适合测试生产环境我用 Gunicorn 启动pip install gunicorn gunicorn -w 4 -b 0.0.0.0:8000 app:app这条命令的意思是启动 4 个 worker 进程监听 8000 端口。这里app:app表示从app.py文件里导入名为app的 Flask 实例。启动后浏览器访问http://服务器IP:8000就能看到页面。如果访问不了先去检查云控制台的安全组或防火墙确认 8000 端口开放。很多人第一步就开错端口代码怎么调都没用。如果你想让服务在后台常驻可以用 systemd 写一个服务文件。这个不是必须但如果项目要连续演示几天建议配置一下。在/etc/systemd/system/movie.service里写[Unit] DescriptionMovie Analysis App Afternetwork.target [Service] Userubuntu WorkingDirectory/home/ubuntu/movie_project ExecStart/home/ubuntu/movie_project/venv/bin/gunicorn -w 4 -b 0.0.0.0:8000 app:app Restartalways [Install] WantedBymulti-user.target然后执行sudo systemctl daemon-reload sudo systemctl enable movie sudo systemctl start movie这样即使服务器重启项目也会自动运行省去手动启动的麻烦。6.3 部署文档需要包含什么如果你是要交付源码加部署文档文档不要只丢一堆命令。我写部署文档的习惯是包含六个部分项目结构说明、环境依赖清单、本地启动步骤、服务器部署步骤、常见问题排查、项目功能展示图。常见问题排查尤其要写具体比如“中文乱码怎么办”“端口被占用怎么办”。这份文档本质上是给一个完全陌生的人看所以要默认对方不知道你的项目结构每一步的操作命令和预期输出都要写清楚。7. 常见问题与排查实录最后这部分是我实际跑这个项目时碰到的真问题每个都折腾了不少时间整理成速查表供你参考。7.1 爬虫被拒出现 418 或 403 怎么办418 这个状态码很有意思意思是“我是茶壶”豆瓣用它来标识“你不是真实用户”。出现 418 时先检查三件事User-Agent 是否正确、请求间隔是否太短、是否带了 Cookie。我的经验是把浏览器后台的 Cookie 复制到 HEADERS 里基本一次就好了。另外不要在短时间内反复重试如果请求失败了就停下来等几分钟再继续不要用循环内非常短的 sleep 来硬刚。7.2 Matplotlib 中文乱码和负号问题这是新手必踩的坑。如果标题、坐标轴出现方块就是字体没设置。Windows 下用SimHeiMac 下用PingFang SC。设置一次后同一个脚本里所有图表都会生效。负号显示成方块则要单独设置axes.unicode_minusFalse。还有一部分情况是 WordCloud 生成词云时中文变成框那不是 Matplotlib 的锅是WordCloud没有指定font_path。把系统中文字体路径填上就能解决。7.3 数据量变大后图表卡顿如果你后来从 Top250 扩展到全部热门电影上万条数据直接丢给 Pyecharts 渲染浏览器可能会卡。解决办法有两个一是在可视化前先聚合不要把所有原始数据传给前端二是把图表分成多个小图展示。查看趋势图时先 groupby 聚合再传聚合后的结果。展示 TopN 时只取前 50 条。这样前端渲染压力会小很多。7.4 部署后图片和样式加载不出来本地正常部署后图片失效大概率是 Flask 静态资源路径问题。如果你把图片放在output目录而模板里直接写static/output/xxx.png路径可能对不上。最简单的方法就是像前面那样写一个路由/output/path:filename返回图片或者把所有图片复制到static目录里。另外服务器防火墙挡了 8000 端口也会导致整个页面访问不了这时先用curl -I http://127.0.0.1:8000在服务器内部验证再检查外部访问。我个人做这个项目最深的体会是第一版千万不要追求花哨。先把 Top250 爬下来画成五张基础图表再慢慢加 Flask 展示和部署。越是想着一步到位越容易在环境配置和样式调整上浪费大把时间。尤其是中文乱码、Gunicorn 启动失败这些坑早踩早总结等真正需要交项目的时候就会顺畅很多。如果你后面打算扩展可以把短评爬得更多一些做成情感分析或者加一个历史评分变化趋势这套源码结构是完全撑得住的。