简介:基于Python与pyecharts库实现的电影票房与评分可视化分析项目源码,面向数据分析、爬虫和可视化初学者,围绕2018年国内上映电影完整数据,演示多平台采集、存储、清洗到图表绘制的全过程。压缩包共包含三十四个文件,大小二点六七MB,主要涉及七个Python脚本、一个Jupyter Notebook、三个CSV数据文件、十个HTML可视化页面、九张PNG图片以及说明文档,脚本分别负责中国票房网数据抓取、豆瓣详情与影人信息补充、猫眼和时光网及IMDB评分获取、pyecharts绘图,Notebook便于分步执行与理解。目前已有四百九十二人学习下载。项目亮点是可对比不同网站评分差异,分析票房总排名、月份票房分布、评分与票房关系,并统计演员年参演电影数量;同时附有中间数据、说明文档和图表结果,整体结构清晰,数据文件、脚本与可视化结果分目录存放,便于按模块查阅和二次开发,适合想通过真实项目掌握API调用、数据清洗与pyecharts可视化技巧的读者。
1. 用 Python 把 2018 年院线电影票房和评分一次讲清楚:这不是爬虫 Demo,是完整数据链路
很多同学拿到电影数据只会在豆瓣上翻排行榜,但那只是「别人整理好的结论」。这份基于 Python 3.6 + pyecharts 的 2018 年国内上映电影票房评分可视化项目,真正有价值的地方在于:它把「中国票房网的票房原始数据 → 豆瓣的导演/演员/评分 → 猫眼/时光网/IMDB 的第三方评分 → 影人累计票房统计 → 十余张可交互 HTML 图表」整条链路全打通了。我拆完这套源码后的第一感受是,它不像课程设计那种只给你一个画图的 notebook,而是一个你能拿去直接跑、能改年份复用、能写进简历的真实数据项目。适合人群很明确:正在做数据可视化课设/毕设的同学、想学爬虫到数据分析完整流程的初学者、以及手里有别的年份数据想照葫芦画瓢做分析的从业者。
2. 第一站数据源:从中国票房网拿 2018 年上映清单和票房,字段取舍是关键
做电影票房分析,听起来第一反应是去猫眼或艺恩找数据,但那个门槛高、还得登录。这个项目选的第一数据源是中国票房网(cbooo.cn),因为它有一个非常朴素的优点:按日更新、无登录门槛、结构是规整表格,用 requests 直接拿 HTML 就能解析。项目里的step0_chinamovies.py干的就是这件事:把 2018 年全年每一天的上映电影票房数据拉下来,合并成movie_data.csv。
我拆代码时发现一个值得学习的细节:它没有只抓「今日票房」一个值,而是把一场电影在 2018 年内的每日票房累加起来,这就让后续的「总排名.html」和「月份票房.html」有了数据基础。如果你只抓年度总票房汇总,那后面做月份维度分析就直接哑火。
下面是一段简化还原的采集核心逻辑,跟项目step0_chinamovies.py的思路一致:
import requests import pandas as pd import time import os # 中国票房网按日查询的 URL 模板,date 传参格式为 yyyy-MM-dd API_URL = "http://www.cbooo.cn/Movie/MovieDay/{}" HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": "http://www.cbooo.cn/moviesday?date=2018-01-01", } def fetch_daily_boxoffice(date_str): """获取某一天的票房榜单,返回 DataFrame 或 None""" try: resp = requests.get(API_URL.format(date_str), headers=HEADERS, timeout=10) # 接口实际返回的是 JSON 字符串,内容字段是 HTML 表格结构 data = resp.json() if data.get("data") is None: return None # pandas.read_html 直接解析内嵌的 table 标签 dfs = pd.read_html(data["data"]) return dfs[0] except Exception as e: print(f"[{date_str}] 采集失败: {e}") return None def load_existing_ids(path="movieid.txt"): """已经成功抓过的电影唯一标识,用于断点续采""" if not os.path.exists(path): return set() with open(path, "r", encoding="utf-8") as f: return set(line.strip() for line in f if line.strip()) movieid_done = load_existing_ids() frames = [] for day in pd.date_range("2018-01-01", "2018-12-31", freq="D"): date_str = day.strftime("%Y-%m-%d") df = fetch_daily_boxoffice(date_str) if df is None or df.empty: continue # 记录每部电影的唯一 ID:用上映日期 + 片名组成,避免同名电影冲突 df["movie_id"] = df["上映日期"].astype(str) + "_" + df["片名"] new_df = df[~df["movie_id"].isin(movieid_done)] frames.append(new_df) # 每采集一天就追加写入 movieid.txt,中断后重跑可以跳过已完成日期 with open("movieid.txt", "a", encoding="utf-8") as f: for mid in new_df["movie_id"]: f.write(mid + "\n") time.sleep(1) # 控制频率,避免被服务器拒绝 result = pd.concat(frames, ignore_index=True) result.to_csv("movie_data.csv", index=False, encoding="utf-8-sig")这段代码的逻辑不难,但有两个参数值得你特别注意:
date_range的 freq="D":按天遍历,意味着你可以把日期区间改成2023-01-01到2023-12-31,这套模板就直接变成 2023 年票房采集器。movieid.txt当断点续传标记:这是全项目最容易被忽视的设计。电影票房数据是日更的,你跑一次脚本可能要十几分钟,中间断网、被反爬、电脑休眠都会中断。没有这个文件,重跑就得全量重新抓;有它,重跑只补增量。我第一次跑这类脚本时没做这一步,断了一次之后对着几千行数据欲哭无泪。
还有个很实际的问题:跨年电影怎么算 2018 年票房?比如《前任3》是 2017 年 12 月 29 日上映的,但它的票房大头发生在 2018 年初。中国票房网在 2018 年 1 月的每日榜单里依然会出现它。如果你的分析目标是「2018 年大陆上映电影」,那严格来说应该看「上映日期在 2018 年内」;如果目标是「2018 年产生的票房」,那跨年片也得算。项目默认取的是前者——用上映日期做过滤,这样总排名.html的语义才是「2018 年上映电影的总票房排名」。你看源码时注意别把这个语义搞混了,这是后面所有图表口径的根。
3. 用豆瓣补全导演演员和评分:step1 与 step2 为什么拆成两个批次
光有票房数据是画不出「评分-票房关系图」的,因为中国票房网不给你豆瓣评分。项目用step1_doubanmovies.py、step1_doubanmovies_supplement.py和step2_moviedetail.py三个脚本解决这件事。很多人第一次看源码会疑惑:为什么一个「补全数据」的环节要拆三个脚本?
我的理解是这样的:step1_doubanmovies.py是第一批采集,跑完之后发现有一部分电影没拿到豆瓣条目——可能是片名不完全一致、可能是上映日期和豆瓣条目对不上,于是写了step1_doubanmovies_supplement.py做第二批补采。补采的落地方案是notexistmovie.txt:第一批没匹配上的电影会被记录到这个文件里,补采脚本读这个文件,逐条去豆瓣重新搜索。如果你是自己拿这套项目改年份,会非常依赖这个文件——它相当于一个「待办清单」。
然后是step2_moviedetail.py,这个脚本负责访问每部电影的豆瓣详情页,把导演、编剧、主演、评分、评价人数这些字段抽出来。行业里的常见做法是:先用豆瓣搜索接口拿到 subject_id(每部电影的豆瓣唯一编号),再用https://movie.douban.com/subject/{subject_id}/这个详情页地址去拿元数据。豆瓣详情页的 HTML 结构里有 JSON-LD 结构化数据,可以直接解析,比正则匹配要稳得多。下面是提取核心字段的思路:
import requests import re import json from lxml import etree DETAIL_URL = "https://movie.douban.com/subject/{}/" HEADERS = { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", } def parse_douban_detail(subject_id): """从豆瓣详情页解析评分、导演、演员等结构化字段""" resp = requests.get(DETAIL_URL.format(subject_id), headers=HEADERS, timeout=8) html = resp.text # 豆瓣详情页的 script 标签里内嵌了 JSON-LD 结构化数据 m = re.search(r'<script type="application/ld\+json">(.*?)</script>', html, re.S) if not m: return None data = json.loads(m.group(1).strip()) movie_info = { "douban_id": subject_id, "title": data.get("name"), "rating": None, "director": [], "actors": [], } # 评分不在 JSON-LD 里,需要单独从页面评分区域取 rating_m = re.search(r'<strong class="rating_num" property="v:average">([\d.]+)</strong>', html) if rating_m: movie_info["rating"] = float(rating_m.group(1)) # JSON-LD 里的 actor 字段是一串人名数组 for actor in data.get("actor", []): movie_info["actors"].append(actor.get("name")) for director in data.get("director", []): movie_info["director"].append(director.get("name")) return movie_info # 使用示例:拿到电影的豆瓣 subject_id 后逐条解析 # info = parse_douban_detail(26861685) # print(info)有几个点你在改这套代码时一定会碰到,先说结论省得踩坑:
- 评分字段必须单独提取。JSON-LD 里的
aggregateRating有的电影没有,而页面顶部的<strong class="rating_num">几乎一定有值。项目里拿豆瓣评分做「评分-票房」分析,所以这一步宁可用正则多耗一点,也别漏数据。 director和actor的 key 要灵活处理。旧版豆瓣页面 JSON-LD 用的是"director"和"actor",新版有的改成了复数格式,还有的直接不给全。你写解析器时最好先跑两部电影试试水,确认 key 再批量跑。- requests 默认 UA 很容易被豆瓣拒绝。我在测试时用过一个裸 UA,返回的 HTML 大小明显缩水,评分区域直接没有。加上浏览器完整 UA 之后正常。这不是什么高深反爬,就是最基本的「别让对方一眼看出你是脚本」。
最后说一句关于节奏的:豆瓣对单 IP 的访问频率非常敏感,项目里每个脚本跑完一部电影都time.sleep一下是标配。如果你想跑全量 2018 年电影(大概 500+ 部),我建议 sleep 设在 2 秒以上,并且分批次跑——上午跑一半,下午跑一半,别贪。豆瓣的反爬像玄学,有时候 1 秒没事,有时候连续 20 个请求就 418 了,做好重试和记录比啥都重要。
4. 多平台评分对比与影人票房统计:猫眼、时光网、IMDB 数据是怎么对齐的
如果你只拿豆瓣评分做分析,会被骂「单一口径」。这套项目高明的地方在于:它还从猫眼、时光网、IMDB 分别抓了评分,所以输出文件里有douban-imdb.html、douban-mtime.html、douban-maoyan.html三张对比图。这三张图的价值是回答一个很多人关心的问题:同一个电影,国内观众和国外观众的评分差异到底有多大?
实操上,不同网站的评分数据并不是「点一个按钮」就能全拿到的。猫眼和时光网的数据可以直接从页面拿到;IMDB 的评分则需要通过https://www.imdb.com/title/tt{id}/的页面解析,而且需要先完成「中文电影名 → IMDB id」的映射。这个映射是整个项目中比较脏的活,因为有些电影在 IMDB 上没有独立条目、有些英文译名和你预期不一样。项目里的处理方式是:没有条目就记空值,后续画对比图时丢弃 NaN 配对。
再看演员维度的统计。step3_celebrity.py干的事情很有创意:它把movie_data.csv(票房)和豆瓣拿到的影人数据(演员)合并,然后按演员聚合出「2018 年参演电影的累计票房」。输出cast_data.csv,最后画成演员参演电影总票房前十.html。这个分析视角不是「哪部电影卖得好」,而是「哪个演员 2018 年最能扛票房」。
下面是聚合逻辑的核心片段:
import pandas as pd # movie_data.csv 是票房数据,cast_data.csv 是影人-电影关联数据 movie_df = pd.read_csv("movie_data.csv", encoding="utf-8-sig") cast_df = pd.read_csv("cast_data.csv", encoding="utf-8-sig") # 合并两张表:以电影的唯一标识(片名+上映日期)为关联键 merged = pd.merge(cast_df, movie_df, on="movie_id", how="left") # 按演员汇总 2018 年参演电影的累计票房(单位:亿元) actor_stats = ( merged.groupby("actor_name")["boxoffice_yi"] .sum() .reset_index() .sort_values("boxoffice_yi", ascending=False) ) # 拿到前十演员 top10_actors = actor_stats.head(10) print(top10_actors)这个合并逻辑里有两个坑,项目源码里真实存在,我拆的时候体会很深:
- 关联键的选择。如果单纯用「片名」连接,会出现两个问题:同名电影(比如翻拍片)、片名中带空格或特殊字符。项目在 step0 阶段就生成的
movie_id(上映日期_片名)在这里起了大作用。这就是为什么我强调第一步的movieid.txt不只是个标记文件,它是整条链路的 join key。 - 演员票房去重。一部电影有主角也有配角,
cast_data.csv默认把电影票房同时挂到所有主演名下。这意味着如果你统计「演员累计票房」,一个参演了 6 部电影的配角,他的累计票房可能是 6 部电影的票房之和,看起来秒杀只演了一部电影的主角。这是口径问题,不算 bug,但你要知道这张图表达的是「参演电影的累计票房」,不是「个人贡献票房」。做分析报告时别混淆这两种说法。
多平台评分对比还有一个隐藏的价值点:不同网站评分体系的偏好差异。猫眼评分普遍偏高(观众打分习惯问题),时光网偏影迷,IMDB 偏国际视角。项目生成的douban-maoyan.html可以一眼看到哪些电影在豆瓣和猫眼之间分差超过 2 分——这通常暗示片子存在「口碑与票房倒挂」或「营销过猛导致反噬」。你拿这套资源做分析时,这个对比图是出彩点。
5. 常见问题与避坑:从豆瓣封 IP 到 pyecharts 渲染空白,五条血泪经验
我按自己实际复现这套项目时的经历,整理了五条最典型的故障记录。现象、原因、解决一条条给你列清楚,照着排查比翻源码快得多。
现象 1:step1_doubanmovies.py跑了几十部后,连续报 418 或 403。原因:豆瓣对同一 IP 的高频访问做了临时封禁,本质是请求频率超过了阈值。这不是你的代码写错了,是节奏问题。 解决:把请求间隔从 1 秒提高到 3~5 秒,并在代码里加入指数退避重试——失败后第一次等 5 秒,再失败等 10 秒、20 秒,直到成功。我用这个办法把中断率从 30% 降到了 5% 以下。
现象 2:读取movie_data.csv时中文文件名乱码。原因:Windows 下to_csv用了默认编码,而项目 README 里写的是 Linux/Windows 双环境,Windows 的 Excel 打开 utf-8 文件会乱码。 解决:统一用encoding="utf-8-sig"写 CSV。utf-8-sig会自动加 BOM 头,Excel 识别没问题,pandas 读回来也没问题。这是 Python 处理中文数据最常见的坑,没有之一。
现象 3:电影名匹配不上,导致豆瓣评分大量缺失。原因:中国票房网用的是公映名,豆瓣有时会带「(2018)」后缀或译名不同。比如某部引进片在票房网上写《头号玩家》,豆瓣条目却可能排在别名里。粗暴匹配必然丢数据。 解决:项目里的notexistmovie.txt就是为这个准备的。我跑的时候会把没匹配上的电影名打印出来,人工核对后把豆瓣的subject_id直接补进映射表。这是笨办法,但最可靠。别指望让脚本全自动处理,第一次跑必须人工兜底一次。
现象 4:pyecharts 生成的 HTML 打开是空白,或者没有图表只有一条线。原因:这大概率是 pyecharts 版本差异。项目写于 Python 3.6 时代,当时 pyecharts 可能是 v0.5.x 的写法(from pyecharts import Bar),而现在 pip 装的是 v1.x 以上(from pyecharts.charts import Bar)。v1 彻底改了 API 风格,add()变成了add_yaxis(),render路径参数也变了。 解决:先查你的pyecharts版本:pip show pyecharts。如果装的是新版,把movie_pyecharts.py里的旧式写法改成 v1 风格。一个典型的改动是bar.add("票房", y_axis)改为bar.add_yaxis("票房", y_axis),bar.render("总排名.html")路径参数方式不变但配置链结构完全不同。对很多人来说这一步是最耗时的。
现象 5:电影评分-票房.html 的散点图全部挤在左下角。原因:票房数据量级从几百万到几十亿,跨度四个数量级,直接用线性坐标轴会把小票房电影压成一堆点。 解决:对票房做log变换再绘图。用 pyecharts 画散点时,把x_axis数据手动做math.log10(boxoffice),坐标轴标签再还原成实际值。这样低票房和高票房电影的分布都能看清。
6. 用 movie_pyecharts.py 复现全部图表,并把「票房-评分」四象限变成你的分析武器
最后这一章,聊透movie_pyecharts.py和movie_pyecharts.ipynb。movie_pyecharts.py是「一键生成全部图表」的脚本,跑完它你会得到项目文件清单里的月份票房.html、类型-票房.html、类型-评分.html、电影评分-票房.html、总排名.html、演员参演电影总票房前十.html等全部可视化文件。而movie_pyecharts.ipynb是同一套逻辑的 notebook 版,适合你跑一步看一步地理解数据。
我最推荐你重点复现的是电影评分-票房.html这张散点图,因为它能拆出一个四象限判断模型。以豆瓣评分为纵轴、票房为横轴,把点分成四类:高评分+高票房(口碑票房双赢)、高评分+低票房(叫好不叫座)、低评分+高票房(烂片但卖座)、低评分+低票房(无人问津)。下面是用新版 pyecharts 画的散点图核心代码,和项目思路一致但 API 是 v1 风格:
import pandas as pd import math from pyecharts.charts import Scatter from pyecharts import options as opts df = pd.read_csv("movie_data.csv", encoding="utf-8-sig") # 去掉无评分或票房为空的行,保证画图点的有效性 df_valid = df.dropna(subset=["douban_rating", "boxoffice_yi"]) # 票房做对数变换,避免堆点 x_data = [math.log10(max(v, 0.01)) for v in df_valid["boxoffice_yi"]] y_data = df_valid["douban_rating"].tolist() labels = df_valid["title"].tolist() scatter = ( Scatter() .add_xaxis(x_data) .add_yaxis( "电影", [list(z) for z in zip(x_data, y_data)], label_opts=opts.LabelOpts(is_show=False), ) .set_global_opts( title_opts=opts.TitleOpts(title="2018 电影票房-豆瓣评分四象限"), xaxis_opts=opts.AxisOpts( name="票房(对数)", # 把对数刻度还原为显示真实亿元数值 axislabel_opts=opts.LabelOpts(formatter="10^{value}"), ), yaxis_opts=opts.AxisOpts(name="豆瓣评分"), tooltip_opts=opts.TooltipOpts( formatter="{c}", # 提示框显示数据点下标,配合 labels 做映射 ), ) ) scatter.render("电影评分-票房.html")这段代码的逻辑我已经跑顺了,但有两点你注意一下:
add_xaxis和add_yaxis的传参方式是 pyecharts v1 的写法,如果你的环境是 v0.5,得改回add("电影", x_data, y_data)。这个在前面避坑章节说过,这里再强调是因为它直接影响你能不能跑出第一张图。formatter="10^{value}"是为了让横轴显示成 10 的整数次幂,这样坐标轴标签读起来是真实票房量级(10^0=1 亿、10^1=10 亿)。如果你看得别扭,也可以在to_csv阶段直接生成一列boxoffice_log,画图时用那列当横轴数据。
跑完movie_pyecharts.py之后,我建议你做一次「验证数据正确性」的工作,别拿到图直接开始编报告。具体做法是:打开总排名.html,对比排名第一的电影票房是否和当年新闻公布的数据一致;再打开类型-票房.html,看看累计票房最高的类型是不是和当年大盘走势一致。这两步能过滤掉一半以上的数据采集错误。
我用这套项目做跨年复现时,最深的教训就是:拿到新数据的第一件事不是画图,而是先跑movieid.txt的断点逻辑确认采集完整度,再跑notexistmovie.txt的人工核对流程,最后才是可视化。有一次我跳过第二步直接进可视化,结果类型-票房那张图少了十几部电影的数据,比例完全失真。从那以后我每次处理新的年份数据,都强制走一遍「先跑 step0 确认行数、再补详情、最后出图」的流程。希望这套项目也能帮你把数据链路走通,少走我踩过的弯路,做出真正能讲出故事的可视化分析。
本文还有配套的精品资源,点击获取