exbsa提取工具:.bsa二进制归档解析与批量资源提取
简介:ExBSA 提取工具面向 BISHOP 社 GALGAME(视觉小说)资源分析与备份场景,聚焦 BSA 打包档案的解析与解包,适合想查看原始图像、音频与剧本,或研究游戏资源组织方式的爱好者与开发者。压缩包共 6 个文件,以 3 个 exe 可执行程序、2 个 cpp 源码和 1 个 txt 说明文档为主,整体约 36KB,轻量易取用。已有 2483 人学习下载,具备一定关注度。工具提供不同版本的可执行文件,可指定 BSA 文件并输出到目标目录;源码便于理解 BSA 头部记录的文件数量、大小与位置,以及逐个读写文件的解包流程,也为处理压缩与加密、二次开发留出空间。另有脚本辅助合并或整理剧本类内容,说明文档给出使用注意事项。读者可掌握档案解析思路、资源提取方法与定制方向,并应在版权许可范围内使用。
1. exbsa提取工具:一个把 .bsa 归档摊开给你看的命令行工具
上周有朋友扔过来一个 1.2 GB 的资源包,后缀是.bsa,说双击没反应、改成.zip解压报“不可预料的压缩文件末端”、7-Zip 能打开但里面全是乱码目录。这种场景下能救场的就是 exbsa 提取工具——它专门解析这类归档容器,按内部目录树把纹理、模型、音频、脚本一条条落成真实文件。它适合三类人:做资源替换的美术、要给旧资源做汉化或重打包的从业者、以及需要批量核对资源完整性的测试。不适合的人也很明确:只想解个普通 zip 的,别绕这一圈。
2. 归档结构拆解:exbsa 到底在文件里读什么
.bsa不是 zip 的马甲,它是带哈希索引的二进制归档。拿通用解压器硬开,运气好能看到文件名,运气差就是一堆无法识别的条目——因为文件名、目录层级、偏移量全部记在归档自己的目录表里,而不是靠中央目录区约定。理解这张表,后面所有参数和坑才有落脚点。
2.1 文件头 36 字节决定了整个解析路径
这类归档的头部是定长的,前 36 字节里塞了版本、目录记录起始偏移、标志位、目录数、文件数、目录名总长、文件名总长、文件标志位。exbsa 启动后第一件事就是读这 36 字节做合法性判断:魔数不对直接退出,版本超范围给警告但仍尝试按兼容模式解析。
| 字段 | 长度 | 作用 | 解析失败的表现 |
|---|---|---|---|
| 魔数 | 4 | 格式标识 | 直接报 not a bsa archive |
| 版本 | 4 | 决定记录结构 | 高版本归档列目录正常但提取报错 |
| 目录记录偏移 | 4 | 目录表起点 | 偏移错则列出 0 个条目 |
| 归档标志 | 4 | 是否含文件名、是否压缩 | 名字全空、或提取出压缩流 |
| 目录数 / 文件数 | 各 4 | 计数校验 | 数量对不上说明归档被截断 |
| 名称长度合计 | 各 4 | 字符串区边界 | 越界读会拿到脏名字 |
| 文件标志 | 4 | 默认压缩策略 | 个别条目解压失败 |
我一般第一步不提取,先让它把头部信息打出来,跟自己用十六进制看到的数字对一遍。数字对得上,说明这个归档没有被二次修改过;对不上,后面所有提取结果都不能信。
# 只读头部与统计信息,不落盘 exbsa info ./archive/pack_core.bsa # 输出形如:version / folders / files / flags / total size # 重点看 files 与实际条目数是否一致info子命令不写磁盘,是排查时最安全的一步。参数上没有花活,唯一需要注意的是路径里有空格要加引号,Windows 下反斜杠建议统一成正斜杠,避免它在拼接虚拟路径时把分隔符吃错。
2.2 目录树与哈希索引:文件名不是随便存的
归档里的目录表是一条条变长记录,每条记录记目录名长度、名称、文件数量,然后跟一批文件记录。文件记录里除了大小和偏移,还有两个哈希值——目录哈希和文件哈希。有些归档在打包时勾了“省略文件名”,那就只剩哈希,这时候 exbsa 无法还原真实名字,只能生成unknown_<hash>这样的占位名。
这不是工具不行,是数据本身没带信息。判断方法很简单:info输出里看归档标志位,如果“含文件名”那一位置空,就别指望提取出可读文件名。遇到这种情况我一般先跑一遍 list,把哈希清单导出来,再拿资源索引库反查名字,最后用重命名脚本补齐。
# 列出全部条目,带哈希和大小,便于后期反查 exbsa list ./archive/pack_core.bsa --hash --size --format csv > entries.csv # 只看某个子目录,减少噪音 exbsa list ./archive/pack_core.bsa --filter "textures/architecture/*" # --hash 输出 folder hash / file hash 两列 # --format csv 避免长名字在终端里被截断--filter用的是 glob 语义,*不跨目录,跨目录要写**。这一点很多人第一次用会翻车:写了textures/*以为能拿全部,结果只拿到textures下一层的文件。
2.3 压缩位与分块:哪些条目能直接吐出来
归档里的条目可能是原样存储,也可能是 zlib 压缩块。压缩与否由文件标志位和单条记录的高位共同决定。exbsa 默认对每条记录判断一次,遇到压缩的就解,遇到原样的直接拷。这里有个容易忽略的边界:部分归档会把大文件切成多个块存储,头部记录的大小是解压后大小,磁盘上实际占用小于它。如果工具只按记录大小做偏移推进,第二条开始就会错位,表现是“前几个文件正常,后面全是坏文件”。
处理方式是加一个安全参数,让它按实际读取量推进而不是按声明大小:
# 保守模式:逐条校验偏移,牺牲速度换正确性 exbsa extract ./archive/pack_core.bsa -o ./out/core --safe-offset --verify-crc # --safe-offset 按实际字节推进,不信任记录里的声明大小 # --verify-crc 每条解压后校验 CRC,失败条目单独记日志 # 大归档建议保留这个组合,多花的时间通常在可接受范围--verify-crc会在输出目录旁生成一份.failed.log,列出校验失败的条目名和偏移。这份日志比终端滚屏有用得多,后面做二次修复基本靠它定位。
3. 把 exbsa 跑起来:环境、参数与一次完整提取
工具本身是个单文件可执行程序,不依赖运行时,但依赖操作系统对长路径和大小写的处理。跨平台搬归档的时候,环境差异造成的失败比归档本身损坏还常见。
3.1 环境准备与最小可用自检
拿到二进制先做三件事:给它执行权限、确认版本能跑、在一个小归档上试一次完整提取。别上来就对着 1 GB 的主包开刀,失败了连是参数问题还是数据问题都分不清。
# Linux / macOS 下先给权限 chmod +x ./exbsa # 确认能正常运行,参数名以实际输出为准 ./exbsa --version ./exbsa --help # 用最小归档做一次冒烟测试 ./exbsa extract ./archive/pack_patch.bsa -o ./out/patch -f "*" --keep-path--keep-path是关键。不加它,所有文件会被平铺到输出目录根下,同名文件互相覆盖,纹理和模型混在一起,事后根本没法还原归属。加上它之后,虚拟路径会原样映射成磁盘目录,textures/architecture/wall.dds就落在out/patch/textures/architecture/wall.dds。
Windows 下要注意路径长度。虚拟路径里常见嵌套七八层、单段名字又长,很容易撞上 260 字符限制。我的做法是把输出目录设到盘符根下的短目录,比如D:\x\,或者开启长路径支持后再跑。
提示:先跑
info再跑list,最后才extract。顺序反过来,出了问题你只有一堆半成品文件,没有任何可比对的基准。
3.2 常用参数逐个拆:输出、过滤、覆盖
参数不多,但每一个都直接影响结果正确性,值得单独说清楚。
| 参数 | 取值示例 | 作用 | 用错的后果 |
|---|---|---|---|
-o | ./out/core | 输出根目录 | 目录不存在时部分构建不自动创建 |
-f | "textures/**" | 条目过滤 | 只写一层通配会漏掉子目录 |
--keep-path | 无值 | 保留虚拟路径 | 不加则全部平铺、互相覆盖 |
--overwrite | skip/force/rename | 同名策略 | 默认 skip,增量提取时容易误判为成功 |
--lower | 无值 | 文件名转小写 | 大小写敏感文件系统上会撞名 |
--jobs | 4 | 并发提取数 | 机械盘上开太高反而更慢 |
--overwrite这个参数我吃过亏。默认skip意味着目标文件已存在就跳过,增量提取时看着日志全是“成功”,实际新版本一个都没落下去。后来我改成默认force,只有在做资源对比实验时才用rename保留两份。
# 全量提取,同名覆盖,保留层级,四线程 exbsa extract ./archive/pack_core.bsa \ -o ./out/core \ -f "**" \ --keep-path \ --overwrite force \ --jobs 4 # 只提贴图,用于快速验证归档是否完整 exbsa extract ./archive/pack_core.bsa \ -o ./out/tex \ -f "textures/**" \ --keep-path \ --overwrite skip--jobs不是越大越好。归档是单文件顺序读,并发主要省的是解压和写盘的等待,机械盘上超过 4 反而因为寻道抖动变慢;SSD 上开到 8 有明显收益,再往上边际递减。
3.3 一次完整的批量提取流程
真实场景很少只有一个归档。主包、补丁包、语音包、材质包各自独立,还可能有版本覆盖关系。我一般按“先全量、再比对、后合并”的顺序走。
#!/usr/bin/env bash set -euo pipefail SRC="./archive" DST="./out" mkdir -p "$DST" # 1. 遍历归档,逐个先打印头部统计,异常的直接跳过 for f in "$SRC"/*.bsa; do echo "=== $f ===" if ! ./exbsa info "$f"; then echo "SKIP: header check failed -> $f" >> ./out/broken.log continue fi done # 2. 按文件名排序后依次提取,保证覆盖顺序可控 for f in $(ls "$SRC"/*.bsa | sort); do name=$(basename "$f" .bsa) ./exbsa extract "$f" \ -o "$DST/$name" \ -f "**" \ --keep-path \ --overwrite force \ --verify-crc \ --jobs 4 done # 3. 汇总失败条目 cat "$DST"/*/.failed.log > ./out/all_failed.log 2>/dev/null || true wc -l ./out/all_failed.log这段脚本的价值在于顺序可控。ls | sort看着土,但它保证补丁包在主包之后提取,覆盖关系符合预期。如果按文件系统返回顺序遍历,遇到 ext4 上的哈希序,覆盖结果每次都不一样,排查时能让人怀疑人生。
脚本里set -euo pipefail是必须的,中间任何一步失败立刻停,别让它带着半截状态往下跑。broken.log和all_failed.log两份日志分开记:前者是归档级失败,后者是条目级失败,处理方式完全不同。
4. 提取结果对不上:路径、编码与文件名映射
提取跑完不等于拿到能用的资源。最常见的三类错位是路径层级错、文件名编码错、同名文件互相覆盖。这三件事都不报错,只有你在引擎里加载资源时才会发现贴图是黑的、模型缺件。
4.1 虚拟路径到磁盘路径的映射规则
归档里的路径分隔符统一是反斜杠,落到磁盘上要转成本地分隔符。exbsa 默认做这层转换,但有几个边界要留意:以分隔符开头的绝对式路径、包含..的相对跳转、以及纯大小写差异的同名目录。
# 干跑模式:只打印将要写入的路径,不落盘 exbsa extract ./archive/pack_core.bsa -o ./out/dry -f "**" --dry-run # 重点检查输出里有没有: # ../ 开头的越界路径 # C:\ 这类带盘符的绝对路径 # 只差大小写的重复目录--dry-run是我最推荐的参数,没有之一。它把映射结果提前摊开,你能在上百行输出里一眼看出路径是不是被吃掉了前缀。有些归档的目录记录里带了上层目录名,--keep-path会老老实实拼上去,结果输出成out/core/textures/textures/architecture,多一层。看到这种就用--strip-prefix textures削掉。
4.2 文件名编码:非 ASCII 名字的还原
归档里的文件名字节流没有编码声明,全靠打包时的本地代码页。英文资源没事,一旦有中文、日文或带重音的目录名,直接按 UTF-8 解就是乱码,按 Latin-1 解出来是一串问号。exbsa 一般提供编码参数,指定源编码后再转成目标编码。
# 归档里读到的原始字节,先按常见代码页尝试 raw = b"\xcd\xe2\xb2\xbf\xd7\xca\xd4\xb4" # 一段 8 字节目录名 for enc in ("cp936", "cp932", "cp1252", "utf-8"): try: print(enc, "->", raw.decode(enc)) except UnicodeDecodeError: print(enc, "-> decode failed") # 输出中只有一种编码能解出可读字符,其余要么报错要么是乱码 # 确认后把该编码传给 exbsa 的 --src-encoding这段脚本的作用是试探,不是最终方案。实际用法是把确认下来的编码喂给工具:
# 指定归档内名字的源编码,输出统一转 UTF-8 exbsa extract ./archive/pack_locale.bsa \ -o ./out/locale \ -f "**" \ --keep-path \ --src-encoding cp936 \ --out-encoding utf-8 # --src-encoding 归档内文件名的原始编码 # --out-encoding 落盘时使用的编码,跨平台协作建议统一 utf-8errors策略默认是严格模式,遇到解不了的字节直接报错退出。做批量迁移时我会临时放宽成替换模式,先让它跑完拿到大部分文件,剩下的从.failed.log里单独处理。放宽模式会引入?字符,所以只用于抢救,不用于正式产出。
4.3 用哈希清单做提取后校验
提取完怎么证明没丢件?靠数量对不上时,最省事的办法是拿归档里的文件哈希跟磁盘文件重算的哈希做比对。
import hashlib import pathlib import csv def sha1_of(path: pathlib.Path, chunk: int = 1 << 20) -> str: h = hashlib.sha1() with open(path, "rb") as f: while True: block = f.read(chunk) if not block: break h.update(block) return h.hexdigest() root = pathlib.Path("./out/core") missing, mismatch = [], [] # entries.csv 由 exbsa list --hash --size --format csv 生成 with open("entries.csv", newline="", encoding="utf-8") as f: for row in csv.DictReader(f): rel = row["path"].replace("\\", "/") p = root / rel if not p.exists(): missing.append(rel) continue if sha1_of(p) != row["sha1"]: mismatch.append(rel) print("missing:", len(missing), "mismatch:", len(mismatch))chunk取 1 MB 是折中值,太小系统调用开销大,太大内存占用高。大文件多的时候这个脚本跑几分钟,但换来的是“哪些文件真的坏了”的确切答案,比对着终端滚屏猜强太多。missing一般对应路径映射错误,mismatch对应解压或偏移错位,两类问题分开查。
5. 避坑与排查:五条踩出来的记录
5.1 提取到一半报 offset 越界
现象:前几百个文件正常,某个大文件之后全部失败,日志刷offset out of range。
原因:归档被截断,或者文件记录里声明的大小是解压后大小,工具按它推进偏移,越过真实边界。还有一种情况是下载不完整,文件尾少了几个字节,头部统计却还是原值。
解决:先看info里文件数跟实际能列出的条目数是否一致,不一致基本可以判定截断,重新拿完整包。一致的话加--safe-offset按实际读取量推进,并开--verify-crc把坏条目隔离出来。
5.2 文件名全是下划线或问号
现象:目录结构对,文件名变成____.dds这种。
原因:归档写入时用了本地代码页,提取时按 UTF-8 解,字节序列不合法被替换成占位符。替换是一次性的,落盘之后再改参数也救不回来。
解决:删掉输出目录重提,用--src-encoding指定正确代码页。判断代码页的办法是拿一个文件名长度对得上的条目做字节级比对,别靠猜。
5.3 同名文件互相覆盖,模型贴图错乱
现象:提取日志显示全部成功,进引擎发现模型引用的贴图是另一个物体的。
原因:没加--keep-path,不同目录下的同名文件平铺到一层,后写的把先写的盖了。或者多个归档按无序顺序提取,补丁包被主包覆盖。
解决:强制加--keep-path;批量提取时显式排序,把基础包放前面、补丁包放后面;需要保留冲突证据时用--overwrite rename,让两份都留下。
5.4 提取出来的 dds 打不开
现象:文件大小正常,图片查看器报“不支持的格式”。
原因:归档里的贴图可能是把头部去掉了的裸 DDS 数据,只留像素块,需要按记录里的宽高和格式重建 128 字节头。也可能是压缩块解压后缺少对齐填充。
解决:先拿一个文件看头 4 字节是不是DDS,不是就说明需要重建头。用文件记录里的尺寸和格式字段补一个标准头再喂给查看器,能打开就说明数据本身没坏,问题在封装层。
5.5 图形界面版双击闪退
现象:命令行版跑得好好的,GUI 版启动就退,没有任何提示。
原因:GUI 版通常要读同目录下的配置文件或资源字典,路径里带中文、空格、或者从网络盘启动都会导致初始化失败。也可能缺运行库。
解决:把整个工具目录挪到纯英文短路径下再试;用命令行带--verbose启动看它停在哪一步;实在不行就放弃 GUI,命令行能干的活没必要图形化。
6. 进阶:把 exbsa 接进资源流水线
单次提取解决不了持续迭代的问题。做资源替换的人迟早会遇到“每周来一个新包,怎么只处理变化的部分”。我的做法是把 exbsa 的 list 和 extract 拆成两步,中间用清单做差集。
#!/usr/bin/env bash set -euo pipefail CACHE="./cache" mkdir -p "$CACHE" # 1. 当前归档的条目清单(含哈希) ./exbsa list ./archive/pack_core.bsa \ --hash --size --format csv > "$CACHE/current.csv" # 2. 跟上一版清单做差集,只保留新增或哈希变化的条目 if [ -f "$CACHE/prev.csv" ]; then python3 - <<'PY' import csv def load(p): with open(p, newline="", encoding="utf-8") as f: return {r["path"]: r["sha1"] for r in csv.DictReader(f)} cur, prev = load("cache/current.csv"), load("cache/prev.csv") changed = [p for p, h in cur.items() if prev.get(p) != h] with open("cache/changed.txt", "w", encoding="utf-8") as f: f.write("\n".join(changed)) print("changed:", len(changed)) PY else cp "$CACHE/current.csv" "$CACHE/prev.csv" fi # 3. 按清单逐条提取,避免全量重跑 while IFS= read -r p; do ./exbsa extract ./archive/pack_core.bsa \ -o "./out/core" \ -f "$p" \ --keep-path \ --overwrite force done < "$CACHE/changed.txt" mv "$CACHE/current.csv" "$CACHE/prev.csv"逐条调用看起来笨,但好处是失败可续:哪条挂了单独重试,不用整个包重来。条目多的时候可以把清单切成每批 200 条,用xargs -n 200批量喂进去,减少进程启动开销。
提取结果的验收我固定做三个动作。第一,数量比对,find ./out -type f | wc -l跟清单行数对齐,差一两个就去.failed.log里找。第二,哈希抽检,随机抽 5% 的文件重算哈希,全过就认为整体可信。第三,抽样加载,把贴图和模型各挑几个扔进查看器,确认封装层没问题——哈希对得上但格式不对的情况是存在的。
注意:增量提取一定要保留上一版清单。丢了清单就只能全量重跑,大归档上这个代价不小。
早期我不做清单缓存,每次改一个参数就全量重提,一个 2 GB 的包跑二十多分钟,一天下来光等进度条就耗掉几个小时。后来强制自己先 list 再 extract,把清单当版本号用,同样的活压到几分钟。从那以后我每次动归档都先跑一遍 list 存清单,再决定要不要提取——希望帮到你。
本文还有配套的精品资源,点击获取