基于Python的电影数据可视化分析系统:爬虫、Pandas与Pyecharts全流程实战

发布时间:2026/9/23 2:55:41
基于Python的电影数据可视化分析系统:爬虫、Pandas与Pyecharts全流程实战 简介这是一套面向计算机相关专业学生与项目实战学习者的电影数据可视化分析系统完整源码可直接用于毕业设计、课程大作业或技能练习。项目经导师指导并获评审98分认可源码均经本地编译与严格调试确保可运行难度适中适合作为高分毕设参考方案。压缩包共252个文件约15.05MB以61个py源码文件为核心辅以23个html页面、11个js脚本与4个css样式构成前端展示层另有27个png与26个jpg图表素材、11个xml配置、1个sqlite3数据库及2个pdf开发文档覆盖数据采集、清洗、分析与可视化全流程。资源还包含开发文档与说明文件便于理解目录结构与模块划分快速定位关键代码与排错思路。目前已有97人学习下载适合需要完整项目案例、想掌握Python数据分析与可视化实现细节的读者参考使用。1. 电影数据可视化分析系统从一份高分毕设源码里拆出可复用的技术骨架很多同学做毕设选题时看到「基于Python的电影数据可视化分析系统」第一反应是——不就是爬点数据、画几张图吗真动手才发现数据源字段对不上、中文乱码、图表堆在一起像调色盘、开发文档写得像流水账最后答辩被问「你的分析结论是什么」直接卡壳。这套系统的核心价值不在于图表多花哨而在于它把「数据采集 → 清洗入库 → 多维分析 → 可视化呈现」这条链路完整跑通了并且配了一份能讲清楚设计决策的开发文档。它适合两类人一是正在找计算机毕设选题、需要一套结构完整可二次开发的项目二是想用Python把爬虫、Pandas、可视化串起来练手的入门者。下面我按实际落地顺序把每个环节的关键代码、参数和踩坑点拆开讲。2. 数据从哪来爬虫采集与字段设计决定后续所有分析的上限2.1 选豆瓣Top250还是自定义关键词爬取电影数据可视化分析系统的数据源通常有两种做法。第一种是爬取豆瓣Top250优点是页面结构稳定、字段规整排名、片名、导演、年份、评分、评价人数、短评适合快速跑通全流程缺点是数据量固定250条做「类型分布」「年代趋势」时样本偏少图表说服力有限。第二种是按关键词或分类爬取比如按「悬疑」「2023」等条件抓取数据量可以到几千条但需要处理分页、反爬和字段缺失。我一般建议毕设场景用「Top250打底 分类补充」的策略先用Top250保证基础字段完整再按类型标签补充300500条这样既有干净数据做核心分析又有足够样本做交叉维度。开发文档里要把数据源选择理由写清楚答辩时这是加分项。2.2 爬虫代码requests BeautifulSoup 的最小可用版本import requests from bs4 import BeautifulSoup import csv 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 parse_top250(start0): url fhttps://movie.douban.com/top250?start{start} resp requests.get(url, headersHEADERS, timeout10) resp.encoding utf-8 soup BeautifulSoup(resp.text, html.parser) items [] for node in soup.select(div.item): title node.select_one(span.title).get_text(stripTrue) rating node.select_one(span.rating_num).get_text(stripTrue) # 评价人数在 rating 后面的 span 里用正则提取数字 votes node.select_one(div.star span:last-child).get_text(stripTrue) info node.select_one(div.bd p).get_text(stripTrue) items.append({ title: title, rating: float(rating), votes: int(votes.replace(人评价, ).replace(,, )), info: info }) return items def crawl_all(): all_data [] for start in range(0, 250, 25): batch parse_top250(start) all_data.extend(batch) time.sleep(random.uniform(1.5, 3.0)) # 随机延时降低被封风险 return all_data if __name__ __main__: data crawl_all() with open(movies_raw.csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[title, rating, votes, info]) writer.writeheader() writer.writerows(data) print(f共采集 {len(data)} 条)这段代码的逻辑很直白按start参数翻页每页解析div.item节点提取片名、评分、评价人数和详情文本。几个关键参数需要说明。time.sleep(random.uniform(1.5, 3.0))是必须的固定间隔容易被识别为脚本随机延时更接近人工浏览节奏。encodingutf-8-sig写入CSV时带BOM头Excel打开不会中文乱码这是血泪经验。timeout10防止某个请求卡死导致整个脚本挂起。info字段是一整段文本包含导演、主演、年份、国家、类型后续需要用正则或字符串切分拆成独立列。这一步在开发文档里要单独写一节「字段解析规则」因为不同页面的info格式可能有细微差异比如有的有「主演」有的没有。2.3 字段拆解把info列变成可分析的维度import re import pandas as pd def split_info(info): # 典型格式导演: 弗兰克·德拉邦特 主演: 蒂姆·罗宾斯 / 摩根·弗里曼 1994 / 美国 / 犯罪 剧情 parts info.split(\xa0\xa0\xa0) # 豆瓣用三个不间断空格分隔 result {director: , actors: , year: , country: , genre: } if len(parts) 1: result[director] parts[0].replace(导演:, ).strip() if len(parts) 2: result[actors] parts[1].replace(主演:, ).strip() if len(parts) 3: meta parts[2].split(/) if len(meta) 3: result[year] re.search(r\d{4}, meta[0]) result[year] result[year].group() if result[year] else result[country] meta[1].strip() result[genre] meta[2].strip() return result df pd.read_csv(movies_raw.csv) meta_df df[info].apply(split_info).apply(pd.Series) df pd.concat([df.drop(columns[info]), meta_df], axis1) df.to_csv(movies_clean.csv, indexFalse, encodingutf-8-sig)这里最容易翻车的地方是分隔符。豆瓣页面里用的是\xa0\xa0\xa0不间断空格直接按普通空格split会失败。年份提取用正则\d{4}比按位置切更稳因为有的条目年份前面带国家名。类型字段可能包含多个值如「犯罪 剧情」后续做类型分布时需要拆成多行这个在分析阶段处理。3. 分析层怎么搭Pandas 分组聚合与四个核心分析维度3.1 评分分布与年代趋势最基础也最容易被追问的两个图评分分布用直方图年代趋势用折线图或柱状图。这两个图看起来简单但答辩时老师最爱问「为什么Top250的评分集中在8.5以上」「哪个年代高分电影最多」。代码本身不复杂关键是分箱参数要合理。import pandas as pd import matplotlib.pyplot as plt import matplotlib matplotlib.rcParams[font.sans-serif] [SimHei] # 中文字体 matplotlib.rcParams[axes.unicode_minus] False df pd.read_csv(movies_clean.csv) df[year] pd.to_numeric(df[year], errorscoerce) df df.dropna(subset[year]) df[year] df[year].astype(int) # 评分分布按0.5分一档 bins [8.0, 8.5, 9.0, 9.5, 10.0] labels [8.0-8.5, 8.5-9.0, 9.0-9.5, 9.5-10.0] df[rating_bin] pd.cut(df[rating], binsbins, labelslabels, rightFalse) rating_dist df[rating_bin].value_counts().sort_index() fig, axes plt.subplots(1, 2, figsize(14, 5)) rating_dist.plot(kindbar, axaxes[0], color#4C72B0, edgecolorwhite) axes[0].set_title(Top250评分分布) axes[0].set_xlabel(评分区间) axes[0].set_ylabel(电影数量) axes[0].tick_params(axisx, rotation0) # 年代趋势按十年分组 df[decade] (df[year] // 10) * 10 decade_count df.groupby(decade).size() decade_count.plot(kindline, axaxes[1], markero, color#DD8452) axes[1].set_title(各年代入选Top250数量) axes[1].set_xlabel(年代) axes[1].set_ylabel(电影数量) plt.tight_layout() plt.savefig(rating_trend.png, dpi150) plt.show()pd.cut的rightFalse表示左闭右开区间避免8.5分被同时算进两个箱子。(df[year] // 10) * 10是整除取整到十位比如1994变成1990这是做年代聚合的常用技巧。dpi150保证导出图片清晰度够放进论文。3.2 类型分布与导演排行需要拆行和计数的两个分析类型字段是「犯罪 剧情」这种多值形式直接value_counts会把「犯罪 剧情」当成一个整体。正确做法是按空格拆开再explode。# 类型分布拆成多行后统计 genre_df df[[title, genre]].copy() genre_df[genre] genre_df[genre].str.split( ) genre_exploded genre_df.explode(genre) genre_count genre_exploded[genre].value_counts().head(10) # 导演排行按入选数量 director_count df[director].value_counts().head(10) fig, axes plt.subplots(1, 2, figsize(14, 6)) genre_count.plot(kindbarh, axaxes[0], color#55A868) axes[0].set_title(Top250类型分布Top10) axes[0].set_xlabel(电影数量) axes[0].invert_yaxis() director_count.plot(kindbarh, axaxes[1], color#C44E52) axes[1].set_title(入选Top250最多的导演Top10) axes[1].set_xlabel(电影数量) axes[1].invert_yaxis() plt.tight_layout() plt.savefig(genre_director.png, dpi150) plt.show()explode是Pandas 0.25以后才有的方法如果环境版本低需要用循环展开。invert_yaxis()让条形图从高到低排列不然默认是从下往上看起来别扭。导演字段有的包含多个导演用「/」分隔严格来说也应该拆开但毕设场景下按原始字符串统计也能接受开发文档里注明这个简化即可。3.3 评价人数与评分的相关性散点图加回归线这个分析能体现「高分是否等于高热度」是答辩时容易出彩的点。import numpy as np fig, ax plt.subplots(figsize(8, 6)) ax.scatter(df[votes], df[rating], alpha0.5, color#4C72B0, edgecolorwhite) # 拟合趋势线 z np.polyfit(df[votes], df[rating], 1) p np.poly1d(z) x_sorted np.sort(df[votes]) ax.plot(x_sorted, p(x_sorted), r--, linewidth2, labelf趋势线 y{z[0]:.2e}x{z[1]:.2f}) ax.set_xlabel(评价人数) ax.set_ylabel(评分) ax.set_title(评价人数与评分关系) ax.legend() plt.tight_layout() plt.savefig(votes_rating.png, dpi150) plt.show()alpha0.5让重叠点能看出密度。np.polyfit做一次多项式拟合返回斜率和截距。如果斜率接近0说明评价人数和评分没有明显线性关系这本身就是一个有价值的结论——Top250里高分不一定靠人多。4. 可视化界面怎么做Pyecharts 还是 Flask ECharts4.1 两种技术路线的选型对比毕设里常见的可视化呈现方式有两种。第一种是用Pyecharts直接生成HTML文件优点是代码量少、图表样式丰富、自带交互缩放、悬浮提示适合快速出效果缺点是页面之间相互独立没有统一导航看起来不够「系统」。第二种是Flask或Django搭后端 ECharts前端优点是能做成多页面系统、有导航栏、能加筛选条件更像一个完整产品缺点是需要写路由、模板和前后端数据接口工作量翻倍。我的建议是如果毕设时间充裕且想拿高分走Flask ECharts路线开发文档里可以写「前后端分离架构」「RESTful接口设计」答辩时更有话说。如果时间紧Pyecharts生成一个综合Dashboard页面也能过但文档里要补上「系统架构设计」章节来撑篇幅。4.2 Pyecharts 最小可用示例一个页面放四张图from pyecharts import options as opts from pyecharts.charts import Bar, Line, Pie, Scatter, Page, Grid import pandas as pd df pd.read_csv(movies_clean.csv) df[year] pd.to_numeric(df[year], errorscoerce).dropna().astype(int) # 图1评分分布柱状图 rating_bins pd.cut(df[rating], bins[8.0, 8.5, 9.0, 9.5, 10.0], labels[8.0-8.5, 8.5-9.0, 9.0-9.5, 9.5-10.0], rightFalse) rating_dist rating_bins.value_counts().sort_index() bar ( Bar() .add_xaxis(rating_dist.index.tolist()) .add_yaxis(电影数量, rating_dist.values.tolist(), color#4C72B0) .set_global_opts(title_optsopts.TitleOpts(title评分分布), xaxis_optsopts.AxisOpts(name评分区间), yaxis_optsopts.AxisOpts(name数量)) ) # 图2年代趋势折线图 df[decade] (df[year] // 10) * 10 decade_count df.groupby(decade).size() line ( Line() .add_xaxis([str(d) for d in decade_count.index.tolist()]) .add_yaxis(入选数量, decade_count.values.tolist(), is_smoothTrue, color#DD8452) .set_global_opts(title_optsopts.TitleOpts(title年代趋势), xaxis_optsopts.AxisOpts(name年代), yaxis_optsopts.AxisOpts(name数量)) ) # 图3类型分布饼图 genre_df df[[genre]].copy() genre_df[genre] genre_df[genre].str.split( ) genre_exploded genre_df.explode(genre) genre_count genre_exploded[genre].value_counts().head(8) pie ( Pie() .add(, [list(z) for z in zip(genre_count.index.tolist(), genre_count.values.tolist())]) .set_global_opts(title_optsopts.TitleOpts(title类型分布)) .set_series_opts(label_optsopts.LabelOpts(formatter{b}: {c})) ) # 图4评价人数与评分散点图 scatter ( Scatter() .add_xaxis(df[votes].tolist()) .add_yaxis(评分, df[rating].tolist(), color#C44E52) .set_global_opts(title_optsopts.TitleOpts(title评价人数 vs 评分), xaxis_optsopts.AxisOpts(name评价人数, type_value), yaxis_optsopts.AxisOpts(name评分)) ) page Page(layoutPage.DraggablePageLayout) page.add(bar, line, pie, scatter) page.render(dashboard.html) print(已生成 dashboard.html)Page.DraggablePageLayout允许在浏览器里拖拽调整图表位置调整好后点「Save Config」会生成一个JSON下次用Page.save_resize_html加载即可固定布局。is_smoothTrue让折线变平滑视觉上更舒服。饼图的formatter{b}: {c}表示显示「类型名: 数量」。4.3 Flask ECharts 的数据接口设计如果走Flask路线核心是设计好返回JSON的接口。前端ECharts通过Ajax拿数据渲染。from flask import Flask, jsonify, render_template import pandas as pd app Flask(__name__) df pd.read_csv(movies_clean.csv) app.route(/) def index(): return render_template(index.html) app.route(/api/rating_dist) def rating_dist(): bins pd.cut(df[rating], bins[8.0, 8.5, 9.0, 9.5, 10.0], labels[8.0-8.5, 8.5-9.0, 9.0-9.5, 9.5-10.0], rightFalse) counts bins.value_counts().sort_index() return jsonify({labels: counts.index.tolist(), values: counts.values.tolist()}) app.route(/api/genre_dist) def genre_dist(): genre_df df[[genre]].copy() genre_df[genre] genre_df[genre].str.split( ) exploded genre_df.explode(genre) counts exploded[genre].value_counts().head(8) return jsonify({data: [{name: k, value: int(v)} for k, v in counts.items()]}) if __name__ __main__: app.run(debugTrue, port5000)jsonify自动把字典转成JSON响应counts.values.tolist()把numpy数组转成Python列表否则jsonify会报错。debugTrue开发时热重载部署时要关掉。前端ECharts的配置代码在HTML模板里用fetch(/api/rating_dist)拿数据后setOption即可。5. 避坑与排查五个让毕设翻车的典型问题5.1 中文显示成方块现象matplotlib图表里中文全是方框。原因默认字体不支持中文。解决在绘图前设置matplotlib.rcParams[font.sans-serif] [SimHei]Linux环境下如果没有SimHei换成[WenQuanYi Micro Hei]或下载字体文件手动注册。Pyecharts不存在这个问题因为渲染在浏览器里。5.2 爬虫跑一半被封现象前几页正常后面返回403或空页面。原因请求频率过高触发反爬。解决加大随机延时到25秒轮换User-Agent必要时用代理池毕设场景一般不需要。另外不要在短时间内反复跑同一个脚本开发文档里注明「数据采集遵循合理频率」。5.3 Pandas读取CSV后年份变成浮点数现象year列显示为1994.0而不是1994。原因CSV里有空值Pandas自动把整列推断为float。解决读取时加dtype{year: Int64}注意大写I或者读完后df[year] df[year].astype(Int64)。用Int64而不是int是因为前者支持空值。5.4 Pyecharts图表在Jupyter里不显示现象page.render()执行了但Notebook里没图。原因Pyecharts默认渲染到HTML文件Jupyter里需要用Notebook渲染模式。解决在代码开头加from pyecharts.globals import CurrentConfig, NotebookType; CurrentConfig.NOTEBOOK_TYPE NotebookType.JUPYTER_NOTEBOOK或者直接用bar.render_notebook()。5.5 开发文档写成代码注释合集现象文档里全是代码粘贴没有设计思路。原因把「开发文档」理解成了「代码备份」。解决开发文档至少包含需求分析、技术选型理由、系统架构图、数据库表设计如果有、核心算法说明、测试用例、不足与改进方向。代码只放关键片段每段配文字解释「为什么这么做」。6. 让毕设从及格到优秀的三个进阶技巧第一个技巧是加「时间维度对比」。Top250的数据是静态的但你可以爬取近五年每年上映电影中评分最高的若干部做「年度口碑变化」分析。这样图表从「分布」升级到「趋势」答辩时能讲出「2020年后高分电影数量下降」之类的结论比单纯画个饼图有深度。实现上就是在爬虫阶段加一个按年份筛选的入口分析阶段用groupby(year)聚合。第二个技巧是用pyecharts.charts.Grid做多图联动。比如把评分分布柱状图和年代趋势折线图叠在同一个坐标系里双Y轴或者用Timeline做按年份的动态播放。Timeline的用法是创建一个Timeline对象循环往里面add每一年的图表最后render。这个效果在答辩演示时很抓眼球代码量也不大。from pyecharts.charts import Timeline, Bar from pyecharts import options as opts timeline Timeline() for year in range(1990, 2025, 5): subset df[(df[year] year) (df[year] year 5)] genre_counts subset[genre].str.split( ).explode().value_counts().head(6) bar ( Bar() .add_xaxis(genre_counts.index.tolist()) .add_yaxis(数量, genre_counts.values.tolist()) .set_global_opts(title_optsopts.TitleOpts(titlef{year}-{year4}年类型分布)) ) timeline.add(bar, str(year)) timeline.render(timeline.html)第三个技巧是给系统加一个「数据筛选」交互。在Flask ECharts方案里前端加一个下拉框选择年份范围或类型通过Ajax把筛选参数传给后端后端用Pandas过滤后返回新数据前端重新setOption。这个功能在开发文档里可以写成「动态查询模块」是区分「静态图表堆砌」和「分析系统」的关键标志。我自己的习惯是先把数据管道跑通确保CSV字段干净再动手写可视化。很多人反过来图先画出来发现数据有问题回头改爬虫结果图表代码全要调。另外开发文档不要留到最后补每完成一个模块就写一节最后拼起来就是完整文档比事后回忆高效得多。希望帮到你。本文还有配套的精品资源点击获取