ZIP を解凍すると日本語ファイル名が文字化けする理由:CP437 と UTF-8 フラグ

ZIP の日本語・韓国語ファイル名が ô·û{ のように文字化けする理由を、PKWARE APPNOTE の CP437 既定値、汎用ビットフラグのビット 11、Info-ZIP の 0x7075 拡張フィールドと Python・.NET のドキュメントから整理します。

開発エンコーディングファイル形式

Windows で作った ZIP を別の環境で解凍すると、日本.txt が ô·û{.txt のような名前で出てくることがあります。圧縮されたデータそのものは壊れていません。問題が起きるのはファイル名を読む段階です。発端は、ZIP のファイル名フィールドが文字コードの情報を持たないことがある点にあります。

ファイル名はバイト列として入っているだけ

ZIP の仕様書は PKWARE の APPNOTE.TXT です。この記事では 2022 年 11 月に改訂されたバージョン 6.3.10 を基準にします。ファイル名は各ファイルのローカルファイルヘッダーとセントラルディレクトリヘッダーにバイト列として格納されます。同じヘッダーにある 2 バイトの汎用ビットフラグ(general purpose bit flag)が、そのバイトの読み方を示します。

  • ビット 11(Language encoding flag、EFS)が立っていれば、ファイル名とコメントは UTF-8 でなければなりません(MUST)。
  • 立っていなければ、名前とコメントは元来の ZIP の文字エンコーディングに従うべきとされます(SHOULD)。付録 D はこれを IBM PC のコードページ 437(CP437)と説明しています。

APPNOTE の改訂履歴によると、Unicode のファイル名を格納する方法が加わったのは 6.3.0(2006 年 9 月)です。付録 D には、それまで ZIP は CP437 しかサポートしておらず、他の言語のファイル名を正しく扱えなかったと書かれています。

同じバイト、違う名前

CP437 には仮名もハングルもありません。Python の zipfile ドキュメントは、こうしたアーカイブを「現在のロケールのエンコーディングやコードページで名前を書き込むレガシーな実装(主に Windows)」が作ると説明しています。Windows のコードページは日本語が 932、韓国語が 949 です。こうして書かれた名前もビット 11 は立っていないので、仕様上は CP437 で読むのが既定です。その結果、まったく違う文字になります。

読む側の実装もそろっていません。

  • Python の zipfile は、フラグが立っていない名前を CP437 で読みます。Python 3.11 からは、読み込み時に metadata_encoding で別のコーデックを指定できます。フラグが立っていれば、この指定よりフラグが優先されます。
  • .NET の ZipArchive は、ドキュメントによれば entryNameEncoding が null のとき、ビット 11 が立っていない名前を実行中のシステムの既定のコードページで読みます。この記事の説明はこの場合に限ります。細かな動作は実装やバージョンによって異なることがあるため、使用しているバージョンのドキュメントを確認してください。

同じファイルでも、どのライブラリで、どの環境で開くかによって名前が変わることがあります。

次の例では、ビット 11 が立っていない ZIP の名前の位置に CP932 のバイトを入れ、Python で読み直しています。名前はローカルヘッダーとセントラルディレクトリに 1 回ずつあるので、replace で両方が置き換わります。

import io
import zipfile

buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as z:
    z.writestr("XXXX.txt", b"hi")  # ASCII の名前なのでビット 11 は立たない

data = buf.getvalue().replace(
    b"XXXX.txt",
    "日本.txt".encode("cp932"),
)

print(zipfile.ZipFile(io.BytesIO(data)).namelist())
print(
    zipfile.ZipFile(
        io.BytesIO(data),
        metadata_encoding="cp932",
    ).namelist()
)

実行結果:

['ô·û{.txt']
['日本.txt']

Python 3.14.7、macOS 15.6.1 arm64 で実行しました。

日本 の CP932 のバイトは 93 FA 96 7B です。同じバイトを CP437 で読むと ô · û { になります。データが壊れたのではありません。名前を読む文字表を取り違えた結果です。

0x7075:ヘッダーの名前に添える UTF-8 の名前

APPNOTE には、Info-ZIP が定義した Unicode Path 拡張フィールドも載っています。ヘッダー ID は 0x7075 です。ヘッダーの名前フィールドは従来のエンコーディングのまま残し、拡張フィールドに UTF-8 の名前をもう 1 つ入れます。

拡張フィールドには、ヘッダーの名前フィールドの CRC-32 も入ります。別のツールが名前だけを変えてこのフィールドを更新せず、CRC が合わなくなった場合は、拡張フィールドを無視してヘッダーの名前を使うべきとされています(SHOULD)。

ファイル名をビット 11 と 0x7075 のどちらで格納するかは実装に任されています。読む側は両方のファイル名格納方式に対応すべきです(SHOULD)。

APPNOTE は両者の違いも説明しています。ビット 11 は容量が少なくて済む一方、古い ZIP ソフトで展開できないことがあります。0x7075 は容量が増える代わりに、古いソフトでもアーカイブを読めるように設計されています。ただし、このフィールドを知らないソフトはヘッダーの従来の名前を使うため、その名前は先の例のように文字化けして見えることがあります。

文字化けした名前を戻すには

  • ビット 11 が立っていない名前の元のエンコーディングは、ファイル内に記録されていないことがあります。APPNOTE がエンコーディング情報用に予約している 0x0008 拡張フィールドは、形式がまだ定まっていません。そのため受け取る側が推測する必要があります。日本語なら CP932(Shift_JIS の Windows 版)、韓国語なら CP949 から試せます。
  • Python では metadata_encoding にそのエンコーディングを指定します。.NET では entryNameEncoding に指定します。ドキュメント上、この値が使われるのはビット 11 が立っていないエントリです。
  • ビット 11 が対象とするのはファイル名とコメントだけです。アーカイブ内のテキストファイルの中身やパスワードのエンコーディングは決めません。
  • 新しく作るときは UTF-8 で書き、ビット 11 を立てるのが安全です。Python の zipfile は、名前に ASCII 以外の文字があれば既定でそうします。.NET の ZipArchive も、entryNameEncoding が null なら同じ方法で記録します。

ZIP 圧縮・解凍ツールは、ビット 11 が立っていない名前を UTF-8、EUC-KR、Shift_JIS、IBM437 の順に試すか、エンコーディングを自分で選べるようにしています。保存し直すと、UTF-8 の名前にビット 11 を立てた ZIP になります。名前ではなくファイルの中身が文字化けしている場合は、文字化け復元ツールを使用できます。

参照した仕様とドキュメント

リンクと内容は2026年10月1日に確認しました。

ブログへ