
简介基于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?date2018-01-01, } def fetch_daily_boxoffice(date_str): 获取某一天的票房榜单返回 DataFrame 或 None try: resp requests.get(API_URL.format(date_str), headersHEADERS, timeout10) # 接口实际返回的是 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(pathmovieid.txt): 已经成功抓过的电影唯一标识用于断点续采 if not os.path.exists(path): return set() with open(path, r, encodingutf-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, freqD): 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, encodingutf-8) as f: for mid in new_df[movie_id]: f.write(mid \n) time.sleep(1) # 控制频率避免被服务器拒绝 result pd.concat(frames, ignore_indexTrue) result.to_csv(movie_data.csv, indexFalse, encodingutf-8-sig)这段代码的逻辑不难但有两个参数值得你特别注意date_range的 freqD按天遍历意味着你可以把日期区间改成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), headersHEADERS, timeout8) html resp.text # 豆瓣详情页的 script 标签里内嵌了 JSON-LD 结构化数据 m re.search(rscript typeapplication/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(rstrong classrating_num propertyv: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 classrating_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, encodingutf-8-sig) cast_df pd.read_csv(cast_data.csv, encodingutf-8-sig) # 合并两张表以电影的唯一标识片名上映日期为关联键 merged pd.merge(cast_df, movie_df, onmovie_id, howleft) # 按演员汇总 2018 年参演电影的累计票房单位亿元 actor_stats ( merged.groupby(actor_name)[boxoffice_yi] .sum() .reset_index() .sort_values(boxoffice_yi, ascendingFalse) ) # 拿到前十演员 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 渲染空白五条血泪经验我按自己实际复现这套项目时的经历整理了五条最典型的故障记录。现象、原因、解决一条条给你列清楚照着排查比翻源码快得多。现象 1step1_doubanmovies.py跑了几十部后连续报 418 或 403。原因豆瓣对同一 IP 的高频访问做了临时封禁本质是请求频率超过了阈值。这不是你的代码写错了是节奏问题。 解决把请求间隔从 1 秒提高到 35 秒并在代码里加入指数退避重试——失败后第一次等 5 秒再失败等 10 秒、20 秒直到成功。我用这个办法把中断率从 30% 降到了 5% 以下。现象 2读取movie_data.csv时中文文件名乱码。原因Windows 下to_csv用了默认编码而项目 README 里写的是 Linux/Windows 双环境Windows 的 Excel 打开 utf-8 文件会乱码。 解决统一用encodingutf-8-sig写 CSV。utf-8-sig会自动加 BOM 头Excel 识别没问题pandas 读回来也没问题。这是 Python 处理中文数据最常见的坑没有之一。现象 3电影名匹配不上导致豆瓣评分大量缺失。原因中国票房网用的是公映名豆瓣有时会带「2018」后缀或译名不同。比如某部引进片在票房网上写《头号玩家》豆瓣条目却可能排在别名里。粗暴匹配必然丢数据。 解决项目里的notexistmovie.txt就是为这个准备的。我跑的时候会把没匹配上的电影名打印出来人工核对后把豆瓣的subject_id直接补进映射表。这是笨办法但最可靠。别指望让脚本全自动处理第一次跑必须人工兜底一次。现象 4pyecharts 生成的 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, encodingutf-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_optsopts.LabelOpts(is_showFalse), ) .set_global_opts( title_optsopts.TitleOpts(title2018 电影票房-豆瓣评分四象限), xaxis_optsopts.AxisOpts( name票房对数, # 把对数刻度还原为显示真实亿元数值 axislabel_optsopts.LabelOpts(formatter10^{value}), ), yaxis_optsopts.AxisOpts(name豆瓣评分), tooltip_optsopts.TooltipOpts( formatter{c}, # 提示框显示数据点下标配合 labels 做映射 ), ) ) scatter.render(电影评分-票房.html)这段代码的逻辑我已经跑顺了但有两点你注意一下add_xaxis和add_yaxis的传参方式是 pyecharts v1 的写法如果你的环境是 v0.5得改回add(电影, x_data, y_data)。这个在前面避坑章节说过这里再强调是因为它直接影响你能不能跑出第一张图。formatter10^{value}是为了让横轴显示成 10 的整数次幂这样坐标轴标签读起来是真实票房量级10^01 亿、10^110 亿。如果你看得别扭也可以在to_csv阶段直接生成一列boxoffice_log画图时用那列当横轴数据。跑完movie_pyecharts.py之后我建议你做一次「验证数据正确性」的工作别拿到图直接开始编报告。具体做法是打开总排名.html对比排名第一的电影票房是否和当年新闻公布的数据一致再打开类型-票房.html看看累计票房最高的类型是不是和当年大盘走势一致。这两步能过滤掉一半以上的数据采集错误。我用这套项目做跨年复现时最深的教训就是拿到新数据的第一件事不是画图而是先跑movieid.txt的断点逻辑确认采集完整度再跑notexistmovie.txt的人工核对流程最后才是可视化。有一次我跳过第二步直接进可视化结果类型-票房那张图少了十几部电影的数据比例完全失真。从那以后我每次处理新的年份数据都强制走一遍「先跑 step0 确认行数、再补详情、最后出图」的流程。希望这套项目也能帮你把数据链路走通少走我踩过的弯路做出真正能讲出故事的可视化分析。本文还有配套的精品资源点击获取