最近游戏库里的3A大作一个比一个能吃硬盘,前阵子看着《最终幻想》重制版、《GT赛车》新作和几个常驻的联机游戏,主机自带的667GB实际可用空间直接见了底。那几天我每天开机第一件事就是纠结:到底删哪个?删了以后想玩怎么办?下载又要等多久?这种"存储焦虑"想必不少PS5玩家都有过。后来我想明白了,与其每次靠感觉做决定,不如做一个把玩家高频需求统一收拢起来的小工具,于是就有了AnyPS5——一个本地的、数据自持的PS5辅助管理工具,主要解决存储空间分析、游戏库整理、奖杯进度管理、主机健康检查这四件事。如果你也是PS5玩家,或者正打算用Python给自己写点效率工具,这篇项目总结应该对你有用。
我不打算把它写成一份自吹自擂的项目演示,而是把从需求分析、功能拆解到踩坑修复的完整过程都摊开来讲,包括每个功能为什么这么设计、哪些数据源是现实可用的、哪些地方最容易翻车。整个项目没有碰任何系统权限,不需要改装主机,也不需要额外付费服务,所有代码跑在电脑上,数据都由玩家自己提供或授权导入。
1. 项目的起点:当2TB硬盘也装不下我的游戏库
最早这个项目只有一个功能原型,就是"存储空间管理",但越做越发现,玩家的痛点从来不是单一维度的。存储焦虑只是表面,背后其实是"游戏库信息不透明":你到底有多少游戏?哪些装了没玩?哪些玩了一半?哪些已经白金了?这些信息分散在主机界面、手机App、记忆和Excel表格里,没有任何一个工具把它们串起来。
1.1 三个日常痛点逼我动手
先说存储。PS5的系统存储页面能看到每个游戏占多少空间,但只能一个一个看,没法排序,也没有历史记录。我想知道"过去半年我到底玩过哪些游戏",系统不告诉我;我想知道"如果必须删三个游戏,删哪三个损失最小",系统也不告诉我。这一切只能靠玩家自己记。
再说游戏库。数字版、光盘版、会免游戏、二档会员游戏混在一起,主机界面的筛选功能够用,但跨时间、跨类型地做统计就很弱。比如"我买过多少款单人剧情游戏""哪一年买得最多""哪个类型囤得最多从来不玩",这些问题的答案明明都在数据里,却没有一个入口能看到。
最后是奖杯进度。补完一个老游戏的奖杯需要反复对照当前进度、剩余目标、稀有度,官方App的奖杯页只能说"能用",但离"好用"有距离。我想要的是一份可以按稀有度排序、按缺口筛选、能告诉我"再拿三个铜杯就能升到下一等级"的清单。这三个痛点单独看都不算大问题,但凑在一起就很烦人,我决定用代码一次性解决。
1.2 为什么叫"AnyPS5"以及项目的形态
名字里的"Any"有两层意思。第一层是"任何玩家",工具的使用门槛要低,不能要求人人会写代码;第二层是"任何场景",希望能把玩家日常高频操作一网打尽,而不是做一个只有极客才用的命令行工具。
当然,理想和现实有差距。主机平台没有开放通用编程接口,我不能直接读取系统数据,所以AnyPS5最终做成了一个跑在电脑上的本地工具,用Python编写,通过"玩家本人提供的截图、导出文件、公开页面快照、手动维护的数据表"这四个来源获取信息。所有数据都保存在本地,不需要注册账号,不依赖服务器。这个形态可能不够"酷",但它足够干净、安全、可控。
我给自己定了一条线:AnyPS5只做"管理玩家自己的数据",不碰主机系统文件,不涉及任何验证规避,不做任何灰色操作。这条线从项目第一天就划死了,后续所有功能决策都和它有关。
2. 先定边界:什么能做,什么坚决不做
开发工具类项目最怕的是没有边界,什么都想做,最后什么都做不成。AnyPS5立项的时候列了一个"不做清单",后来证明这份清单帮我省掉了至少90%的麻烦。
2.1 三个"不做"帮我少了90%的麻烦
第一,不做任何系统层面的读取和修改。不碰主机系统分区,不碰存档文件,不碰系统固件。这意味着AnyPS5永远不会导致主机变砖,也不会因为系统更新而失效。代价是我只能拿到玩家"看得见"的数据,比如截图、页面文本。
第二,不做任何账号敏感信息的采集。不要求玩家输入主机账号密码,不抓取未授权的个人数据,不保存任何凭据。所有涉及在线信息的操作,只针对玩家自己公开的内容,或者玩家手动导出的本地文件。
第三,不做任何"加速""绕过""解锁"类功能。网络上这类需求很旺盛,但往往是灰色地带,而且和"辅助管理"的定位格格不入。AnyPS5的价值是帮玩家把已有的东西管好,而不是去突破什么限制。
这三条边界确立之后,项目的技术选型也跟着清晰了:不需要破解任何协议,不需要逆向主机的通信流程,只需要处理好本地文件解析、OCR识别、CSV数据管理和简单的数据可视化。
2.2 数据从哪里来:四条公开可行的路径
既然不碰系统内部,就得把"公开数据源"研究透。我实际验证过,下面四条路径在现实场景里都是走通的:
| 数据用途 | 来源路径 | 获取方式 | 更新频率 |
|---|---|---|---|
| 存储空间分析 | 主机系统设置中的存储页面截图 | 玩家手动截图,通过工具扫描识别 | 每季度一次或空间告急时 |
| 游戏库清单 | 主机商店的已购列表 / 手机App的游戏库页面 | 玩家手动导出页面文本或维护CSV | 每季度一次 |
| 奖杯进度 | 公开的奖杯展示页面 | 玩家提供自己公开页面的页面快照 | 每月一次 |
| 游玩记录 | 主机使用情况概览,部分可从通知记录获取 | 玩家手动补充或由CSV维护 | 每周一次 |
有人可能会说,这四种方式都离不开"手动",太原始了。但事实上,对个人工具而言,"半自动"往往是稳定性和隐私性的最佳平衡点。我宁可每季度花十分钟截图导入,也不想为了全自动去碰不该碰的接口。
主流程做完之后,我把整个项目拆成四个功能模块:存储管家、游戏库整理、奖杯进度助手、主机体检。每个模块都可以独立使用,也可以共享同一套CSV数据。接下来挑几个核心模块讲讲实现思路和踩坑过程。
3. 存储管家模块:从"删哪个"到"有数据支撑地删"
这个模块是AnyPS5最早成型、也是使用频率最高的部分。它的核心任务只有一个:把"主机存储空间的截图"变成"带排序、带评分、带建议的删除清单"。
3.1 OCR识别截图:识别游戏名与体积
实现逻辑非常简单:玩家在主机系统设置里打开存储页面,对游戏列表逐屏截图,然后把图片丢给AnyPS5,工具调用本地OCR引擎识别出每一行的游戏名称和占用体积,整理成结构化数据。
实际操作中,为了让识别率足够高,我做了两个约束。第一是截图规范:建议在主机系统界面把列表滚动到最顶部,保持每行文字在同一水平位置,避免游戏名称被图标遮挡;分辨率尽量保持系统默认,不需要特意调高。第二是识别策略:游戏名和体积要分开识别,体积单位统一转换成GB,方便后续排序和计算。
import json import re def parse_storage_line(raw_line: str): # raw_line 示例: "最终幻想 VII 重生 84.32 GB" # 先提取体积数字和单位 pattern = r'([\d.]+)\s*(GB|MB)$' match = re.search(pattern, raw_line.strip()) if not match: return None size_value = float(match.group(1)) unit = match.group(2) if unit == "MB": size_value = size_value / 1024 # 剩余部分是游戏名 game_name = raw_line.strip()[:match.start()].strip() return {"name": game_name, "size_gb": round(size_value, 2)}识别结果会统一写进一个名为storage_scan.json的文件里,里面包含游戏名、体积、扫描时间和当时的剩余空间。多扫几次之后,就自然有了历史数据,可以画出一条"存储水位曲线"。
3.2 体积排行榜与"删除建议分"
有了结构化数据,接下来的问题就是:怎么帮玩家做删除决策?我设计了一个"删除优先级评分",分数越低越优先删。评分公式如下:
- 基础分(50分):已通关且不追求全收集的游戏直接给到50分,未通关的给30分,仍在新游戏阶段(一个月内购买)的给10分。
- 游玩活跃度(0到30分):以"距离上次游玩的天数"为指标,越久没玩分数越高。超过90天给满分30分,30天内给5分以下。
- 体积修正(0到20分):体积越大的游戏,删除后释放空间越明显,给予额外加分。100GB以上给20分,50到100GB给15分,20到50GB给10分,20GB以下给5分。
- 联机依赖(扣分项,最高-20分):如果游戏是常驻联机游戏,比如每天都会上线做任务的类型,扣20分,表示系统不建议删除。
这套评分规则本质上是把"删哪个损失最小"这个模糊问题量化。我见过不少玩家做删除决定时只看体积,结果删了一个正在玩的游戏,过两天又连夜下载回来,白白浪费时间。有了评分之后,决策就变成了一句话:优先删"通关后再没碰过、体积又大、和联机朋友无关"的游戏。
3.3 实测效果:一次扫描省下200GB
模块上线后的第一次全量扫描是在某个周末,当时主机剩余空间只剩41GB。我按连续三屏截图导入,OCR识别出35款游戏,总占用约526GB,其中体积最大的一款赛车游戏占了96GB,上次游玩时间是132天前,评分高达92分。系统给出的删除建议是按评分从高到低删掉六款游戏,合计释放约219GB。
实际操作中我没有一次全删,而是先删了评分最高、体积最大的三款,释放了约140GB,剩下的等需要时再说。这个流程如果用人工来做,光翻列表就要十几分钟,而且很容易纠结。AnyPS5把整个过程压缩到了五分钟以内:截图、导入、看推荐、动手删。
这里有个比较重要的心得:OCR不是100%准确的,所以工具在更新删除建议前会显示"识别置信度",低置信度的行会被标出来让玩家人工确认。千万别图省事跳过这步,我遇到过把游戏名"GT赛车7"识别成"GT豪华7"的情况,要不是复查了一轮,删游戏的时候就找不到对应条目了。
4. 游戏库与奖杯模块:让"烂尾"和"补完"都变得可视化
存储模块解决的是"空间不够怎么办",游戏库和奖杯模块解决的则是"我到底有哪些游戏、它们玩到什么程度"。这两个模块共享同一份游戏主数据表,是整个AnyPS5的数据中枢。
4.1 游戏库CSV的维护技巧
游戏库数据落在一个名为games.csv的文件里,每行一款游戏,字段包括:
| 字段名 | 示例 | 说明 |
|---|---|---|
| game_id | FF7REBIRTH | 唯一标识,建议用英文缩写 |
| title | 最终幻想VII 重生 | 游戏中文名 |
| genre | 剧情RPG | 一级类型标签 |
| release_year | 2024 | 发售年份 |
| source | 数字版/光盘版/会免 | 获取渠道 |
| status | 通关/烂尾/进行中/未开始/白金 | 当前进度状态 |
| last_played | 2025-01-12 | 最近游玩日期 |
| size_gb | 84.32 | 最近一次扫描到的体积 |
| notes | 二周目Hard难度 | 备注 |
这个表是整个项目里唯一需要玩家"动手维护"的地方,也是大多数玩家觉得麻烦的环节。为了降低维护成本,我做了两件事:一是提供import_from_ocr()函数,可以直接把存储扫描结果中的游戏名和体积导入,自动填充title和size_gb两个字段;二是把status字段做成了下拉式约束,只允许填几个固定值,避免"打了""玩了"这类模糊词。
我自己的维护频率是每季度一次,加上每次通关后顺手改一下status。不需要做到"实时",因为游戏库的信息不像存储空间那样容易告急,几周的延迟完全可以接受。
4.2 奖杯进度抓取与稀缺度排序
奖杯数据来自玩家公开的奖杯展示页面。AnyPS5不做账号登录,只要求玩家把自己公开页面的内容保存为本地快照,然后解析其中的奖杯列表和完成百分比。解析结果写入trophies.csv,字段包括游戏名称、稀有度等级(铜/银/金/白金)、是否已获得、全球获得率、最后更新时间。
拿到这张表之后,最有用的功能是"缺口清单":比如你某个游戏还差3个铜杯、1个银杯就能白金,清单会直接告诉你"差什么、在哪个章节/模式拿、全球获得率是多少"。按稀有度排序之后,低获得率的奖杯会排在最前面,因为它往往是全游戏最费时的一个点,也是补完计划里最应该提前做功课的部分。
我还做了一个"白金距离指数",其实就是缺失奖杯数量和估算耗时的加权,用来解决"要不要开这个坑"的选择困难。比如一款通关了但对白金没有执念的游戏,如果它的白金距离指数超过80(意味着还要投入十几个小时,且包含多个高难度杯),系统会建议"先别急着刷,留着,以后想挑战再说"。
4.3 两个数据源如何串起来
游戏库CSV和奖杯CSV通过游戏名称关联。听起来简单,实际上有坑,因为这两个来源的名称写法经常不一样:游戏库用的是中文名,奖杯页面用的是英文名或缩写。解决方案是在game_id上做主键,两个表都维护这个字段。名称匹配只用于"导入时自动提示可能属于同一款游戏",确认后由玩家指定对应的game_id。
def guess_game_id(title: str, games_table) -> str | None: # 优先精确匹配中文名,其次去掉特殊字符后匹配,最后按缩写匹配 candidates = [] for row in games_table: if row["title"] == title: return row["game_id"] normalized_title = re.sub(r"[\s::\-]", "", row["title"]) if re.sub(r"[\s::\-]", "", title) == normalized_title: candidates.append(row["game_id"]) return candidates[0] if len(candidates) == 1 else None这个"半自动关联"的设计让AnyPS5在数据录入时要多花一点人工确认的时间,但换来了极好的稳定性,不会出现两套数据互相打架的情况。
5. 开发踩坑记录:小工具也有大教训
AnyPS5本身不算复杂项目,但麻雀虽小五脏俱全,从编码、OCR、时区处理到文件编码全都踩过一遍。这里挑三个影响最大的坑分享。
5.1 坑一:OCR对简体中文游戏名识别率惨不忍睹
第一版测试的时候,OCR引擎对英文游戏名的识别率大概在95%以上,但切到简体中文游戏名之后直线下降,尤其是带"VII""重生""Remake"这种中英混排的名字,经常把中文识别成错别字,把罗马数字识别成乱码。
排查之后发现,问题不在OCR引擎本身,而在预处理。主机系统界面使用的字体偏细,而且列表行有半透明底层,截图放大后文字边缘有大量锯齿。解决办法是先用图像处理库把截图转成灰度图,做二值化处理,再放大两倍后再交给OCR引擎。这个简单的预处理把中文游戏名的识别准确率从不到70%提升到了90%以上。
另外我还加了一个"游戏名词典"作为兜底:从主机商店页面整理了一份热门游戏名称列表,OCR结果先和词典做模糊匹配,匹配不上才靠纯文本识别。这样就算识别出错,也能通过词典纠正回来。
5.2 坑二:时区让"最近游玩天数"算错一天
删除建议评分里用到"距离上次游玩日期的天数",这个数据来自游戏库CSV的last_played字段。第一次写完,我拿自己的数据测,发现有些游戏明明昨天还在玩,计算出来的天数却是2天。
根因是时区错乱:CSV里存的是本地日期(CST),程序里转成了UTC时间,再和当天UTC时间相减,导致"昨天下午"直接变成了"前天",多算了一天。这种问题在存储管理里不致命,但会导致评分偏差,如果体感不细心很容易被忽略。
修复方案是:CSV里的日期字段一律只存"日期"不存"时间",所有计算都基于日期字符串,不做时区转换。具体来说就是date.fromisoformat()之后只比较date对象,不交叉datetime。
5.3 坑三:Excel打开CSV时中文乱码但程序内正常
这个问题特别经典。CSV文件用Python写出来的时候默认是UTF-8编码,程序自己读没问题,但玩家收到数据表之后往往用Excel打开,一打开满屏乱码。Excel对UTF-8无BOM的CSV默认按本地编码解析,中文自然就废了。
解决办法是在写CSV时加上UTF-8 BOM头:
with open("games.csv", "w", encoding="utf-8-sig", newline="") as f: writer = csv.writer(f) writer.writerow(["game_id", "title", "genre"])utf-8-sig写出来的文件带BOM,Excel能正确识别,程序员用Python读的时候又不会受到BOM影响。这个修复只有一行,但属于"不做就迟早会被用户问询"的典型问题。
5.4 结构设计与扩展取舍
AnyPS5的代码结构被刻意设计成"功能模块 + 数据层"两大块。功能模块负责界面交互和业务逻辑,数据层负责所有CSV和JSON的读写。这样做的好处是:以后不管是想加Web界面,还是把数据迁移到SQLite,都不用改动核心逻辑。
扩展性方面,我做了两个取舍。第一,暂时不做多用户支持,因为个人工具的数据模型都是单机单用户的,提前设计多用户会让复杂度暴增;第二,不做云同步,数据留在本地反而省心,真有同步需求可以自己放进网盘目录。这两个决定让整个项目始终保持"周末能维护"的体量。
6. 后续可以怎么玩(给想复刻的人留的作业)
AnyPS5做到当前版本,对我来说已经解决了"三分钟判断删什么游戏""一眼看清游戏库结构""快速定位白金缺口"这三个核心问题。如果你也想复刻一个类似的个人工具,有几个方向非常值得继续做。
6.1 给存储模块加历史趋势
每次扫描结果都会被记录,那么画出一张"主机剩余空间 vs 游戏库总体积"的时间趋势图就是顺手的事。你甚至可以做简单的线性预测:按最近三个月的涨势,估算多久之后空间会再次告急,提前决定是把旧的会免游戏清掉还是直接加一块外置硬盘。
6.2 奖杯目标的"缺口清单"再进一步
现在的奖杯缺口清单只做到了"列出缺失奖杯和稀有度",进一步可以做成"补完路线图":按奖杯之间的前置关系排序,先给出一周目标,再给出一个月目标。这个方向有点类似任务管理工具,核心是维护一张奖杯依赖关系表,工程量不小,但做出来会很有成就感,也会让"白金列表"不再只是挂在天花板上的装饰。
6.3 新闻聚合与发售日历提醒
游戏库CSV里有类型和关注度字段,可以拿来做一个简单的发售日历提醒:订阅公开的官方新闻RSS或页面改动,一旦关注的系列有新作发售日,就推送到本地通知。这个功能和主机数据无关,完全独立,作为个人学习试水的第二个项目也不错。
如果你打算照着这个思路写,我最想提醒你的是:先花一个晚上把数据模型定清楚,把"游戏名""游戏ID""状态字段"这些基础表设计好,再动手写功能。我在0.1版本的时候就是建模没想清楚,游戏名一会儿用中文一会儿用英文,后面被迫重写了一遍导入逻辑。数据模型是这种个人工具的地基,地基歪了,上层功能再漂亮也会天天返工。