简介:一份面向高校计算机相关专业毕业设计、课程设计的Python猫眼电影数据分析实战资源,覆盖数据爬取、清洗、存储到可视化全流程,尤其涉及字体反爬破解等实际难点,适合毕设参考或爬虫进阶学习。压缩包共77个文件,约4.79MB,以Python源码为核心,辅以HTML可视化报告、JSON/DB数据库文件、图片素材、配置文件及设计文档,各类型分工明确,便于按模块阅读运行,还附有运行环境与依赖说明。目前已有69人学习,可借鉴其项目框架与实现思路。内容涵盖完整可运行的爬虫与可视化代码、猫眼电影2018年票房/评分/产地等分析图表,以及配套说明文档;从项目搭建到功能完善均有清晰记录,还可在此基础上扩展其他题材,性价比高,适合需要快速落地相关课设或毕设的读者。读者可通过爬虫采集、数据入库、可视化展示的完整链路,掌握真实的爬虫反破解思路与数据分析方法,亦可为课程答辩或项目展示提供素材。
1. 从爬虫到可视化:猫眼电影毕设的全链路价值
毕业设计里能同时覆盖 Python 爬虫、数据分析和可视化三个方向的选题不多,猫眼电影 Top100 榜单是其中少有的“一片顶三片”。榜单结构固定、字段完整——排名、片名、主演、上映时间、评分都能直接从静态 HTML 里取,不需要处理复杂的 Cookie 流程;数据量又控制在 100 条级别,爬下来的数据放进 CSV 或 SQLite,用 pandas 做什么分析都顺手。另一个常被忽略的点是,这个题目自带业务场景:评分分布、年份趋势、演员关联这些结论,放回电影行业语境里都讲得通,论文“研究意义”部分不用硬凑。我们直接从从业角度给出可复现的实现方案,不依赖任何现成源码包,把爬虫抓取、数据质量处理、图表输出到最终报告的全链路讲清楚,新手照着做能跑通,熟手也能看到字段清洗和反爬参数的取舍。
2. Python 猫眼电影爬取的架构:模块划分、存储选型与依赖管理
2.1 requests 爬虫模块:为什么不用 Scrapy 和 Selenium
很多第一次做爬虫项目的人,一上来就在 Scrapy 和 Selenium 之间纠结。Scrapy 作为一个完整的爬虫框架,有 Downloader、Scheduler、Pipeline 一整条链路,工程上确实规范,但如果选题只是猫眼 Top100 这种单站单表的数据,Scrapy 的目录结构本身就占了项目一半的复杂度,调试成本全花在 Item 和 Pipeline 的流转上。Selenium 会把浏览器整个拉起来,解析速度慢,无头环境的配置也麻烦,拿来做单页静态数据属于杀鸡用牛刀。
所以这里直接选 requests。它是同步 HTTP 客户端,配合正则或 BeautifulSoup 解析 HTML,够用且直观。搜索热词里 Python 爬虫方向被提及最多的库就是 requests,底层逻辑很简单:拿 URL、带 headers、发 GET 请求、拿响应文本。先看一个最小请求写法:
import requests from fake_useragent import UserAgent ua = UserAgent() def get_page(url): headers = { "User-Agent": ua.random, "Referer": "https://www.maoyan.com/", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", } response = requests.get(url, headers=headers, timeout=10) response.raise_for_status() response.encoding = "utf-8" return response.text这里ua.random的作用是每次请求换一个 UA 字符串,让请求看起来来自不同浏览器版本;Referer带上首页地址是模拟从站内点进榜单的路径。raise_for_status()在返回 4xx/5xx 时直接抛异常,省去手动判断状态码的分支;encoding显式设成 utf-8,否则 requests 会按响应头里的编码猜测,中文片名偶尔会出现乱码。100 条数据分 10 页拉,同步请求加 sleep 整个跑完不到一分钟,这也是这个项目不需要上异步或多线程的关键理由。
2.2 SQLite 与 CSV 双存储怎么选,以及完整目录结构
存储上要讨论的是 CSV 和 SQLite 的取舍。CSV 的优点是人眼直接能看,Excel 打开就能检查字段;SQLite 的好处是查询方便、数据类型被强制约束,适合论文里写“数据库设计”一节时直接贴 SQL。对于 100 条规模的数据,CSV 完全够用,所以最终方案是原始数据落 CSV,分析过程从 CSV 读入内存;如果指导教师要求数据库内容,再写一个to_sql把 DataFrame 存入 SQLite,两条路都走通,报告里也能多写一块内容。
maoyan_top100/ ├── data/ │ ├── maoyan.csv │ └── maoyan.db ├── src/ │ ├── crawler.py # 爬虫模块 │ ├── analysis.py # 数据分析模块 │ └── visualize.py # 可视化模块 ├── output/ │ ├── bar_year.html │ ├── pie_score.html │ └── wordcloud_actors.html ├── requirements.txt └── README.md目录按功能拆分,不按文件类型拆。爬虫、分析、可视化三个模块各自独立,后面写报告时每个章节正好对应一个模块,也方便答辩时演示“三步运行”的流程。data 和 output 目录初始为空,运行时自动生成,README 里写清楚这一点,评分老师打开项目不会一头雾水。
2.3 requirements.txt 与运行顺序,报告里怎么描述依赖
毕设代码给别人的时候,依赖清单比代码本身更重要。requirements.txt 至少要包含下表内容:
| 包名 | 版本建议 | 用途 |
|---|---|---|
| requests | >=2.25 | 发 HTTP 请求 |
| fake-useragent | >=0.1.11 | UA 随机轮换 |
| beautifulsoup4 | >=4.9 | 备用 HTML 解析 |
| pandas | >=1.3 | 数据清洗与分析 |
| pyecharts | >=1.9 | 图表可视化 |
| jieba | >=0.42 | 中文分词,词云图用 |
jieba 很多人会漏掉。如果只统计演员名字的词频,确实不需要分词;但如果想把片名或短片简介丢进词云,中文会被切成单个汉字,生成效果很差。用jieba.lcut切一遍再统计,成本只有一行代码,效果完全不同。
运行顺序是python crawler.py生成 data/maoyan.csv,再python analysis.py输出统计指标,最后python visualize.py生成 output/ 下的 HTML 图表。三个脚本独立运行,报告里把这一条链路画成数据流图,比贴大段代码更能体现工程思维。
3. 猫眼 Top100 榜单爬取实现:offset 翻页、请求头与正则解析
3.1 榜单 URL 规则与页面结构分析
猫眼 Top100 榜单的入口是“排行榜-更早榜单-经典榜”,路径固定在/board/4。翻页不是常规的/page/2,而是/board/4?offset=0这种以榜单偏移量控制的方式,offset=0 是第一页,offset=10 是第二页,依此类推,每页 10 条。
用浏览器开发者工具查看页面源码,榜单里每部电影嵌在一个<dd>标签里,字段结构固定:
| 字段 | HTML 特征 |
|---|---|
| 排名 | <i class="board-index board-index-1">1</i> |
| 片名 | <a href="/films/123" title="肖申克的救赎"> |
| 主演 | <p class="star">主演:蒂姆·罗宾斯, 摩根·弗里曼</p> |
| 上映时间 | <p class="releasetime">上映时间:1994-09-10</p> |
| 评分 | <i class="integer">9.</i><i class="fraction">7</i> |
抓取的最佳策略是直接定位整个<dd>标签,在这个片段里做字段提取,而不是在整个页面里逐个 find class。前者逻辑清晰、不怕字段错位;后者一旦某个电影缺字段,后面的解析结果全都会串行。
3.2 爬虫代码:requests 请求、重试、UA 轮换与解析
基于上述设计,下面这份爬虫核心代码可以直接运行。它只用了 requests 同步请求加正则,没有框架层依赖,逐行解释起来也容易:
import requests import re import time import random import csv from fake_useragent import UserAgent def fetch_html(url, retries=3): ua = UserAgent() headers = { "User-Agent": ua.random, "Referer": "https://www.maoyan.com/board/4", } for attempt in range(1, retries + 1): try: resp = requests.get(url, headers=headers, timeout=10) resp.raise_for_status() resp.encoding = "utf-8" return resp.text except requests.RequestException as exc: print(f"第 {attempt} 次请求失败: {exc}") time.sleep(2 * attempt) return None def parse_movies(html): result = [] pattern = re.compile( r'<dd>.*?board-index.*?">(\d+)</i>' r'.*?title="(.*?)"' r'.*?class="star">主演:(.*?)</p>' r'.*?class="releasetime">上映时间:(.*?)</p>' r'.*?class="integer">(.*?)</i><i class="fraction">(.*?)</i>', re.S ) for match in pattern.finditer(html): rank, title, actors, release, score_int, score_frac = match.groups() result.append({ "rank": int(rank), "title": title.strip(), "actors": actors.strip(), "release_time": release.strip(), "score": float(score_int + score_frac), }) return result def crawl_top100(): all_movies = [] for offset in range(0, 100, 10): html = fetch_html(f"https://www.maoyan.com/board/4?offset={offset}") if html: all_movies.extend(parse_movies(html)) print(f"已抓取 offset={offset},累计 {len(all_movies)} 条") time.sleep(random.uniform(1, 3)) return all_movies这段代码有两个容易出问题的地方。第一个是正则里的.*?非贪婪匹配,只有配合re.S让.匹配换行,才能跨行匹配<dd>之间的完整内容;一旦漏掉re.S,finditer 什么都拿不到且不会报错。第二个是评分字段,猫眼把评分拆成整数部分和小数部分两个<i>标签,必须先拼接再转 float;直接取其中一个标签会导致评分只有个位数,分析时会出现 9.0 分占满全表的假象。
重试机制的time.sleep(2 * attempt)是线性退避,第一次失败等 2 秒,第二次等 4 秒。这个策略在答辩时会被问到,解释成“避免连续失败时请求频率过快,给服务器留恢复时间”即可。
3.3 字段清洗与 CSV 落盘,评分字段的坑
直接抓下来的 actors 字段带有“主演:”前缀和英文逗号,release_time 带有“上映时间:”前缀。清洗时要统一处理,方便后续 pandas 拆分:
def clean_and_save(movies, csv_path="data/maoyan.csv"): for m in movies: m["actors"] = m["actors"].replace(",", "、").strip() m["release_time"] = m["release_time"].replace("上映时间:", "").strip() with open(csv_path, "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=["rank", "title", "actors", "release_time", "score"]) writer.writeheader() writer.writerows(movies) print(f"已保存 {len(movies)} 条数据到 {csv_path}")注意 CSV 要写入utf-8-sig而不是utf-8。前置 BOM 是给 Excel 打开用的,不加 BOM 的中文 CSV 用 Excel 打开会乱码。这一步清洗逻辑单独写成函数,答辩时能清晰指出数据质量处理的环节,比笼统说“做了清洗”有说服力得多。
4. 数据分析:评分分布、年份趋势与主演词频的 pandas 实现
4.1 读取与数据清洗:类型转换、空值处理
爬虫落盘到 CSV 之后,pandas 读进来第一件事不是直接做图,而是检查数据质量。数据质量决定后面分析有没有说服力。pd.read_csv读入后,立刻看每列的 dtype 和缺失值:
import pandas as pd df = pd.read_csv("data/maoyan.csv", encoding="utf-8-sig") print(df.info()) print(df.isna().sum())df.info()会打印每列的非空数量与数据类型。实际跑下来,release_time 是 object 类型,必须用pd.to_datetime转成 datetime64,后续按年聚合才能用.dt访问器。再把 score 用astype(float)做一次强转,这行有一个隐藏作用:如果 CSV 里某一行评分不是数字,这里会直接抛 ValueError,把坏数据在分析前拦下来。
df["release_time"] = pd.to_datetime(df["release_time"]) df["year"] = df["release_time"].dt.year df["score"] = df["score"].astype(float)4.2 三个分析维度以及对应代码
这个项目最值得做的分析有三个:评分分布、年份趋势、主演词频。
评分分布看 Top100 的得分集中在哪个区间,能直接反映榜单含金量;年份趋势看每年上榜电影数量,能说明榜单的时间跨度是否覆盖老片和新片;主演词频把 100 部电影的主演全部拆开统计,得到“上榜最多演员榜单”,这是报告里最有故事性的一个表。
# 评分分布描述性统计 score_desc = df["score"].describe() # 年份趋势 year_count = df.groupby("year").size().sort_index(ascending=False) # 主演词频 actor_counter = {} for actors in df["actors"].str.split("、"): for actor in actors: actor = actor.strip() actor_counter[actor] = actor_counter.get(actor, 0) + 1 actor_df = pd.Series(actor_counter).sort_values(ascending=False).head(20)分析代码的坑在str.split("、")这一步:第 3 章清洗时如果只处理了英文逗号而漏掉全角逗号,拆分出来的同一个演员会被统计成两个人。第 3 章 replace 的目标就是把逗号统一成顿号,这里才能放心 split。.dt.year取年份后,groupby 不会自动补全没有上榜电影的年份,画图时横轴会有空洞,后面会处理。
4.3 分析结果的呈现方式与报告素材
pandas 的 describe 输出适合直接复制进 Word 表格,但标题要改写,不能叫“describe 输出”。例如评分均值为 9.0、最高 9.7、最低 8.3 这种结论,适合改写为一句业务语言:“Top100 中超过六成影片评分在 9 分以上,头部分化明显,9.5 分以上属于绝对的经典阵营。”
年份趋势的结果需要补全缺失年份再画图:
year_count = year_count.reindex(range(1994, 2020), fill_value=0)这里 range 的终点 2020 需要根据实际抓取数据调整,如果榜单里有 2023 年的电影,终点就取 2024。固定写死年份在答辩时会被问“为什么是 2020”,回答“由数据范围确定,脚本里可以改为df['year'].min()和df['year'].max()动态生成”会更严谨。这里得到的三个聚合结果,正好决定下一章要选什么图表:评分分布用饼图或直方图,年份趋势用柱状图,主演词频用词云。
5. 可视化图表:pyecharts 柱状图、饼图、词云与图形化大屏
5.1 pyecharts 基础配置
pyecharts 1.x 之后是链式调用风格,每个 add 和 set_ 方法返回图表对象本身,最后 render 成独立 HTML。这个 HTML 不需要后台服务,双击就能在浏览器看效果,对毕设来说是最省事的一环。不需要 Node、不需要 Nginx,把 HTML 放进报告附录也没问题。
from pyecharts.charts import Bar from pyecharts import options as opts bar = ( Bar() .add_xaxis(year_count.index.tolist()) .add_yaxis("上榜数量", year_count.values.tolist()) .set_global_opts( title_opts=opts.TitleOpts(title="Top100电影上榜年份分布"), xaxis_opts=opts.AxisOpts(name="年份"), yaxis_opts=opts.AxisOpts(name="数量"), ) ) bar.render("output/bar_year.html")这里最容易踩的坑是add_xaxis要求传列表,而year_count.index是 Index 对象,必须显式tolist(),否则某些 pyecharts 版本会报类型错误。values.tolist()同理。中文标题不需要额外配置字体,pyecharts 生成的 HTML 页面会读本地系统字体。
5.2 饼图、词云图的代码与参数说明
饼图适合展示评分段占比。先把评分切成区间,再统计各段数量:
bins = [8, 8.5, 9, 9.5, 10] labels = ["8-8.5", "8.5-9", "9-9.5", "9.5-10"] df["score_range"] = pd.cut(df["score"], bins=bins, labels=labels, right=False) range_count = df["score_range"].value_counts().sort_index() from pyecharts.charts import Pie pie = ( Pie() .add("", [list(z) for z in zip(range_count.index.tolist(), range_count.values.tolist())]) .set_global_opts(title_opts=opts.TitleOpts(title="评分区间占比")) .set_series_opts(label_opts=opts.LabelOpts(formatter="{b}: {d}%")) ) pie.render("output/pie_score.html")add第二参数要求[(name1, value1), (name2, value2), ...]这种二元组列表,直接用zip再包一层list是常见写法。formatter里的{b}是名称,{d}是占比百分比,{c}是原始数值,这三个占位符在答辩时被问到的概率很高,要能说清楚区别。
词云图可以用 pyecharts 的 WordCloud 类:
from pyecharts.charts import WordCloud wordcloud = ( WordCloud() .add("", actor_df.items(), word_size_range=[20, 100], shape="circle") .set_global_opts(title_opts=opts.TitleOpts(title="上榜演员词云")) ) wordcloud.render("output/wordcloud_actors.html")actor_df.items()返回可迭代的元组对,是 WordCloud 最直接的输入格式。word_size_range控制词的显示字号范围,默认的 20-80 在高频词只有十几个时显得稀疏,调到 20-100 后视觉层级更明显。shape参数支持 circle、diamond、triangle 等形状,但不要用太花哨的轮廓,词云的可读性比形状重要。
5.3 图表导出与拼接可视化大屏
上面三个图是单独的 HTML 文件,报告里各自截图即可。如果指导教师想要“可视化大屏”的效果,常见做法是用 pyecharts 的 Page 类把多个图合并到一个页面:
from pyecharts.charts import Page page = Page(layout=Page.DraggablePageLayout) page.add(bar, pie, wordcloud) page.render("output/dashboard.html")生成后用鼠标拖拽调整布局,再做一次page.save_resize_html把坐标固化成独立文件。这一步不需要写配置代码,DraggablePageLayout 模式自带“保存布局”按钮。关于可视化大屏,有两条建议:第一,不要把图表背景色堆得大红大紫,电影榜单用深色底或纯白底都行,保持统一;第二,不要在报告里硬上地图可视化,Top100 榜单里没有地域字段,硬凑省份分布做出来的地图没有业务含义,答辩时反而会被问住。
6. 把项目做成完整毕设:报告结构、答辩要点与常见坑
6.1 报告结构怎么匹配评分点
高分报告不要求技术多难,要求每个评分点都有对应章节。最稳的结构是:摘要里写明“爬虫-清洗-分析-可视化”四段式;第二章写需求分析与技术选型;第三章写爬虫设计与实现,包括反爬策略和重试机制;第四章写数据清洗与统计分析;第五章写可视化展示与业务结论。代码不要全文贴进正文,附录放核心代码段,正文引用运行结果截图,这符合学校对“系统实现”章节的常见预期。
6.2 答辩现场最容易被问的三个问题
第一个问题:“如果 100 条数据不够怎么办?”回答方向是扩展到全站电影的方法:把 offset 上限抬高、处理动态加载接口、再考虑更严格的反爬限制。这个问题考察的不是代码量,而是有没有想过数据规模变化时的架构影响。
第二个问题:“为什么只设计了 5 个字段?”要说明为了分析聚焦,舍弃了评论数、票房等字段;如果指导教师希望看到更多字段,可以说明在爬虫正则里扩展字段的路径,比如加一个review_count的正则分组。
第三个问题:“反爬具体是怎么做的?”回答三个要点:UA 随机、Referer 伪装、sleep 随机间隔。这三个点对应代码里三处细节,比笼统说“用了代理”更实在,也更符合实际项目里小规模爬取的真实策略。
6.3 给代码“保鲜”的最后一招
提交前把每个脚本都加上清晰的打印输出:爬虫每次打印当前 offset 和累计条数,分析模块打印描述性统计结果,可视化模块在 render 后打印“已生成 XX.html”。这看起来不起眼,但评分老师通常不会逐行读 pandas 代码,能看到清晰的运行过程比看注释有效得多。
答辩现场跑一遍三个脚本,同时把控制台输出的清洗前后对比保留下来,会比朗读代码更有说服力。数据爬取、数据分析、可视化这三段恰好对应运行时的三次输出,整个项目的完成度就完整落在了一个可演示的流程上。
本文还有配套的精品资源,点击获取