HN 日本語サマリー

← 一覧へ戻る
プログラミング

バイトオーダーマークに関するバグの修正

Fixing a bug with byte order marks (alexwlchan.net)

6 pointsby surprisetalk6 コメント

要約

筆者はSRT形式の字幕ファイルをWebVTT形式に変換する際に、UTF-8エンコーディングのバイトオーダーマーク(BOM)の扱いでバグに遭遇しました。BOMを正しく処理しないと、変換後のファイルで問題が発生することが判明しました。Pythonの`encoding="utf-8-sig"`オプションや`ripgrep`コマンドラインツールを活用して、この問題を解決し、既存の破損ファイルを修正するプロセスを解説しています。

全文翻訳

バイトオーダーマークに関するバグの修正 投稿日 2026年7月18日 最近、ローカルメディアライブラリの字幕ファイルを整理していました。字幕にはSRT(SubRip Subtitle)とWebVTT(Web Video Text Tracks)という2つの一般的なファイル形式があります。 私はWebVTT形式に標準化しています。なぜなら、WebVTTはHTML5の<video>要素と連携し、私はすべての動画を静的ウェブサイトに埋め込まれた<video>要素で再生しているからです。しかし、多くの字幕はSRT形式でしか利用できないため、SRTファイルをWebVTTに変換するPython関数を作成しました。形式は単純に見え、変換も簡単だと思われました。有名な最後の言葉です! 変換された字幕をチェックしたところ、バイトオーダーマーク(BOM)の処理にバグがあることに気づき、修正にはいくつかのステップが必要でした。 UTF‑8 BOMの確認漏れ バイトオーダーマークは、テキストファイルの先頭にあるゼロ幅ノーブレークスペース文字U+FEFFの特別な使い方で、ファイルを読み取るプログラムにテキストがどのようにエンコードされているかを伝えます。これは、文字をエンコードするために使用されるバイトの正確なシーケンスに依存します。例をいくつか示します。 EF BB BF – ファイルはUTF‑8テキストです。UTF‑8は常に同じバイト順序を持つため、エンコーディングについて教えてくれるだけです。 FE FF – ファイルはUTF‑16テキストで、ビッグエンディアンバイト順序(UTF‑16BE)です。 FF FE – ファイルはUTF‑16テキストで、リトルエンディアンバイト順序(UTF‑16LE)です。 00 00 FE FF – ファイルはUTF‑32テキストで、ビッグエンディアンバイト順序です。 私のSRT入力ファイルはすべてUTF‑8エンコーディングされており、そのうちのいくつかはUTF‑8バイトオーダーマークを持っていましたが、私はそれを正しく処理していませんでした。例えば、次のような入力SRTファイルがあったとします。 <U+FEFF>1 00:00:01,001 --> 00:00:10,010 You have grown, Keyne. 2 00:02:00,002 --> 00:20:00,020 Soon you’ll be needing another name. WebVTTに変換する際、WebVTTヘッダーを追加し、シーケンス番号を削除し、タイムスタンプ形式を変更したいと考えていました。 シーケンス番号を削除するために、行がすべて数字かどうかをチェックしていました。BOMが最初のシーケンス番号と同じ行にあるため、その行はすべて数字ではありませんでした。そのため、削除しませんでした。代わりに、BOMを含めて行全体をWebVTTファイルの中央にコピーしました。 WEBVTT <U+FEFF>1 00:00:01.001 --> 00:00:10.010 You have grown, Keyne. 00:02:00.002 --> 00:20:00.020 Soon you’ll be needing another name. 正しい変換では、バイトオーダーマークと最初のシーケンス番号の両方が削除されるはずです。 WEBVTT 00:00:01.001 --> 00:00:10.010 You have grown, Keyne. 00:02:00.002 --> 00:20:00.020 Soon you’ll be needing another name. 私のローカルメディアライブラリでは、すべてUTF‑8であると想定できます。バイトオーダーマークを安全に削除でき、ウェブブラウザは引き続き字幕を正しくデコードします。 エンコーディング="utf-8-sig"でコンバーターを修正 最初の修正では、BOMを手動で処理しようとしました。U+FEFFを探し、ファイルからストリップするコードを書きました。検出して変換されたWebVTTファイルに再挿入しようとしました。(これは、単に完全に削除できることに気づく前でした。)低レベルのテキストエンコーディングコードと高レベルの字幕変換ステップを混在させていたため、少し煩雑でした。 この記事を調査しているうちに、よりエレガントな解決策があることに気づきました。SRTファイルを`encoding="utf-8-sig"`で開けば、Pythonはファイル先頭のオプションのUTF‑8エンコードされたBOMを自動的に検出し、スキップしてくれます。残りのコードは、BOMが存在することを知ることも気にかけることもありません。 このバグを関数で修正したため、将来の変換は正しく機能するようになります。しかし、すでに生成された破損ファイルはどうなるでしょうか? ripgrepでUTF‑8 BOMを検出する 最初はTextMateでU+FEFFを検索しようとしましたが、その検索では一貫してクラッシュしたため、コマンドラインツールに切り替えました。 テキスト検索にはripgrepを使用しています。デフォルトでは、ファイルに対して「BOMスニッフィング」を行います。ファイルを読むとき、最初の数バイトを見て、ファイルを実際のエンコーディングからUTF‑8にトランスコードし、トランスコードされたバージョンで検索を実行します。これはBOMが使用されるべき方法ですが、BOM自体が検索対象である場合にはあまり役立ちません! 代わりに、正規表現で`(?-u:...)`を使用して、ripgrepのUnicodeサポートを無効にし、生のバイトを検索できます。(このフラグはRustのregexクレートから来ています。)次のコマンドは、UTF‑8 BOMで始まる行を探します。 $ rg '^(?-u:\[xEF\]\[xBB\]\[xBF\])' ファイル先頭のBOMのみを探している場合は、`--multiline`フラグも使用します。これにより、キャレット`^`は任意の行の先頭ではなく、ファイルの先頭にアンカーされます。しかし、ファイルの中間にBOMを探しているため、`--multiline`を省略するのが正しいです。 この検索により、破損したBOMを持つ数十個のファイルが見つかりました。最初はTextMateで破損ファイルを開いて手動で編集しました。 $ rg --files-with-matches --null '^(?-u:\[xEF\]\[xBB\]\[xBF\])' | xargs -0 mate しかし、すぐに遅すぎると気づいたため、すべてのファイルを一度にクリーンアップするPythonスクリプトを作成しました。 #!/usr/bin/env python3 import glob for filepath in glob.glob("**/*.vtt", recursive=True): with open(filepath, "rb") as f: content = f.read() if b"\xef\xbb\xbf" in content: content = content.replace(b"\xef\xbb\xbf", b"") with open(filepath, "wb") as f: f.write(content) print(filepath) このスクリプトを実行した後、ripgrepコマンドを使用して正しく機能しているか確認しました。確かに、すべての不正なBOMがメディアコレクションから削除されていました。字幕ファイルはGitリポジトリでも管理しているため、スクリプトが他の変更を導入していないことも確認できました。 このバグの前は、バイトオーダーマークについて漠然と聞いたことがあるだけで、実際にそれに対処したことはありませんでした。このような教訓こそが、ローカルメディアアーカイブを手作りの静的ウェブサイトとして管理するのが好きな理由です。ローファイなアプローチは、低レベルのアイデアを探求し、コンピューター上で実際に何が起こっているかを学ぶ多くの機会を与えてくれます。