先日、Mac製ZIPファイルの文字化け問題について書きましたが、その後さらに調べていたところ、根本的な解決方法につながる情報を見つけました。
その結果、前回の記事で書いた「WindowsがUTF-8に対応していない」という理解は誤りだったことが判明しました🙇実際には、WindowsはUTF-8対応した実装されていました。
文面が「Windows、まだShift_JISに依存しているのかよ~」というものになっていたかもしれません。謹んでお詫びいたします。マジスマン🙇
済まないと思うだけでは良くないので、メモとして残しておきます。
自分で調べた「Mac製Zipファイルの文字化け問題」https://t.co/X8Per9AZXd
— ueponx (@ueponx) 2025年10月14日
根が深いなと思ってもう少し調べたところ以下がヒットhttps://t.co/slSYclENdr
WindowsもZipファイルにフラグがあれば文字化けしないとのこと。これ必須にしておけばみんなハッピーなんじゃねえの?
前回のおさらい
前回のブログでは、MacでUTF-8(NFD)で作成されたZIPファイルがWindowsやUbuntuで文字化けする問題について、以下のような解決方法を紹介しました。
「WindowsがUTF-8に対応していないから」とシャーない感じで理解していましたが、誤解でした。正確には「Mac側がZIP仕様のEFSフラグを設定していないため」です。
新たな発見:EFSフラグの存在
以下のQiita記事を見つけました。
Windows エクスプローラー、7-Zip でファイル名を UTF-8 エンコードした ZIP ファイルを文字化けせずに解凍するためには ZIP ファイル内で EFS が有効にされている必要がある?
この記事を読んで、重要な事実がわかりました。
実は、WindowsエクスプローラーはちゃんとUTF-8に対応しているというのです!
参考エントリーにあるEFSフラグとは
ZIP仕様では、UTF-8を使う場合に設定すべきフラグが定義されています。それがEFS(Language encoding flag)です。
- ビット11: Language encoding flag (EFS)
- 値:
0x0800(16進数) - 効果: このフラグが有効な場合、ファイル名とコメントはUTF-8でエンコードされていると解釈される
Windowsエクスプローラーは、ZIP仕様通りにEFSフラグを見てUTF-8として処理しているのです。つまり、Windowsは仕様に対応している!
真の問題:Mac側がEFSフラグを設定していない
問題は、MacのZIPツールがUTF-8でファイル名をエンコードしているのに、EFSフラグを設定していないことでした。
これでは、Windowsエクスプローラーは「ZIPファイル内にEFSフラグが無い → UTF-8ではない → Shift_JISだろう」と(仕様に従って)判断してしまうため、文字化けが発生していたのです。
Mac側が仕様通りにEFSフラグを設定してくれていれば、問題は起きなかったということになります。
解決策:EFSフラグを有効化するプログラムを作成
この情報を元に、既存のZIPファイルのEFSフラグを有効化するPythonプログラムを準備しました。
プログラムの概要
GitHubにおいておきます。
fix_zip_utf8.py
#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ZIPファイルのEFS (Language encoding flag)を有効化するスクリプト. UTF-8エンコードされたファイル名が文字化けしないようにフラグを設定します。 Windowsエクスプローラーや7-Zipで日本語ファイル名を正しく表示するために使用します。 """ import struct import sys import argparse from pathlib import Path from typing import List, Tuple, Optional, Union class ZipEFSEnabler: """ZIPファイルのEFSフラグを有効化するクラス. ZIP仕様(APPNOTE.TXT)で定義されているLanguage encoding flag (EFS)を ローカルファイルヘッダとセントラルディレクトリヘッダに設定します。 Attributes: LOCAL_FILE_HEADER_SIGNATURE: ローカルファイルヘッダのシグネチャ (PK\x03\x04) CENTRAL_DIRECTORY_SIGNATURE: セントラルディレクトリヘッダのシグネチャ (PK\x01\x02) END_OF_CENTRAL_DIR_SIGNATURE: セントラルディレクトリ終端のシグネチャ (PK\x05\x06) EFS_FLAG: EFSフラグのビットマスク (0x0800 = ビット11) zip_path: 処理対象のZIPファイルパス """ # ZIPファイルの各シグネチャ LOCAL_FILE_HEADER_SIGNATURE: bytes = b'PK\x03\x04' CENTRAL_DIRECTORY_SIGNATURE: bytes = b'PK\x01\x02' END_OF_CENTRAL_DIR_SIGNATURE: bytes = b'PK\x05\x06' # EFSフラグのビットマスク (ビット11) EFS_FLAG: int = 0x0800 def __init__(self, zip_path: Union[str, Path]) -> None: """ZipEFSEnablerを初期化する. Args: zip_path: 処理対象のZIPファイルパス Raises: FileNotFoundError: 指定されたZIPファイルが存在しない場合 """ self.zip_path: Path = Path(zip_path) if not self.zip_path.exists(): raise FileNotFoundError(f"ZIPファイルが見つかりません: {zip_path}") def enable_efs(self, output_path: Optional[Union[str, Path]] = None) -> Tuple[List[Tuple[str, int, int, int]], Path]: """ZIPファイルのEFSフラグを有効化する. ローカルファイルヘッダとセントラルディレクトリヘッダの汎用目的ビットフラグに EFSフラグ(ビット11)を設定します。これにより、ファイル名とコメントが UTF-8エンコードされていることを示します。 Args: output_path: 出力ファイルパス。Noneの場合は元のファイルを上書きします。 Returns: 変更情報のリストと出力ファイルパスのタプル。 変更情報は (ヘッダタイプ, オフセット, 元のフラグ, 新しいフラグ) の形式。 Raises: IOError: ファイルの読み込みまたは書き込みに失敗した場合 """ # ZIPファイルを読み込み with open(self.zip_path, 'rb') as f: data: bytearray = bytearray(f.read()) # 変更箇所を記録 modifications: List[Tuple[str, int, int, int]] = [] # ローカルファイルヘッダのフラグを更新 offset: int = 0 while offset < len(data) - 4: if data[offset:offset+4] == self.LOCAL_FILE_HEADER_SIGNATURE: flag_offset: int = offset + 6 current_flag: int = struct.unpack('<H', data[flag_offset:flag_offset+2])[0] new_flag: int = current_flag | self.EFS_FLAG # フラグが変更される場合のみ記録 if current_flag != new_flag: struct.pack_into('<H', data, flag_offset, new_flag) modifications.append(('ローカルファイルヘッダ', offset, current_flag, new_flag)) # 次のヘッダへスキップ(最小サイズ分) offset += 30 else: offset += 1 # セントラルディレクトリヘッダのフラグを更新 offset = 0 while offset < len(data) - 4: if data[offset:offset+4] == self.CENTRAL_DIRECTORY_SIGNATURE: flag_offset = offset + 8 current_flag = struct.unpack('<H', data[flag_offset:flag_offset+2])[0] new_flag = current_flag | self.EFS_FLAG # フラグが変更される場合のみ記録 if current_flag != new_flag: struct.pack_into('<H', data, flag_offset, new_flag) modifications.append(('セントラルディレクトリヘッダ', offset, current_flag, new_flag)) # 次のヘッダへスキップ(最小サイズ分) offset += 46 else: offset += 1 # 出力パスの決定 if output_path is None: output_path = self.zip_path else: output_path = Path(output_path) # ファイルに書き込み with open(output_path, 'wb') as f: f.write(data) return modifications, output_path def main() -> None: """メイン処理を実行する. コマンドライン引数を解析し、ZIPファイルのEFSフラグを有効化します。 デフォルトでは新しいファイルを作成し、--overwriteオプションで 元のファイルを上書きすることができます。 Raises: SystemExit: エラー発生時または処理中断時 """ parser = argparse.ArgumentParser( description='ZIPファイルのEFSフラグを有効化して、UTF-8ファイル名の文字化けを防ぎます。', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=''' 使用例: # 新しいファイルを作成(デフォルト: 元のファイル名_fixed.zip) python fix_zip_utf8.py input.zip # 出力ファイル名を指定 python fix_zip_utf8.py input.zip -o output.zip # 詳細な変更情報を表示 python fix_zip_utf8.py input.zip -v # 元のファイルを上書き(注意: バックアップを取ることを推奨) python fix_zip_utf8.py input.zip --overwrite ''' ) parser.add_argument('input', help='入力ZIPファイルパス') parser.add_argument('-o', '--output', help='出力ZIPファイルパス(指定しない場合は自動生成)') parser.add_argument('--overwrite', action='store_true', help='元のファイルを上書きする(デフォルトでは新しいファイルを作成)') parser.add_argument('-v', '--verbose', action='store_true', help='詳細な変更情報を表示する') args = parser.parse_args() try: input_path: Path = Path(args.input) # 出力パスの決定 if args.overwrite: output_path: Path = input_path print("⚠ 警告: 元のファイルを上書きします") elif args.output: output_path = Path(args.output) else: # デフォルト: 元のファイル名に _fixed を付ける stem: str = input_path.stem suffix: str = input_path.suffix output_path = input_path.parent / f"{stem}_fixed{suffix}" # 出力ファイルが既に存在する場合の確認(上書きモード以外) if not args.overwrite and output_path.exists(): response: str = input(f"ファイル '{output_path}' は既に存在します。上書きしますか? [y/N]: ") if response.lower() not in ['y', 'yes']: print("処理を中止しました。") sys.exit(0) enabler: ZipEFSEnabler = ZipEFSEnabler(input_path) modifications: List[Tuple[str, int, int, int]] modifications, output_path = enabler.enable_efs(output_path) print(f"✓ EFSフラグを有効化しました: {output_path}") print() if modifications: # 変更箇所をカテゴリごとに集計 local_count: int = sum(1 for m in modifications if m[0] == 'ローカルファイルヘッダ') central_count: int = sum(1 for m in modifications if m[0] == 'セントラルディレクトリヘッダ') print(f"変更箇所: {len(modifications)}件") if local_count > 0: print(f" - ローカルファイルヘッダ: {local_count}件") if central_count > 0: print(f" - セントラルディレクトリヘッダ: {central_count}件") # 詳細モードの場合は個別の変更情報も表示 if args.verbose: print() print("詳細:") for header_type, offset, old_flag, new_flag in modifications: print(f" - {header_type} @ 0x{offset:08X}") print(f" 0x{old_flag:04X} → 0x{new_flag:04X}") else: print("変更箇所: なし(すでにEFSフラグが有効でした)") except KeyboardInterrupt: print("\n処理を中断しました。") sys.exit(1) except Exception as e: print(f"エラー: {e}", file=sys.stderr) sys.exit(1) if __name__ == '__main__': main()
基本的な使い方
# 新しいファイルを作成(input_fixed.zip) $ python fix_zip_utf8.py input.zip # 出力ファイル名を指定 $ python fix_zip_utf8.py input.zip -o output.zip # 詳細な変更情報を表示 $ python fix_zip_utf8.py input.zip -v
実行例
$ python fix_zip_utf8.py mac_archive.zip ✓ EFSフラグを有効化しました: mac_archive_fixed.zip 変更箇所: 12件 - ローカルファイルヘッダ: 6件 - セントラルディレクトリヘッダ: 6件
このプログラムで処理したZIPファイルは、ZIP仕様に準拠した形になるため、Windowsエクスプローラーでも日本語ファイル名が正しく表示されるようになります。
ZIPファイルの技術的な構造の補足
ZIPファイルは、各ファイルごとに以下の2つのヘッダを持っています。
- ローカルファイルヘッダ … 各ファイルの実データの直前にある
- セントラルディレクトリヘッダ … ZIPファイルの最後にまとめて配置される
そのため、ZIPファイル内のファイル数 × 2 箇所のフラグを変更する必要があります。
フラグの位置
- ローカルファイルヘッダ: オフセット+6の汎用目的ビットフラグ
- セントラルディレクトリヘッダ: オフセット+8の汎用目的ビットフラグ
それぞれのフラグに 0x0800 をOR演算で設定します。
参考にしたブログにも書かれているので、詳細はそちらもご確認ください。
おわりに
NKT(長く苦しい戦いだった)
前回は「なぜ文字化けするのか」と「対処療法」について書きましたが、今回は「真の原因」と「根本的な解決方法」を見つけることができました。
そして何より、前回「Windowsに修正してほしい」と書いたのは完全な誤解でしたね。Windowsエクスプローラーもそれに従って正しく実装されていました。
誤解して申し訳ございませんでした🙏これ修正される日はくるのかな?
参考リンク
以前書いたエントリ
Windows エクスプローラー、7-Zip でファイル名を UTF-8 エンコードした ZIP ファイルを文字化けせずに解凍するためには ZIP ファイル内で EFS が有効にされている必要がある?
ZIPファイルの仕様