Python猫眼电影数据分析与可视化:从爬虫到ECharts大屏实战

发布时间:2026/9/29 5:29:26
Python猫眼电影数据分析与可视化:从爬虫到ECharts大屏实战 简介这是一份基于Python的猫眼电影数据分析可视化系统的完整设计文档适合影视行业从业者、数据分析学习者以及需要毕业设计参考的高校学生。系统以requests库自动抓取猫眼公开电影数据并借助Pandas完成去重、缺失值与异常值清洗再通过Matplotlib和Echarts生成评分分布、票房趋势、电影类型等多维可视化图表同时利用Flask框架搭建Web操作界面让非技术用户也能直观掌握电影市场动态。文档正文从绪论、国内外研究现状、需求分析到系统架构、核心模块实现、测试与扩展性设计均有详细阐述既展示完整技术路线也体现了代码的可维护性与未来功能扩展空间封面、目录、正文和图表配套完整。压缩包内共1个docx文件大小约3.31MB结构清晰读者可直接参照搭建同类数据分析可视化项目也可为制定电影宣传策略和发行计划提供数据依据。目前已有382人学习下载适合作为课程设计、毕业设计或行业数据分析实践的重要参考资料。1. 先搞清楚这套「猫眼电影数据分析可视化系统」到底在解决什么问题很多人第一次看到这个标题第一反应是「不就是爬个猫眼榜单再画几张图吗」。但实际动手你会发现真正劝退你的不是爬虫而是后面一整条链路数据抓下来了怎么存、字段脏了怎么洗、评分和票房放在一起怎么分析、最后怎么在可视化大屏上把结论讲清楚。这个标题背后是一套完整的「数据采集 → 存储 → 清洗 → 分析 → 可视化」闭环适合正在做 Python 数据分析入门项目、毕业设计或者想给自己攒一个能写进简历的数据分析作品集的人。它不涉及高深的机器学习但非常磨手你会遇到反爬、字段缺失、编码混乱、图表选型失当等一系列真实问题。把这套链路走通你对 pandas、requests、ECharts 的掌握程度会明显上一个台阶这也是它值得做的核心原因。2. 用 Python 把猫眼数据抓下来先从 Top100 榜单入手2.1 为什么选 Top100 作为数据源而不是全站爬取猫眼电影的数据接口大体分两类一类是面向网页的 HTML 页面一类是后端异步加载的 JSON 接口。全站爬取意味着你要处理登录态、签名参数、大量的反爬验证对入门项目来说性价比极低。相比之下Top100 榜单页的结构非常规整每一部电影的标题、主演、上映时间、评分都集中在同一个列表页里只需要几十行代码就能拿到干净的结构化数据。你的系统核心目标是「数据分析与可视化」不是「爬虫对抗」。所以数据源的选择标准应该是数据足够覆盖分析场景同时采集难度可控。Top100 榜单每页 10 部电影一共 10 页能拿到片名、主演、上映日期、评分、票房等字段支撑评分分布、上映年份趋势、票房区间分析这些常见可视化主题绰绰有余。后面想扩展再横向加「正在热映」或「口碑榜」也不迟。实际采集时最常见的实现方式是requests加BeautifulSoup把页面里的dd节点逐一解析。这个方案在 2024 年前后依然可行但前提是你必须伪装好请求头并且控制采集频率。下面给出一个可直接跑通的最小实现。2.2 爬取脚本的最小实现与逐行说明import time import random import requests from bs4 import BeautifulSoup import pandas as pd 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://www.maoyan.com/board/1, Accept-Language: zh-CN,zh;q0.9, } def fetch_one_page(offset: int) - list: 抓取 Top100 榜单单页数据offset 为 0、10、20 ... 90 url fhttps://www.maoyan.com/board/1?offset{offset} resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() soup BeautifulSoup(resp.text, html.parser) items [] for dd in soup.select(dl.board-wrapper dd): title_tag dd.select_one(.name a) star_tag dd.select_one(.star) time_tag dd.select_one(.releasetime) score_tag dd.select_one(.score) if not all([title_tag, star_tag, time_tag, score_tag]): continue items.append({ title: title_tag.get_text(stripTrue), actors: star_tag.get_text(stripTrue).replace(主演, ), release_time: time_tag.get_text(stripTrue).replace(上映时间, ), score: score_tag.get_text(stripTrue), }) return items def crawl_top100(): all_rows [] for offset in range(0, 100, 10): rows fetch_one_page(offset) all_rows.extend(rows) time.sleep(random.uniform(1.5, 3.5)) # 随机延时避免请求过快 print(f已抓取 offset{offset}, 累计 {len(all_rows)} 条) return pd.DataFrame(all_rows) if __name__ __main__: df crawl_top100() df.to_csv(maoyan_top100_raw.csv, indexFalse, encodingutf-8-sig) print(df.head())这段代码有几个地方要重点说明。HEADERS里的Referer和User-Agent缺一不可猫眼对裸的requests请求识别非常敏感缺少这两个字段很容易被重定向到验证页。select选择器依赖的是榜单页的 DOM 结构如果你在复现时发现抓不到数据先打开浏览器开发者工具确认.board-wrapper dd这个节点路径是否还成立。解析后立即用pandas落盘避免内存数据因程序异常丢失。time.sleep(random.uniform(1.5, 3.5))是防止被封的关键。把固定延时改成随机区间行为上更接近真实用户也避免多个请求之间出现固定节律被识别。如果你只想验证代码流程可以把范围改成0.5 - 1.0但完整采集时建议不要低于 1 秒。utf-8-sig编码是为了让 Excel 直接打开 CSV 不乱码这是处理中文数据的一个常规细节。2.3 采集时的反爬边界与失败信号采集过程中最常见的失败信号不是请求被拒而是返回了「验证页面」。当你发现解析出来的items列表为空或者title字段全部是乱码/空值第一件事不是改解析逻辑而是把resp.url和resp.status_code打印出来看看。如果状态码是 200 但页面内容里找不到榜单节点说明你拿到的可能是一个反爬中间页。此时requests.get的allow_redirects参数默认为 True你看到的是跳转后的结果。解决办法是检查resp.text的前 200 个字符里有没有「验证」之类的关键词有就说明需要加强请求头伪装或降低频率。另一个容易翻车的地方是字段缺失。榜上的老电影偶尔会缺「上映时间」或「主演」直接用get_text(stripTrue)不会报错但会留下空字符串。这一步先原样保留等进入清洗阶段再统一处理。采集阶段追求「尽量不丢」清洗阶段再做「去空和规范化」两个阶段混在一起会让代码变得非常难调试。3. 把数据交给 pandas 前先想清楚存在哪、怎么存3.1 选 SQLite 还是 CSV根据你的可视化链路决定爬下来的数据最终要喂给可视化层存储方案的选择直接影响后续开发的体验。如果你的可视化是 Flask ECharts 这类 Web 方案数据需要被后端反复查询和聚合那么 SQLite 是比 CSV 更合理的选择。它不需要额外安装数据库服务端Python 标准库自带sqlite3一张表存 100 条电影数据读写效率绰绰有余。CSV 的优势是简单直观适合单机分析、用 pandas 一把梭。但缺点也很明显字段类型需要你手动确认中文编码在不同工具间容易出问题而且每次可视化前的数据聚合都要重新读文件。我一般建议的折中方案是采集完先存 CSV 做备份同时写入 SQLite 作为主数据源。这样既留了后悔药又不影响后续查询效率。还有一点要注意如果你的可视化大屏要用到 Redis 做缓存或热数据存储那 SQLite 负责持久化、Redis 负责缓存查询结果是一个很常见的数据分析项目架构。不过对 Top100 这个量级的数据来说Redis 属于可选优化不是必需品先用 SQLite 把链路跑通再加缓存不迟。3.2 建表与写入字段类型和索引一开始就要定好CREATE TABLE IF NOT EXISTS movies ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, actors TEXT, release_time TEXT, release_year INTEGER, score REAL, box_office REAL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );建表时最容易忽略的是release_year和box_office这两个字段。原始数据里的release_time是「2018-07-05 中国大陆上映」这种混合字符串直接存 TEXT 会导致后面做年份趋势分析时要反复字符串截取box_office在 Top100 页面里往往不是显式字段需要你在清洗阶段从其他信息里提取或者留空所以先把表结构设计成可扩展的样子比后面ALTER TABLE改结构要省心得多。写入时建议用INSERT OR REPLACE配合唯一索引防止重复采集时产生脏数据。实现上可以先把数据转成DataFrame再用pandas.to_sql一次性写入但注意to_sql默认不会创建主键和索引你得在写入后手动执行建索引的语句。import sqlite3 conn sqlite3.connect(maoyan.db) df pd.read_csv(maoyan_top100_raw.csv, encodingutf-8-sig) # 将清洗后的 DataFrame 写入 SQLite df.to_sql(movies, conn, if_existsappend, indexFalse) # 手动创建索引加速后续按年份、评分的聚合查询 conn.execute(CREATE INDEX IF NOT EXISTS idx_year ON movies(release_year)) conn.execute(CREATE INDEX IF NOT EXISTS idx_score ON movies(score)) conn.commit() conn.close()if_existsappend和replace之间的选择需要你根据业务决定。append适合增量采集后追加数据但如果你改了清洗逻辑要重新生成数据务必先手动DROP TABLE或改用replace否则会出现同一部电影多个版本的记录。索引的命名规范建议带上前缀idx_后续维护时一眼就能看出哪些列有索引。3.3 清洗阶段的四个标准动作# 1. 去重 df df.drop_duplicates(subset[title], keeplast) # 2. 从 release_time 中提取年份 df[release_year] df[release_time].str.extract(r(\d{4})).astype(Int64) # 3. 评分统一转 float非法值置为 NaN df[score] pd.to_numeric(df[score], errorscoerce) # 4. 演员字段拆成列表去掉空值 df[actors] df[actors].fillna().str.split(, ) df[actors] df[actors].apply(lambda x: [a for a in x if a])这四步处理顺序有讲究。先去重再提取年份是为了避免重复记录干扰后续的聚合统计评分转float时errorscoerce会把无法转换的值变成NaN比直接报错中断程序要好得多演员字段从字符串拆成列表是给后期「哪些演员出现频率最高」这类分析预留结构。release_year用Int64而不是int是因为astype(int)无法处理NaN这一点很容易让新手翻车。清洗完成后建议立刻做一个简单的质量检查打印数据的形状、每列的缺失值数量、评分字段的描述性统计。如果 100 条数据里缺失值超过 10%那问题大概率出在采集阶段而不是清洗阶段回头去补抓数据比在这里硬填要靠谱。4. 分析什么、怎么算三个能立刻用上的分析维度4.1 评分分布用 pandas 直接出结论数据分析部分的核心不是把数据画出来而是先想清楚「要回答什么问题」。对于猫眼 Top100 这个数据集有三类问题最容易出成果高分电影的年份分布、不同年份段的评分差异、演员的霸榜频次。这三个方向既不需要复杂建模又能展示你在数据理解上的基本盘。import pandas as pd df pd.read_sql(SELECT * FROM movies, sqlite3.connect(maoyan.db)) score_desc df[score].describe() print(score_desc) # 按年份分组计算每年的平均分与电影数量 yearly_stats ( df.groupby(release_year) .agg(avg_score(score, mean), movie_count(score, count)) .reset_index() ) print(yearly_stats.sort_values(movie_count, ascendingFalse).head(10))groupby聚合是这里最核心的操作。agg里对不同列应用了不同的聚合函数mean和count同时算出来避免写两遍groupby。输出结果后你大概率会发现一个规律Top100 里近五年的电影数量明显偏多但平均分不一定比老片高。这个现象本身就可以成为可视化大屏上最值得展示的洞察。同时建议做一次评分区间分布分析把 9.0 分以上、8.0-8.9、7.0-7.9、7.0 以下分成四个桶用pd.cut实现。这个分布结果直接对应后面的柱状图或饼图几乎没有额外工作量。4.2 演员/导演维度字符串拆分的下一步演员字段在清洗阶段已经转成了列表分析时真正落地为可量化指标的方法是「拆行」。最常见的做法是把一条包含多个演员的记录拆成多条每人一行然后再做频次统计。# 将演员列表拆成多行方便统计单人频次 actors_exploded df.explode(actors) actor_counts ( actors_exploded[actors] .value_counts() .reset_index() ) actor_counts.columns [actor, count] print(actor_counts.head(20))explode是 pandas 里处理列表字段的标准方法它把一行里的多个演员展开成多行其他字段自动复制。这里要注意的是拆完之后数据的行数会变成几百行如果后续还要和原表做关联需要保留title字段作为关联键。统计结果里你大概率会看到几个熟面孔高频出现这说明这批演员的票房号召力确实集中在头部。这一步还有一个加分操作把「演员出现次数」和「对应电影的平均评分」结合起来挑选出演了至少 3 部电影且平均分大于 8.5 的演员作为「口碑担当」榜单。这个交叉分析能体现你对多表数据关系的理解比单纯数数高一个档次。4.3 分析结果导出为可视化准备 JSON 结构可视化层面主要靠 ECharts而 ECharts 的图表数据格式是 JSON。所以分析阶段的最后一步是把 pandas 的DataFrame转换成前端可以直接消费的JSON数组。import json # 生成评分区间分布数据 bins [0, 7, 8, 9, 10] labels [7分以下, 7.0-7.9, 8.0-8.9, 9.0以上] df[score_bin] pd.cut(df[score], binsbins, labelslabels, rightFalse) bin_counts df[score_bin].value_counts().reindex(labels).fillna(0) # 组装成 ECharts 友好的键值对结构 chart_data [ {name: str(bin), value: int(count)} for bin, count in bin_counts.items() ] with open(score_distribution.json, w, encodingutf-8) as f: json.dump(chart_data, f, ensure_asciiFalse, indent2)reindex在这里很关键value_counts的结果会自动按频次倒序排列而不是按你定义的区间顺序直接转列表会导致图表上区间顺序紊乱。用reindex强制对齐到你期望的顺序再fillna(0)处理空区间。ensure_asciiFalse保证中文不乱码这是所有导出 JSON 都要注意的细节。到这一步你的输出物已经从前端的「画图需求」反推到了「数据到底要长什么样」。整个链路不再是爬完数据就结束而是每一层都在为下一层做准备。这也是数据分析项目区别于单纯爬虫项目的分水岭。5. 避坑指南这套系统最常见的五个翻车现场5.1 爬虫被重定向到验证页不是 IP 被封是请求头不够像浏览器现象requests.get返回 200但解析不出任何电影节点打印resp.url发现地址已经不是原来的榜单页。原因服务器识别出这是一个非浏览器发起的请求返回了反爬中间页。缺少User-Agent或Referer是最常见原因其次是请求频率过于规律被判定为脚本行为。解决先补全请求头把User-Agent换成真实 Chrome 的完整字符串加上Referer指向榜单页本身。如果问题依旧把time.sleep的固定延时改成随机延时。仍不行就考虑用session保持连接先访问首页再访问榜单页模拟真实浏览路径。5.2 评分字段解析成「8.9」字符串后无法做数值比较现象df[df[score] 9.0]报错或结果为空明明数据里有很多 9 分以上的电影。原因爬虫解析出来的score是字符串且可能是「9.2」和「暂无评分」混合在一起。字符串比较「9.10」和「9.2」时逐位比较结果完全不符合数值逻辑。解决用pd.to_numeric(df[score], errorscoerce)统一转数值无法转换的置为NaN。后续所有统计都要基于转换后的数值列不要在原字符串列上做比较运算。分析前用df[score].dtype确认类型已经是float64。5.3 写入 SQLite 后中文变成乱码现象从 SQLite 里read_sql出来的中文正常但用第三方工具如 Navicat 或 DBeaver打开表看到的是乱码或问号。原因连接 SQLite 时没有指定text_factory或数据库本身的编码声明不对。SQLite 默认以 UTF-8 存储文本但某些客户端工具在创建连接时用了本地编码解析。解决Python 连接时显式设置conn.text_factory str。同时避免在 Windows 下用默认编码gbk去读 UTF-8 内容。更稳妥的做法是数据入库前统一转成 UTF-8CSV 备份文件用utf-8-sig写入保证两个存储实体的编码口径一致。5.4pd.cut报错「bins must increase monotonically」现象按评分区间分组时pd.cut直接抛异常提示分箱边界必须单调递增。原因代码里写bins [0, 7, 8, 9, 10]看起来是递增的但数据里有NaN值pd.cut默认无法处理缺失值或者边界传入了None。也可能是你写了bins[10, 0, 7, 8, 9]没有注意到顺序。解决分箱前先df df.dropna(subset[score])或df[score].fillna(df[score].median())。检查bins列表确实是严格递增的数值序列。然后用pd.cut的rightFalse参数明确边界归属避免边界值 7.0 落在两个区间的歧义。5.5 可视化大屏的图表加载不出数据JSON 结构对不上现象ECharts 页面打开后图表空白浏览器控制台报错提示data不是数组或属性不存在。原因后端传过去的 JSON 是DataFrame.to_json()输出的对象格式形如{0: {...}, 1: {...}}而 ECharts 的series.data需要的是[{name: ..., value: ...}, ...]这样的数组格式。解决用json.loads(df.to_json(orientrecords))转成数组或者像前面清洗阶段那样用json.dump手动构造列表结构。前后端联调时先固定一个 mock JSON 文件把前端调通后再接后端真实数据这样问题边界清晰不会两边互相甩锅。6. 验证与进阶从「数据能跑」到「系统能演示」整套系统做完后验证逻辑应该从两个方向走一是数据正确性验证二是可视化响应验证。数据正确性最简单的检验方式是把你的统计结果和猫眼榜单页面上肉眼可见的数字做对照。比如 Top100 里最高分电影是那部、9 分以上有多少部这些都能在榜单页上直接数出来。对不上就先查清洗逻辑再查爬虫解析不要急着怀疑数据库。可视化响应验证则要关注大屏的实际使用体验。比较常见的做法是启动 Flask 服务后强制刷新页面 20 次观察图表是否稳定加载、接口响应是否在几百毫秒内。如果图表加载明显卡顿优先检查是不是每次请求都重新读取 SQLite 做全量查询。一个立竿见影的优化是把分析结果缓存到内存里只有数据更新时才重新计算这也是我在真实项目里必做的一步。进阶方向上最值得投入的是时间维度的动态效果用 ECharts 的timeline组件做年份滑块让观众拖动滑块看每一年上榜电影的数量和平均分变化。这个功能在原版 Top100 静态数据上做不了你得扩展采集范围到「近十年每年年度榜单」然后把采集、清洗、分析的逻辑封装成按月可调的脚本。这一套做下来系统就从「一个页面」变成了「一个真正的数据分析工具」。另一个进阶点是给 Flask 后端加接口参数。比如/api/charts/score_distribution?year2018让前端能按年份筛选数据。这需要你给 SQLite 表加一个「榜单归属年份」字段重构查询逻辑。改完之后你可以声称这套系统支持多维度下钻分析简历上的分量和只有一个静态大屏完全不同。最后提醒一点整个项目里我最深刻的教训是不要急着写前端代码。先把数据清洗到「一眼能看到结论」的程度再动手做图表。数据是脏的任何可视化都是空中楼阁。先花 70% 时间把数据链路磨顺剩下 30% 时间你会发现做可视化快得惊人。希望这篇笔记能帮你在自己的数据分析项目里少走几步弯路。本文还有配套的精品资源点击获取