ZIPファイルの日本語ファイル名が文字化けする問題をNode.jsで解決する(Shift-JIS対応)
はじめに
外部システムから受け取ったZIPファイルを展開したところ、中のファイル名が ÉñÉvÉäÉPÅ[ÉVÉáÉì.pdf のような文字化けになっていました。
原因はZIPファイル内のファイル名がShift-JISでエンコードされているのに、展開ライブラリがUTF-8として読んでいたのです。
本記事では、adm-zip と iconv-lite を使って正しくファイル名をデコードする方法を紹介します。
前提・環境
- Node.js + TypeScript
adm-zip(ZIP操作ライブラリ)iconv-lite(文字エンコーディング変換ライブラリ)
なぜZIPファイル名がShift-JISなのか
ZIPファイルの仕様(APPNOTE.TXT)では、元々ファイル名のエンコーディングはIBM Code Page 437(ASCII拡張)と定義されていました。UTF-8のサポートは2006年のバージョン6.3.0で追加され、General Purpose Bit Flag のビット11で「UTF-8使用」を示す仕組みになっています。
しかし、日本のWindows環境で作成されたZIPファイルは以下の理由でShift-JISのままであることが多いです。
- 古いバージョンのWindows標準ZIP機能がUTF-8フラグに対応していなかった
- レガシーシステムが生成するZIPは仕様のアップデートを反映していない
- 一部のアーカイバは互換性を優先してShift-JISを使い続けている
問題の再現
adm-zip でZIPを展開する通常のコードです。
import AdmZip from "adm-zip";
const zip = new AdmZip("input/report.zip");
zip.extractAllTo("output/report", true);
// output/report/ の中身:
// ÉñÉvÉäÉPÅ[ÉVÉáÉì.pdf ← 文字化け
// ÉfÅ[É^.csv ← 文字化け
adm-zip はデフォルトでファイル名をUTF-8として解釈します。Shift-JISのバイト列をUTF-8として読むと、マルチバイト文字が壊れて文字化けになります。
解決: iconv-liteで手動デコード
adm-zip のエントリには rawEntryName プロパティがあり、エンコード前の生バイト列にアクセスできます。これを iconv-lite で正しいエンコーディングに変換します。
import AdmZip from "adm-zip";
import iconv from "iconv-lite";
import path from "path";
import fs from "fs-extra";
interface UnzipOptions {
deleteZip?: boolean;
password?: string;
encoding?: string; // "Shift_JIS", "EUC-JP" など
}
export function unzip(
destFolder: string,
filePath: string,
options: UnzipOptions = {}
): string {
if (!fs.existsSync(filePath)) {
throw new Error(`ZIP file not found: ${filePath}`);
}
const zip = new AdmZip(filePath);
const basename = path.basename(filePath, ".zip");
// エンコーディングが指定されている場合、ファイル名を手動デコード
if (options.encoding) {
zip.getEntries().forEach((entry) => {
if (entry.entryName) {
try {
const decodedName = iconv.decode(
entry.rawEntryName,
options.encoding || ""
);
entry.entryName = decodedName;
} catch (err) {
console.warn(
`Could not decode "${entry.entryName}" as ${options.encoding}`
);
}
}
});
}
const outputDir = path.join(destFolder, basename);
fs.mkdirSync(outputDir, { recursive: true });
// パスワード付きZIPの処理
options.password
? zip.extractAllTo(outputDir, true, true, options.password)
: zip.extractAllTo(outputDir, true);
// ZIPファイルの削除(オプション)
if (options.deleteZip) {
try {
fs.unlinkSync(filePath); // 同期的に削除
} catch (err) {
console.error(`Error deleting ZIP file: ${err}`);
}
}
return outputDir;
}
使い方
// Shift-JIS エンコードのZIPファイルを展開
const outputDir = unzip("output", "input/report.zip", {
encoding: "Shift_JIS",
deleteZip: true,
});
// output/report/ の中身:
// アプリケーション.pdf ← 正しく表示される
// データ.csv ← 正しく表示される
実装のポイント
rawEntryName vs entryName
entry.entryName // → ÉñÉvÉäÉPÅ[ÉVÉáÉì.pdf(UTF-8として解釈済み)
entry.rawEntryName // → Buffer(生バイト列)
rawEntryName はBufferオブジェクトで、エンコーディングの解釈が行われていない状態です。これを iconv.decode() に渡すことで、正しい文字列に変換できます。
unlinkSync を使う理由
元のコードでは fs.unlink(非同期版)を使っていましたが、unzip メソッドが return outputDir した後にファイル削除が実行されるため、タイミングによっては後続処理と競合する可能性がありました。
// Before: 非同期で削除 → 関数リターン後に実行される
fs.unlink(filePath, (err) => { /* ... */ });
return outputDir; // ← 削除完了前にリターン
// After: 同期的に削除 → 削除完了後にリターン
fs.unlinkSync(filePath);
return outputDir;
エンコーディング付きのファイル読み込み
ZIP内のファイルだけでなく、テキストファイルの読み込みでもエンコーディングの指定が必要なことがあります。
export function readWithEncoding(fileName: string, encoding: string): string {
const fileBuffer = fs.readFileSync(fileName);
const fileContent = iconv.decode(fileBuffer, encoding);
return fileContent;
}
// EUC-JP のCSVファイルを読み込む
const csvContent = readWithEncoding("data.csv", "EUC-JP");
エンコーディングの判別方法
ZIPファイルのファイル名がどのエンコーディングで作られたかは、ZIP仕様上は明示されていません。以下の方法で判別します。
1. General Purpose Bit Flagのビット11を確認
zip.getEntries().forEach((entry) => {
const flags = entry.header.flags;
const isUtf8 = (flags & 0x800) !== 0;
console.log(`${entry.entryName}: UTF-8=${isUtf8}`);
});
ビット11が立っていればUTF-8です。立っていなければ、OS/環境依存のエンコーディングが使われています。
2. 発信元で判断
- 日本のWindows環境 → ほぼ確実にShift-JIS
- 日本のUnix/Linux環境 → EUC-JPの可能性あり
- macOS → UTF-8が多い
3. chardet ライブラリで自動判定
import chardet from "chardet";
const encoding = chardet.detect(entry.rawEntryName);
// → "Shift_JIS" or "UTF-8" or "EUC-JP"
ただし、ファイル名は短いため自動判定の精度は高くありません。発信元がわかっている場合は明示的に指定する方が確実です。
まとめ
- 日本の企業システムが生成するZIPファイルは、ファイル名がShift-JISエンコードであることが多い
adm-zipのrawEntryNameで生バイト列を取得し、iconv-liteで正しいエンコーディングにデコードする- ZIP仕様のGeneral Purpose Bit Flagビット11でUTF-8使用を確認できる
- このバグはASCII文字だけのファイル名ではテストで検出できない。日本語ファイル名でのテストケースを必ず含めること






