Files
blog_cn/_posts/2026-06-01-dedupe.md
mayx 71d493c2a8 Update 4 files
- /_data/other_repo_list.csv
- /_posts/2026-06-01-dedupe.md
- /assets/js/pjax.js
- /index.html
2026-05-31 16:00:35 +00:00

210 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
layout: post
title: 如何节约游戏占用的硬盘空间?
tags: [dedupe, RPG制作大师, 游戏]
---
浪费硬盘空间是可耻的!<!--more-->
# 起因
在几年前,我写过一篇在[MacBook上玩游戏](/2023/10/21/game.html)的文章在那之后我已经在我的Mac上下载了几十部游戏。只不过有个问题……我的Mac只有256GiB的硬盘存储空间下载一堆游戏会让我的硬盘空间不够用但是又不太想删所以我该怎么尽可能让游戏占用更少的空间呢
首先为了能在Mac上尽可能流畅地玩我玩的游戏大多都是用跨平台能力很强的引擎编写的游戏比如[Ren'Py](https://github.com/renpy/renpy)、RPG制作大师、Godot之类的而像RPG制作大师这种引擎制作的游戏还有一个特点开发者一般都会使用引擎自带的素材进行开发有时候还会用不少第三方的罐头素材之类的实际上甚至还有好多AVG为了蹭这些引擎的公用素材刻意用它们所以这几十个游戏里应该有非常多的重复素材如果能想办法把它们去个重应该能节省相当多的空间吧……
# 去重的方法
如果想要对文件进行去重,我搜了一下,有个叫做[jdupes](https://codeberg.org/jbruchon/jdupes)的工具就很不错它支持多种去重方式比如使用硬链接或者用一些文件系统的写时复制特性。不过如果用写时复制特性jdupes在第二次执行的时候会认为去重后的文件还是单独的文件就会重复去重了而且最终也不好统计反正对我玩的游戏来说要去重的都是游戏素材不存在后续修改的可能性所以我打算全部用硬链接。
所以最终要执行的命令也非常简单,直接一句`jdupes -r -L Game`就可以了,这样以后每次下载了新的游戏之后重复执行这个操作,就可以将游戏中和其他游戏里有的素材去重了。
不过实际上很多游戏并不能直接用这种方式去重,因为它们的资源文件有些是打包成单个文件,有些进行了简单的加密,导致即使是相同的素材,文件也并不相同,所以我必须让所有的资源以单独原始的形态出现。对于不同的引擎也有不同的处理方式,所以接下来我需要对它们进行一些研究。
# 不同引擎的处理方式
## RPG制作大师MV/MZ
对于RPG制作大师MV/MZ开发的游戏来说解密很简单比较知名的是一个叫做[RPG-Maker-MV-Decrypter](https://gitlab.com/Petschko/RPG-Maker-MV-Decrypter)的工具它可以在浏览器中进行解密但一个游戏的资源文件非常多……要是全上传给浏览器实在是太麻烦了……后来我又搜了一下有一个用C#写的叫[RPG Maker Decrypter](https://github.com/uuksu/RPGMakerDecrypter)工具也很不错它作为命令行工具比在浏览器中执行简单多了而且还能只把资源文件单独提出来这样就可以剔除掉游戏自带的浏览器文件。不过他这个仓库的代码有个问题它在选择文件的时候似乎会区分大小写文件夹名中含有大写字母的似乎会被剔除……这样不太符合我的要求啊当然我不会C#于是我用AI改了一下还给他提了个[PR](https://github.com/uuksu/RPGMakerDecrypter/pull/28)不过这家伙看起来似乎不太喜欢AI写的代码看起来不打算合我的PR😅。不过无所谓了反正我也是自用他爱合不合吧。
这个工具的用法也非常简单,一句`RPGMakerDecrypter-cli [input] -p -o [output]`就处理好了,处理完之后只需要把`data/System.json`中的`hasEncryptedImages``hasEncryptedAudio`设置为false就可以正常识别以后在Mac中只要在游戏路径下执行`python3 -m http.server`就可以在浏览器中游玩了。
在这个过程中我还发现有一些游戏喜欢把原画文件直接放到游戏里面一张图片好几M但RPG制作大师的引擎在渲染的时候根本不会渲染出那么高的分辨率结果毫无意义地浪费一大堆存储空间而且因为图片是加密的对大多数人来说也没有收藏价值。所以在解密完之后我就想干脆把这些图片全部有损压缩一遍估计能节省不少存储空间于是让AI写了个简单的压缩脚本处理了一下
```python
#!/usr/bin/env python3
"""
图片压缩脚本(多进程版本)
将 pictures.orig 文件夹中的图片使用 WebP 格式进行高效压缩,
保持分辨率不变,肉眼看不出差异,压缩后的图片保存到 pictures 文件夹。
使用方法:
python3 compress_images.py
压缩策略:
- 保持原始分辨率不变
- 使用 WebP 格式(有损压缩,高质量)
- 质量设置为 85在保持视觉质量的同时显著减小文件大小
- 文件名和后缀保持不变
- 多进程并行处理
- 处理失败时自动复制原文件
"""
import os
import shutil
from PIL import Image
from pathlib import Path
from multiprocessing import Pool, cpu_count
from functools import partial
# 配置路径
SOURCE_DIR = "pictures.orig"
OUTPUT_DIR = "pictures"
# WebP 质量设置 (0-100数值越高质量越好文件也越大)
# 85 是一个很好的平衡点,肉眼几乎看不出差异
WEBP_QUALITY = 85
# 对于带有透明通道的图片,可以设置不同的质量
WEBP_QUALITY_WITH_ALPHA = 80
# 并行进程数,默认为 CPU 核心数
NUM_WORKERS = cpu_count()
def compress_single_image(img_file: tuple[str, str, str]) -> tuple[str, bool, int, int]:
"""
压缩单个图片文件(用于多进程)
Args:
img_file: (源文件路径, 输出文件路径, 输出目录) 元组
Returns:
(文件名, 是否成功, 原始大小, 压缩后大小) 元组
"""
source_path, output_path_str, output_dir = img_file
source_path = Path(source_path)
output_path = Path(output_path_str)
original_size = source_path.stat().st_size
try:
img = Image.open(source_path)
# 检查是否有透明通道
has_alpha = img.mode in ('RGBA', 'LA', 'PA') or (img.mode == 'P' and 'transparency' in img.info)
# 确定使用的质量
quality = WEBP_QUALITY_WITH_ALPHA if has_alpha else WEBP_QUALITY
# 保存为 WebP 格式,但使用原始的文件扩展名
img.save(
str(output_path),
format='WEBP',
quality=quality,
method=6 # 压缩方法 0-66 是最慢但压缩率最高的
)
compressed_size = output_path.stat().st_size
return (source_path.name, True, original_size, compressed_size)
except Exception as e:
# 处理失败时,复制原文件到输出目录
try:
shutil.copy2(source_path, output_path)
compressed_size = output_path.stat().st_size
return (source_path.name, False, original_size, compressed_size)
except Exception as copy_error:
return (source_path.name, False, original_size, 0)
def main():
source_dir = Path(SOURCE_DIR)
output_dir = Path(OUTPUT_DIR)
# 检查源目录是否存在
if not source_dir.exists():
print(f"错误: 源目录 '{SOURCE_DIR}' 不存在")
return
# 创建输出目录
output_dir.mkdir(exist_ok=True)
# 获取所有图片文件(支持多种格式)
image_extensions = ('*.png', '*.jpg', '*.jpeg', '*.bmp', '*.gif', '*.tiff', '*.webp')
image_files = []
for ext in image_extensions:
image_files.extend(source_dir.glob(ext))
image_files = sorted(set(image_files)) # 去重并排序
if not image_files:
print(f"在 '{SOURCE_DIR}' 中没有找到图片文件")
return
# 构建任务列表
tasks = []
for img_file in image_files:
output_path = output_dir / img_file.name # 保持原文件名和后缀
tasks.append((str(img_file), str(output_path), str(output_dir)))
print(f"找到 {len(tasks)} 个图片文件")
print(f"源目录: {SOURCE_DIR}")
print(f"输出目录: {OUTPUT_DIR}")
print(f"WebP 质量设置: {WEBP_QUALITY}")
print(f"并行进程数: {NUM_WORKERS}")
print("-" * 70)
# 使用多进程池处理图片
success_count = 0
fail_count = 0
total_original = 0
total_compressed = 0
with Pool(processes=NUM_WORKERS) as pool:
for i, (filename, success, original_size, compressed_size) in enumerate(pool.imap(compress_single_image, tasks), 1):
total_original += original_size
total_compressed += compressed_size
if success:
success_count += 1
marker = "✓"
reduction = (1 - compressed_size / original_size) * 100 if original_size > 0 else 0
status_msg = f"{reduction:+.1f}%"
else:
fail_count += 1
marker = "✗"
status_msg = "复制原文件"
status = f"[{i}/{len(tasks)}] {filename}"
print(f"{marker} {status:50} {original_size/1024:>8.1f}KB -> {compressed_size/1024:>8.1f}KB ({status_msg})")
# 输出总结
print("-" * 70)
total_reduction = (1 - total_compressed / total_original) * 100 if total_original > 0 else 0
print(f"压缩完成!")
print(f" 成功处理: {success_count}/{len(tasks)} 个文件")
if fail_count > 0:
print(f" 失败(已复制原文件): {fail_count}/{len(tasks)} 个文件")
print(f" 原始总大小: {total_original / 1024 / 1024:.2f} MB ({total_original / 1024:.1f} KB)")
print(f" 压缩后大小: {total_compressed / 1024 / 1024:.2f} MB ({total_compressed / 1024:.1f} KB)")
print(f" 总压缩率: {total_reduction:.1f}%")
print(f" 节省空间: {(total_original - total_compressed) / 1024 / 1024:.2f} MB")
if __name__ == "__main__":
main()
```
最终压缩完之后我把原图上传到了[EH画廊](https://e-hentai.org/g/3901673/426a7a17ba/)中本地只留压缩后的图片大小从原来的2GiB多下降到了300多MiB可以说效果相当显著了。
除此之外还有一些游戏使用了Ogg FLAC背景音乐这种音乐不仅占用磁盘空间很大而且我在Safari上玩的时候浏览器根本没法解析Chrome应该可以。虽然我听音乐是会考虑[HiFi](/2025/03/22/hifi.html),但玩游戏就没必要了吧……所以像这种音乐,就得用一句:
```bash
ffmpeg -i input.flac.ogg -c:a vorbis -strict -2 -q:a 10 output.ogg
```
转换为正常有损的Ogg音乐了。
## RPG制作大师XP/VX/VA
对于RPG制作大师XP/VX/VA引擎开发的游戏来说它们都是基于用Ruby语言开发的RGSS编写的作为脚本来说倒是有跨平台的条件但因为官方并没有做跨平台所以不能直接在Mac上运行。不过有一款叫做[mkxp-z](https://github.com/mkxp-z/mkxp-z)的工具允许跨平台运行使用RPG制作大师XP/VX/VA制作的游戏因此这类游戏我也收集了一些。
这些游戏的资源通常会进行简单的混淆加密一般会打包成单个RGSSAD文件这个解包也很简单用刚刚的RPG Maker Decrypter就可以。不过这种游戏还有个特点有些游戏需要使用[RTP](https://www.rpgmakerweb.com/run-time-package)才能运行它这个RTP其实就是RPG制作大师自带的素材包当时设计出来估计也是想着用来节约硬盘空间吧就是不知道为什么到后来的MV/MZ却取消了这种方式……虽然mkxp-z是支持通过配置文件引入RTP的但既然我已经选择了硬链接的方式就没必要单独搞RTP了我选择把RTP直接和游戏合并然后让jdupes直接去重就好了这样相比于RTP的方式还有一些好处就是XP/VX/VA可能有一些和MV/MZ使用相同的素材这部分也可以不用占用重复的空间了。
## Ren'Py
对于Ren'Py来说因为这个引擎并没有自带的公共资源所以重复素材的问题并不是很大。不过在我之前对[Ren'Py的探索](/2024/01/20/renpy.html)中提到过我玩的一些游戏是系列游戏这种系列游戏有非常多的素材复用但显然开发者并不会为了节约玩家硬盘空间而共享这部分资源而且Ren'Py游戏也都是打包成单个文件的所以接下来我们依然得要解包才能进行去重处理。
Ren'Py使用的rpa文件解包起来依然很简单有一款现成的工具[unrpa](https://github.com/Lattyware/unrpa)可以直接解包用pip就能安装。不知道为什么这些引擎总是喜欢把资源文件都打成一个包明明很容易就能解包……难道是为了性能吗
不过也正是因为Ren'Py的公共资源不多如果玩的不是系列游戏就没有解包的必要了解包之后一堆小文件有可能会比整个rpa文件更大毕竟文件系统存在“簇”有可能会消耗没对齐的空间。
# 验证结果
最终进行完上述操作,可以通过执行`du -sh``du -shl`进行对比来验证节约的硬盘空间,我在这次游戏的瘦身中节约了:
```
~ % du -sh Game
33G Game
~ % du -shl Game
47G Game
```
看起来还是相当可观啊……尤其是在当下硬盘价格大涨的情况下,如果很多人能通过这些方式来节约硬盘空间,就能减少对硬盘容量的需求吧……不过说到底其实也都是网上能下到的资源,也许玩完之后就删掉才是最好的节约硬盘的方式吧😂。
<input name="live2dBGM" value="https://music.163.com/song/media/outer/url?id=1968116350.mp3" type="hidden" />