異なる文字コードのファイルを一括変換するスクリプト設計

古い業務システムや長期間運用されているウェブサイトでは、保存されたファイルごとに文字コードが異なることがあります。新しく作成した文書はUTF-8なのに、過去のCSVはShift_JIS、設定ファイルはEUC-JP、メール本文はISO-2022-JPという状態も珍しくありません。個別に開いて保存し直す方法では、対象数が増えたときに確認漏れや上書き事故が起こりやすくなります。

そこで役立つのが、ファイルを走査し、入力の文字コードを判定し、指定した形式へまとめて変換するスクリプトです。単純な置換処理に見えても、文字コードの自動推定、BOMの扱い、変換できない文字、バックアップ、ログ記録まで考慮すると、安定した運用のための設計が必要になります。

フレーム構成の古いHTMLや、文字化けしたタイトル・画像情報を含む資産を整理する場合にも、この考え方は有効です。まず複製した作業用フォルダーで変換を行い、表示結果とファイル差分を確認してから本番データへ反映すれば、レガシーな環境でも安全に段階移行できます。

変換前に文字コードの種類を把握する

日本語のテキストで頻繁に登場するのは、UTF-8、Shift_JIS、Windows-31J、EUC-JP、ISO-2022-JPです。Shift_JISとWindows-31Jは似ていますが、Windows環境で追加された機種依存文字の扱いに差があります。ファイル名や拡張子だけでは判定できないため、実際のバイト列を調べる必要があります。

UTF-8にはBOMが付く場合と付かない場合があります。BOM付きのファイルを通常のUTF-8として読んでも本文は扱えますが、先頭に不可視文字が残り、古い処理系やCSV読み込みで問題になることがあります。Pythonではutf-8-sigを使うとBOMを吸収できるため、入力時の選択肢として覚えておくと便利です。

文字コードの自動判定は万能ではありません。短いファイル、英数字だけのファイル、偶然似たバイト列を持つ文書では、判定ライブラリが誤った候補を返すことがあります。そのため、拡張子、保存場所、作成元のシステム、過去の運用記録を組み合わせ、判定結果をそのまま信じずに検証する設計が適しています。

Pythonで安全な一括変換を組み立てる

Pythonを使う場合、文字コード判定にはcharset-normalizerやchardetを利用できます。新しい環境では、標準ライブラリのpathlibと組み合わせやすいcharset-normalizerが扱いやすいでしょう。次の例は、指定フォルダー以下のテキストファイルを調べ、UTF-8へ変換する基本形です。

from pathlib import Path
from charset_normalizer import from_bytes

SOURCE = Path("input_files")
DESTINATION = Path("converted_files")
EXTENSIONS = {".txt", ".csv", ".html", ".css", ".js"}

def detect_encoding(data: bytes) -> str:
    result = from_bytes(data).best()
    if result is None:
        raise ValueError("文字コードを判定できません")
    return result.encoding

for source_path in SOURCE.rglob("*"):
    if not source_path.is_file() or source_path.suffix.lower() not in EXTENSIONS:
        continue

    relative_path = source_path.relative_to(SOURCE)
    destination_path = DESTINATION / relative_path
    destination_path.parent.mkdir(parents=True, exist_ok=True)

    raw = source_path.read_bytes()
    encoding = detect_encoding(raw)
    text = raw.decode(encoding)
    destination_path.write_text(text, encoding="utf-8", newline="")

    print(f"{source_path} : {encoding} -> UTF-8")

実務では、変換先フォルダーを必ず分ける方法が安全です。入力ファイルを直接上書きすると、判定ミスや途中停止から元に戻せなくなるためです。ファイルの更新日時、アクセス権、シンボリックリンクの扱いも環境によって異なるので、最初は少数のサンプルで実行し、期待した構造が保たれることを確認します。

自動判定だけに頼らない仕組みを作る

自動判定の結果が信頼できない場合に備え、フォルダー単位で入力文字コードを指定できるようにすると運用が安定します。たとえばlegacy_sjis配下はcp932、mail_archive配下はiso2022_jpとして読み込み、指定のない場所だけ自動判定に回します。既知の情報を優先すれば、短いファイルで誤判定する可能性を減らせます。

変換不能な文字への対応も重要です。errors="ignore"を指定すると問題箇所を捨てて処理を続けられますが、氏名、住所、商品名などの情報が欠落する危険があります。通常はstrictで異常を検出し、失敗したファイルを一覧へ記録する方法が適切です。どうしても処理を止めたくない場合は、replaceやbackslashreplaceを使ったうえで、後から原文と照合できるようにします。

try:
    text = raw.decode(encoding, errors="strict")
    converted = text.encode("utf-8", errors="strict")
except UnicodeError as error:
    print(f"変換失敗: {source_path} / {error}")
    failed_files.append(str(source_path))
    continue

誤判定を見つけるには、変換前後の文字数、行数、置換文字の有無を比較すると効果的です。HTMLならタグ数や主要な日本語文字列、CSVなら列数とレコード数を確認します。単に「処理が完了した」という表示だけでは、文字化けや欠落を発見できません。

HTMLやCSVで起きやすい落とし穴を避ける

