如果你正在为数据科学与大数据技术专业的毕业设计或课程设计发愁,那"基于Python的电影票房爬取与可视化系统"这个题目十有八九出现在你的候选清单里。它看起来门槛不高——爬点票房数据,画几张图表,似乎一周就能搞定;但真正动手后你会发现,这个项目同时牵扯到HTTP请求、反爬对抗、数据清洗、数据库建模、Web后端、前端图表适配,几乎把数据岗位日常的技术栈完整串了一遍。这篇文章我按自己的实操经验,从需求拆解、数据源选型、爬虫实现、清洗入库、可视化落地到答辩准备,把整个系统的搭建过程完整过一遍。适合正在做这个课题的在校生,也适合想用Python快速搭建一个数据采集分析Demo的入门开发者。
1. 项目定位拆解:从标题到可落地的功能清单
1.1 一句话标题背后藏着哪些模块
很多同学拿到题目就直接开爬,结果爬到一半发现不知道数据存哪、画完图不知道页面怎么串起来,最后交付的东西像一堆零散脚本。我建议先花半天时间把标题拆开,搞清楚"电影票房爬取与可视化系统"实际上包含四个可独立验收的模块:
- 数据采集模块:从公开票房平台定时抓取电影票房数据,包括单日票房、累计票房、上映天数、排片场次、上座率等字段;
- 数据存储模块:把非结构化的网页数据清洗成结构化记录,写入数据库,保证后续可以被反复查询和分析;
- 数据分析模块:对票房数据做排序、汇总、同比环比计算,为可视化提供符合前端展示口径的数据集;
- 可视化展示模块:通过Web页面呈现票房排行、走势、占比等信息,支持筛选和交互。
如果你的课题名称带了"大数据技术"前缀,还要额外考虑数据量的积累问题。单日抓一次数据量很小,但如果设计成每天定时抓取、持续运行几个月,再叠加历史票房数据,就有了"数据从少到多、存储和查询方案需要跟着演进"这条故事线,这在后续论文和答辩里是很有分量的素材。
1.2 分层架构与数据流向
这个项目我推荐的架构是经典的四层结构,数据从源头单向流动,层次清晰,写文档也好画图:
采集层(Python + Requests) →存储层(MySQL / SQLite) →处理层(Pandas + PyMySQL) →展示层(Flask + ECharts)。
采集层只负责拿数据,不负责业务判断;存储层只负责持久化;处理层负责把数据库里的原始记录变成前端能直接消费的聚合结果;展示层只做数据呈现和用户交互。每一层之间通过明确的接口衔接——采集层产出原始JSON,存储层产出表记录,处理层产出视图或API响应,展示层消费JSON。
这个分层的核心价值在于"职责单一"。如果爬虫代码里混着数据库写入逻辑,清洗逻辑又塞在Flask路由函数里,前期写起来快,后期改一个字段就要动三处代码,调试成本非常高。我见过不少同学的系统最终跑不起来,问题不在某个环节多难,而是层与层之间耦合太紧,一个小异常就能拖垮整条链路。
1.3 为什么选Python而不换Java或Node
这不是情怀问题,是效率问题。Python在这类数据采集分析项目里有三个不可替代的优势:
第一,爬虫生态成熟。Requests处理HTTP请求、BeautifulSoup和lxml解析HTML、json处理接口响应,这几个库组合起来,写一个稳定爬虫的成本极低。用Java写同样功能,HttpClient加上Jsoup虽然也能做,但代码量至少多一倍。
第二,数据分析链路原生无缝。爬完数据直接用Pandas做清洗,用PyMySQL写回数据库,再用Flask提供API,全程都是Python对象在流转,没有跨语言序列化的麻烦。这也正好卡中"数据科学与大数据技术"专业的课程体系——Pandas和NumPy本来就是必修内容,用在这个项目里属于学以致用。
第三,可视化方案灵活。ECharts虽然是前端框架,但Python生态里有PyECharts库,可以在后端直接生成图表配置,也能用Flask + 原生ECharts做更自由的前后端分离方案。起步门槛低,进阶天花板也不低。
不过Python也有它的毛病,最典型的是全局解释器锁(GIL)导致多线程爬虫效率有限。但这个项目的数据量级完全不需要上多线程、多进程那套复杂方案,单线程加合理延时就能在几分钟内完成一次全量抓取。
2. 数据源选型与爬虫核心实现
2.1 四个候选数据源的横向对比
写爬虫第一步不是写代码,是选数据源。数据源选得不对,后面所有工作都会在反爬和字段不全这两个坑里反复挣扎。我对当时调研的几个公开数据源做了个对比:
| 数据源 | 数据完整度 | 获取难度 | 是否适合学习项目 |
|---|---|---|---|
| 猫眼专业版 | 高,字段丰富 | 有反爬,接口返回JSON | 推荐,技术含量适中 |
| 灯塔专业版 | 高,更新及时 | 登录门槛高,接口加密较强 | 不推荐新手 |
| 艺恩数据 | 中,偏行业报告 | 部分数据需权限 | 可作为辅助数据源 |
| Box Office Mojo | 高,全球数据 | 国内访问不稳定,英文 | 适合扩展对比分析 |
我最终选的是猫眼专业版的榜单数据,核心原因有三个:一是它的榜单数据通过JSON接口返回,不需要解析复杂的HTML嵌套;二是字段齐全,片名、类型、上映日期、当日票房、累计票房、排片场次、上座率都有;三是公开的票房排行本身就是可被检索的公共信息,用于课程设计和技术学习是合适的。
这里要特别说一句:爬虫项目一定要遵守目标网站的robots协议,控制请求频率,抓取的数据仅用于学习研究,不要用于任何商业用途。这不是套话,是每个爬虫开发者必须养成的职业习惯。你的毕业设计论文里也建议写上"遵守robots协议、数据仅供学术研究"这一条,答辩时老师会很在意这个意识。
2.2 以票房榜单接口为例的请求与解析
选定数据源后,打开浏览器开发者工具,切到Network面板,刷新榜单页面,找到返回JSON数据的XHR请求。这类接口通常需要带两个关键请求头:User-Agent告诉服务器你是浏览器,Referer标识你从哪个页面跳转过来。缺少这两个头,服务器很容易直接拒绝服务。
下面是一段基础请求代码,结构上可以复用到不同平台的榜单接口:
import requests import time import json def fetch_rank_data(date: str) -> list: """抓取指定日期的票房排行数据,返回解析后的列表。""" 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://piaofang.maoyan.com/", } # 接口路径以你在开发者工具里抓到的实际请求为准 url = "https://piaofang.maoyan.com/rankings/year" params = {"date": date} resp = requests.get(url, headers=headers, params=params, timeout=10) resp.raise_for_status() data = resp.json() # 不同的榜单接口返回结构不一样,这里以常见的 list 字段为例 items = [] for item in data.get("list", []): items.append( { "film_name": item.get("movieName"), "release_date": item.get("releaseInfo"), "daily_box_office": item.get("boxDesc", "").replace(",", ""), "cumulative_box_office": item.get("sumBoxDesc", "").replace(",", ""), "show_count": item.get("showCount"), "avg_seat_rate": item.get("avgSeatView"), } ) time.sleep(1) # 控制请求频率,别给服务器造成压力 return items if __name__ == "__main__": result = fetch_rank_data("2025-01-01") with open("raw_data.json", "w", encoding="utf-8") as f: json.dump(result, f, ensure_ascii=False, indent=2)这段代码看起来简单,但有几个细节值得注意。time.sleep(1)不是随手写的,是给请求之间留出间隔,模拟人工浏览节奏;resp.raise_for_status()会在HTTP返回4xx、5xx时直接抛出异常,避免把错误页面当正常数据解析;ensure_ascii=False保证中文字段以可读形式写入文件,而不是变成\uXXXX转义序列。
抓完数据先落一份原始JSON文件,这一步很多人会跳过,直接写数据库。但保留原始数据是数据开发的基本素养——清洗逻辑可能会改,数据库表结构可能会调,原始数据保留着一份,任何时候都能重新跑清洗流程,不用重新爬一遍。
2.3 反爬应对和请求纪律
反爬是这个项目最容易被问"为什么"的地方。本质上,网站反爬和爬虫对抗的根源是"服务器无法区分正常用户和自动化脚本"。应对思路也从来不是绕过,而是让请求看起来更像真实用户。
我自己在项目里用到的三个稳妥手段:
- 请求头完整化:除了User-Agent和Referer,补上Accept、Accept-Language等常见头字段,模拟浏览器的HTTP特征;
- 请求频率控制:单次请求后延时1到3秒,避免短时间高频访问触发限流。这个延时用随机值比固定值效果更好,代码上可以用
time.sleep(random.uniform(1, 3)); - 失败重试与日志记录:请求失败时不要无脑重试,先记录状态码和失败原因,间隔5秒后重试,最多重试3次。
如果触发了更严格的风控,服务器返回验证码或直接封禁IP,我的建议是停止继续抓取,再从协议合规性、请求频率和请求头三个维度逐一自查。很多所谓"被封"其实只是请求频率太高,把延时调大就能缓解。
我的原则是:学习项目里没有必要去做突破验证码、绕过签名这类对抗性操作,一旦你做了,项目论文的"合规性"就讲不清楚了。控制在合理范围内的基础反爬应对,已经足够支撑整个毕业设计的技术深度。
3. 数据清洗、去重与入库设计
3.1 原始数据三大问题:脏、乱、重复
爬虫拿到的是半结构化数据,直接可视化的结果通常惨不忍睹。我在第一次跑通全流程时,就遇到过三种典型脏数据:
第一是单位混用。"当日票房"这个字段,有的返回"1.2亿",有的返回"3500万",还有的返回"928.4万",Pandas排序时按字符串排,"1.2亿"会排在"3500万"前面,完全错误。必须统一转换成数值型的"元"为单位。
第二是字段缺失。新上映电影可能没有累计票房,冷门电影可能没有排片数据,接口里这些字段直接返回空字符串或null。如果不清洗就入库,后续SQL聚合会出现NULL传播,图表上则表现为空白或0值,分析结论全偏。
第三是重复记录。连续几天抓同一部电影的数据,如果只按片名去重,会误删历史记录;如果不去重,同一部电影每天的数据会写成多行。去重逻辑必须建立在"片名 + 统计日期"这个业务键上。
3.2 Pandas清洗流程与单位转换
用Pandas做清洗是最顺手的方案,流程清晰,每一步都能看到中间结果。核心代码如下:
import pandas as pd def convert_to_number(value) -> float: """把'1.2亿'、'3500万'这类字符串统一转成以元为单位的数值。""" if value is None or value == "": return 0.0 value = str(value).strip() if "亿" in value: return float(value.replace("亿", "")) * 100_000_000 if "万" in value: return float(value.replace("万", "")) * 10_000 return float(value) def clean_data(raw_path: str, output_path: str) -> pd.DataFrame: df = pd.read_json(raw_path) # 1. 日期标准化 df["stat_date"] = pd.to_datetime(df["stat_date"]) # 2. 票房字段统一转数值 df["daily_box_office"] = df["daily_box_office"].apply(convert_to_number) df["cumulative_box_office"] = df["cumulative_box_office"].apply(convert_to_number) # 3. 缺失值填充 df["show_count"] = df["show_count"].fillna(0) # 4. 按业务键去重,保留最近一次抓取结果 df = df.drop_duplicates(subset=["film_name", "stat_date"], keep="last") # 5. 过滤异常数据 df = df[df["daily_box_office"] >= 0] df.to_csv(output_path, index=False, encoding="utf-8-sig") return df这里有个容易忽略的细节:文件输出用utf-8-sig编码而不是utf-8。前者会在文件开头写入BOM头,让Excel直接打开CSV时中文不乱码,这个经验在答辩演示Windows环境时很管用。
单位转换为什么要统一到"元"而不是保留"万"?因为后续可视化要做排序和堆叠,数值型数据放图表y轴才能正确计算刻度;保留字符串单位只会让前端图表拿到一坨没法加工的文本。
3.3 表结构设计和增量更新思路
清洗后的数据要落库。数据库选MySQL还是SQLite取决于你机器的环境。如果你是第一次做完整项目,我建议先用SQLite,零配置、单文件、Python标准库就支持;如果课题明确要求"大数据存储方案"或生产环境模拟,再上MySQL,写法和SQL语句几乎一致,差别主要在连接驱动。
核心表设计如下:
CREATE TABLE movie_daily_box ( id INTEGER PRIMARY KEY AUTOINCREMENT, film_name TEXT NOT NULL, stat_date DATE NOT NULL, release_date DATE, daily_box_office REAL DEFAULT 0, cumulative_box_office REAL DEFAULT 0, show_count INTEGER DEFAULT 0, avg_seat_rate REAL, crawl_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE (film_name, stat_date) );这张表的主键是自增id,但业务唯一键是(film_name, stat_date)。唯一约束保证了同一部电影同一天的记录不会重复,配合INSERT OR REPLACE或INSERT ... ON DUPLICATE KEY UPDATE,就能实现"有则更新、无则插入"的增量写入。
每天抓取的时间也记录在表里,crawl_time字段是给自己留的追溯依据。如果你发现某天的数据源接口异常导致数值不对,可以通过crawl_time定位是哪一次抓取写进去的,直接修正或删除重爬。
4. 可视化系统的实现路径:Flask + ECharts
4.1 后端接口怎么把数据交给前端
可视化部分我采用Flask提供API、前端用ECharts渲染的方案。这个组合的好处是前后端职责清晰,爬虫和数据处理都是Python,后端接口也用Python,整条链路语言统一。
先设计接口的返回格式,这一步直接决定前端图表好不好写。我强烈建议所有接口统一返回{ "code": 0, "data": [...] }结构,前端统一做错误处理,而不是每个接口返回不同风格的JSON。接口设计如下:
from flask import Flask, jsonify from flask_cors import CORS import pandas as pd app = Flask(__name__) CORS(app) DB_PATH = "box_office.db" def query_df(sql: str) -> pd.DataFrame: import sqlite3 conn = sqlite3.connect(DB_PATH) df = pd.read_sql_query(sql, conn) conn.close() return df @app.route("/api/box_office/top10") def top10(): df = query_df( """ SELECT film_name, SUM(daily_box_office) AS total_box FROM movie_daily_box GROUP BY film_name ORDER BY total_box DESC LIMIT 10 """ ) return jsonify( { "code": 0, "data": df.to_dict(orient="records"), } ) @app.route("/api/box_office/trend") def trend(): df = query_df( """ SELECT stat_date, SUM(daily_box_office) AS daily_total FROM movie_daily_box WHERE stat_date >= date('now', '-30 day') GROUP BY stat_date ORDER BY stat_date """ ) return jsonify( { "code": 0, "data": df.to_dict(orient="records"), } ) if __name__ == "__main__": app.run(debug=True, port=5000)这里用Pandas读取SQL查询结果再转成records字典,省去了手动遍历游标组装JSON的麻烦,代码量少一半。对毕业设计这个数据量级,性能完全够用;如果数据量上了百万行,后续可以换成直接写原生SQL聚合、前端分页加载,这是天然的论文优化方向。
4.2 不同图表的选型逻辑
可视化不是把数据丢进图表就完事,每种图表都应该回答一个具体问题。我在做页面布局时,对图表类型和业务问题的对应关系做了如下规划:
| 图表类型 | 回答的问题 | 对应数据 |
|---|---|---|
| 柱状图 | 哪部电影票房最高 | 票房Top10排行 |
| 折线图 | 大盘票房随日期的走势如何 | 近30天全市场每日总票房 |
| 饼图 | 不同类型电影的票房占比 | 按影片类型汇总票房 |
| 散点图 | 上映天数与票房是否有相关性 | 每部电影上映天数与累计票房 |
以票房Top10柱状图为例,ECharts接收后端返回的数据后,核心配置如下:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>电影票房可视化系统</title> <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> </head> <body> <div id="top10_chart" style="width: 100%; height: 400px;"></div> <script> const chart = echarts.init(document.getElementById('top10_chart')); fetch('/api/box_office/top10') .then(res => res.json()) .then(res => { const data = res.data; chart.setOption({ title: { text: '票房Top10', left: 'center' }, tooltip: {}, xAxis: { type: 'category', data: data.map(item => item.film_name), axisLabel: { rotate: 30 } }, yAxis: { type: 'value', name: '票房(元)' }, series: [{ type: 'bar', data: data.map(item => item.total_box), itemStyle: { color: function(params) { // 第一名用醒目颜色,弱化后续排名 return params.dataIndex === 0 ? '#d4380d' : '#91caff'; } } }] }); }) .catch(err => console.error(err)); </script> </body> </html>注意axisLabel.rotate: 30这个配置,电影名通常较长,不旋转会互相遮挡,这是很常见的显示细节。另外图表标题一定要写清楚,答辩时老师扫一眼页面就知道这张图在表达什么,而不需要你在旁边解释半天。
4.3 从静态图到"系统":筛选、联动与定时刷新
如果只是几个静态图表拼在一起,这个项目的"系统感"是不够的。我建议至少补三个功能,工作量不大但对完成度提升明显:
- 日期筛选:页面顶部放一个日期选择器,切换后所有图表联动刷新。实现上就是前端监听日期变化,重新请求带日期参数的接口,后端SQL加
WHERE stat_date = ?条件; - 类型筛选:下拉框按电影类型(动作、喜剧、动画等)过滤饼图和柱状图,让不同维度的分析能自由组合;
- 定时数据刷新:用APScheduler在每天固定时间触发一次爬虫任务,让系统真正实现"自动采集 → 自动入库 → 手动刷新页面查看最新数据"的闭环。
from apscheduler.schedulers.background import BackgroundScheduler def start_scheduler(): scheduler = BackgroundScheduler() scheduler.add_job( func=daily_crawl_job, trigger="cron", hour=9, minute=30, id="daily_box_office_crawl", ) scheduler.start()daily_crawl_job函数里就是把前面写的fetch_rank_data、清洗、入库三个步骤串起来。加了这个定时任务后,你的系统就具备了"持续运行、自动产出"的数据管道特征,论文里可以顺势引出"轻量级数据管道设计"这个技术点,比单纯爬虫+可视化的立意高一个层级。
5. 实操中真正踩过的坑与完整排查过程
5.1 数字乱码:字体反爬的排查链路
我第一次抓票房数字时,页面显示正常的"1200.5万",可拿到的源码里却是一串像臤、翂这样的乱码字符。初看以为是编码问题,换了各种charset都一样,折腾了一个多小时。
后来我打开开发者工具,在Elements面板里逐个对比渲染后的数字和源码文本,发现页面上通过CSS样式动态映射了字体文件:真实数字被替换成了自定义字体里的字符,直接抓HTML文本自然拿到的是乱码。这就是典型的字体反爬。
完整的排查链路是这样的:先确认不是编码问题(对比多个页面和接口返回),再检查字体文件(Network面板过滤字体资源,看到woff/ttf文件),最后下载字体文件解析字符映射关系。不过如果你用JSON接口而不是HTML页面,绕开这个过程就简单得多——这也是我建议优先找接口的原因:反爬手段一般集中在HTML页面层,后端数据接口反爬成本高,相对干净。
对于必须在网页层解析的场景,可以用fontTools库解析字体文件,建立字符到真实数字的映射表,替换后再取值。这一步如果你的项目没遇到,可以在论文里作为"难点与解决方案"讨论,属于加分内容。
5.2 请求被拒:频率限制与断点续爬
跑全量历史数据时,我遇到过跑二十多条请求后突然连续返回403的情况。刚开始以为是IP被封了,心里一凉,后来冷静下来查日志,发现状态码是403且响应体内容很短,带有明显的限流特征。
排查过程分三步走:先把请求头补全(对比浏览器真实请求的Header,缺什么补什么),再把请求间隔从固定0.5秒改成1到3秒随机值,最后加了失败自动重试和断点续爬机制。断点续爬的意思是:每次抓取前先查数据库里已有的stat_date,跳过已抓取过的日期,只补缺的日期。
def get_missing_dates(start_date, end_date): import sqlite3 conn = sqlite3.connect(DB_PATH) cursor = conn.execute( "SELECT DISTINCT stat_date FROM movie_daily_box " "WHERE stat_date BETWEEN ? AND ?", (start_date, end_date), ) existed = {row[0] for row in cursor.fetchall()} conn.close() all_dates = pd.date_range(start_date, end_date).strftime("%Y-%m-%d").tolist() return [d for d in all_dates if d not in existed]这个函数的价值在于,断点续爬让整个采集任务变成"可恢复"的,而不是要么成功要么重来。这套"查缺补漏"的思路在任何数据同步场景里都是通用技能,写进简历不丢人。
5.3 图表中文显示方块的踩坑经历
项目在本地Windows开发时一切正常,部署到实验室的Ubuntu服务器后,ECharts图表里的中文全变成了方框。我当时第一反应是前端编码问题,排查半天无解,最后才发现是服务器系统缺少中文字体。
ECharts图表内的文字渲染依赖浏览器所在的系统字体库,服务器没有中文字体时,图形上的文字就会显示为豆腐块。解决办法也很朴素:给服务器安装中文字体,或者在前端配置里显式指定fontFamily: "Microsoft YaHei, PingFang SC, sans-serif",让浏览器优先使用常见中文字体,缺失时退回系统默认。
这个坑提醒我一个经验:系统开发不能只在"我的电脑上能跑"就完事,环境差异是最容易忽视的问题。答辩演示如果临时换设备,图表中文乱码会让你当场陷入被动,提前在代码层面把字体配置写清楚,能省掉很多临场麻烦。
6. 论文撰写与答辩准备的实战建议
6.1 论文结构怎么搭最稳妥
毕业设计论文的评审逻辑是"流程清晰、工作量饱满、有结论有验证"。我建议把论文结构按下面的顺序安排,每一章对应你系统的一个可验证模块:
- 摘要:一句话说清系统目标,一句话说清技术方案,几句话说明实现效果和结果;
- 绪论:背景与意义(票房数据对电影行业决策的价值)、国内外研究现状、本文主要工作;
- 相关技术介绍:Python、Requests、Pandas、Flask、ECharts、MySQL,每个技术写作用和在系统里承担的角色;
- 需求分析:从用户角色出发,列功能需求和非功能需求(数据准确性、采集稳定性、系统响应时间);
- 系统设计:总体架构图、功能模块划分、数据库ER图与表结构说明;
- 系统实现:每个模块的核心代码、实现思路、关键页面截图;
- 系统测试:功能测试用例表格、爬虫稳定性测试、数据准确性抽样对比;
- 总结与展望:项目完成情况、不足、后续优化方向。
"系统测试"这一章经常被学生忽略,但它恰恰是最好凑工作量而且能体现工程素养的部分。你可以做一张测试用例表,列出"输入日期、预期返回数据条数、实际返回条数、断言结果",再把爬虫连续运行7天的稳定性数据放进去,这就是很有说服力的成果展示。
6.2 高频答辩问题与应答思路
答辩时老师的问题通常围绕"为什么这么选""数据怎么保证对""系统有什么难点"三个方向展开。我梳理了几个高频问题及应答思路,给你做参考:
问题一:为什么选这个数据源,数据准确吗?
应答要点:说明数据源是行业内公认的票房统计平台,数据来自公开榜单;同时强调在系统测试阶段做了抽样核对——随机选5部电影,把爬取结果和网站页面人工核对,准确率100%。这个回答把技术方案和数据验证都覆盖了。
问题二:你的系统大数据体现在哪里?
这是最容易翻车的问题。诚实的回答是:目前数据量不大,但系统架构上已经为数据增长做了准备——每天定时采集、增量入库、按日期聚合查询,数据持续积累三个月后,表里会有几千条核心票房记录和数万条明细,可以支撑趋势分析和档期对比。再把"数据管道"的可持续运行能力作为重点讲,比硬吹大而全更有说服力。
问题三:系统的创新点是什么?
诚实优先,不要夸大。可以说:一是实现了从数据采集、清洗、存储到可视化的全链路自动化,定时调度让系统可持续运行;二是在数据清洗环节处理了"亿/万"单位统一和字体反爬等实际问题;三是前端图表与后端查询联动,支持按日期、类型多维度筛选。把每个模块里你真正解决过的实际问题挖出来讲,就是最有说服力的"创新点"。
写到这里,这套系统的关键路径已经完整走了一遍。如果你按这个路线图推进,每天投入三到四个小时,两周左右能把核心链路全部跑通。最后再分享一个我个人的小习惯:每完成一个模块,立刻截图、记录踩坑过程,这些素材到最后写论文、做答辩PPT时全是现成的弹药库,不用临时回忆赶工。祝你的毕设顺利收场。