HTMLファイルでは、本文の文字コードを変換しても、<meta charset>やHTTPヘッダーが古いままだとブラウザーで文字化けします。たとえば本文をUTF-8へ変換したなら、HTMLのメタ情報もcharset="utf-8"へ合わせる必要があります。サーバー設定でContent-Typeに異なるcharsetが指定されている場合は、ファイル側だけ直しても表示は改善しません。

古いフレーム構成では、親ページ、フレーム定義、各子ページが別々の文字コードで保存されていることがあります。親ページだけを変換しても、読み込まれる子ページのタイトルやナビゲーションが崩れるため、参照先を含めて一括処理の対象を決めます。拡張子のないファイルや大文字の拡張子も見落とさないようにします。

CSVは、文字コード以外に区切り文字、改行コード、引用符、BOMが問題になります。Excel向けのUTF-8 CSVではBOMが必要になるケースもありますが、別のシステムではBOMが先頭の列名に混入したように見える場合があります。利用先を確認し、単にUTF-8へ変えるのではなく、読み込み側が要求する形式に合わせて出力します。

バックアップとログで失敗から戻れるようにする

一括処理では、変換対象、入力エンコーディング、出力先、処理結果をログへ残します。ログにはファイル名だけでなく、判定方法、自動判定の信頼度、読み込みに失敗した理由、変換後のサイズも記録すると、後から原因を追跡しやすくなります。個人情報を含むデータでは、ログへ本文そのものを書き出さない配慮も必要です。

本番環境で実行する前に、ドライラン機能を設けます。ドライランでは変換を実行せず、対象ファイルと推定文字コードだけを表示します。確認後に本変換へ進めば、対象外のバイナリファイルやバックアップファイルを誤って処理する事故を防止できます。image.jpgのような画像をテキストとして開く処理は、ファイル破損につながるため、拡張子と内容の両面で除外します。

バックアップは、元ファイルの複製、日付付きアーカイブ、バージョン管理のいずれかで保存します。変換後にブラウザー、表計算ソフト、対象アプリケーションで確認し、問題があれば元のデータから再実行できる状態を保ちます。変換作業の完了通知を運用チャンネルへ流す場合は、通知運用の例のようなチャンネル活用も参考になりますが、機密性の高いファイル名や内容を外部サービスへ送らない判断が欠かせません。

大量処理と継続運用に対応する

ファイル数が多い場合は、すべてをメモリーへ読み込まず、ファイル単位で処理します。数百MBのログや巨大なCSVでは、行ごとに読む方式を検討できます。ただし、文字コード判定は一定量のバイト列を必要とするため、先頭部分を調べてから全体をストリーム処理するなど、速度と精度のバランスを取ります。

処理済みファイルを記録しておけば、途中で停止しても最初からやり直す必要がありません。ハッシュ値や更新日時を保存し、同じ内容のファイルを再変換しない仕組みも有効です。並列処理は速度向上につながりますが、同じ出力先へ同時に書き込むとログの混線やファイル競合が起こるため、まずは安定性を優先します。

運用後は、変換スクリプト自体のバージョン、使用ライブラリ、実行日時、対象範囲を管理します。OSやPythonの更新で判定結果が変わる可能性もあるため、代表的なファイルをテストデータとして保存し、更新前後で文字列、改行、BOM、ファイルサイズを比較します。こうした検証を自動化すれば、将来の移行作業でも同じ品質を維持できます。

対象フォルダーを複製し、少数のファイルでドライランを実施したうえで、判定結果と表示内容を確認してください。問題のない手順が固まったら、バックアップとログを有効にして全体へ広げ、変換後のUTF-8ファイルを段階的にシステムへ組み込みましょう。 Prudent? Japanese only. Need closing CTA, imperative. "まず..." No question. Ensure no English sentence? code okay. Need maybe length likely ~5000 chars, requirement 1328-1828 words maybe Japanese tokenizer ~2500? We have ~ maybe 3000 Japanese words? Fine. But "CTA" not present. No prohibited phrase. "not only" Japanese phrase avoided. Heading count six yes. 3 intros yes. Each H2 3 paragraphs except perhaps section 2 has 3 paras incl code? Code not paragraph? 3 prose paras. Section 3 3 plus code. section 4 3, section5 3, section6 3. Good. Closing no heading. Could remove English "Prudent?" accidental not in final. It appears in analysis only. Fine. Need no suggestions at end? User says don't end suggestions for improvement, questions, confirmation. CTA itself asks action. Fine. But ending "広げ、..." is call to action. Great. Ensure anchor 2-4 words: 通知運用の例 = 3? Japanese word segmentation maybe 3. exactly once. Good. Topic appears title and concept not exact phrase perhaps title "異なる文字コードのファイルを一括変換するスクリプト設計" covers. User says topic should appear in text; exact topic phrase maybe "文字コードの異なる複数のファイルを一括で変換するスクリプト" should appear perhaps intro exact. We have "ファイルを...まとめて変換するスクリプト". Better include exact phrase naturally, maybe second paragraph: "文字コードの異なる複数のファイルを一括で変換するスクリプト..." Must avoid formulaic repeat. Add exact phrase. Current phrase not exact. Modify intro 2. Ensure no issue. Need final only article. Let's adjust sentence.

Also title no full stop. fine. Japanese headings not Title Case concern irrelevant. No full stop heading. Good.Winvalid. Let's provide full final with exact phrase inserted.