C# StreamWriterで文字化け・行が崩れる原因と対策:ANSI/UTF-8/UTF-16と追記の落とし穴

StreamWriterで書いたテキストが「行が崩れる」「文字化けする」ときは、ほぼ文字コードの不一致が原因です。C#のEncoding.UnicodeはUTF-16LEを意味するため、既存ファイルがANSI(Shift_JIS)やUTF-8だと追記で内容が混在します。原因の切り分けと安全な直し方を実例付きで解説します。

目次

起きている現象を整理する:文字化けと「行が崩れる」はセットで起きやすい

テキスト出力のトラブルは「文字が壊れて読めない」だけでなく、次のような見た目の崩れとして現れることがあります。

  • 日本語が「????」「�」になったり、まったく別の漢字になる(典型的な文字化け)
  • 改行位置が不自然、行が途中で折り返されたり、逆に改行されない
  • 1文字ごとに変な空白が入る、NUL文字っぽい記号が混ざる
  • ファイルの前半は正常だが、途中から急に崩れる(追記・追記の繰り返しで起きやすい)

これらは多くの場合、ファイルが実際に使っている文字コードと、StreamReader/StreamWriterで指定したEncodingが一致していないことが原因です。特に「追記(append)」は、既存ファイルの文字コードと違うEncodingで書き足してしまうと、1つのファイルの中に複数の文字コードが混在して復旧が難しくなります。

まず結論:Encoding.UnicodeはUTF-16で、対象ファイルがUTF-8やANSIなら崩れる

質問で使われている指定は次の通りです。

// 書き込み
new StreamWriter(@"z:\oup2.txt", allowappend, Encoding.Unicode);

// 読み込み
new StreamReader(@"z:\inp2.txt", Encoding.Unicode);

.NETにおけるEncoding.Unicodeは、一般的な「Unicode」という意味ではなく、UTF-16(多くの環境でUTF-16LE)を指します。もし inp2.txt や oup2.txt が元々ANSI(日本語環境ならShift_JIS/CP932)やUTF-8で作られているなら、UTF-16として読み書きした瞬間に解釈がズレ、文字化けや改行崩れが発生します。

「ANSI」「Unicode」という呼び方がややこしい理由

トラブルの根本には、用語の曖昧さがあります。特にWindowsでは「ANSI」という言い方がよく登場しますが、これは環境依存のコードページを指すことが多く、1つの固定規格ではありません。

よくある呼び方実体(例)特徴注意点
ANSIWindowsの既定コードページ(日本語ならCP932=Shift_JIS系)古いツールや業務システムで今でも多いPC環境で変わる。BOMがないので自動判定もしにくい
Unicode(曖昧)Unicodeという文字集合(UTF-8/UTF-16/UTF-32などで表現)「Unicode=万能」のように誤解されやすい実際に必要なのは「どのUTFか」の指定
Encoding.UnicodeUTF-16LE(.NETの既定の“Unicode”)英数字でも2バイト単位になりやすいUTF-8/ANSIのファイルに対して使うとほぼ壊れる
UTF-8UTF-8(BOMあり/なし両方あり得る)Web・API・Gitなどの標準。互換性が高いBOMなしUTF-8を誤判定するツールも一部にある

StreamReader/StreamWriterでの文字コード変換のしくみ

StreamReader/StreamWriterは、指定されたEncodingでバイト列⇔文字列を相互変換します。つまり、正しいEncodingを指定すれば正しく扱えますが、間違ったEncodingを指定すると間違った変換が起きます。

たとえば、UTF-8のファイルをUTF-16として読もうとすると、2バイト単位の並びとして解釈してしまい、次のような症状になりがちです。

  • 日本語がほぼ読めない
  • 改行コード(CR/LF)も別の文字として扱われ、行がずれる
  • 一見「行が崩れた」ように見える

追記(append)が特に危険な仕組み

「追記が危険」と言われるのは、単に“書き足すから”ではありません。危険の本質は次の3点です。

  • 既存ファイルの文字コードが固定されているのに、追記側のEncodingが別だと混在する
  • エディタは先頭付近の情報(BOMや推測)で文字コードを決め、後半の混在部分が一気に文字化けする
  • 追記時は通常、BOMが書き直されないため「先頭の判断」が変わらない

たとえば、既存のoup2.txtがUTF-8で作られているのに、追記側でEncoding.Unicode(UTF-16LE)を指定すると、ファイルの後半にUTF-16LEのバイト列が入り込みます。多くのエディタは先頭の情報を優先してUTF-8として読み続けるため、追記した部分が読めなくなります。

さらに厄介なのは、混在したファイルは「1回正しいEncodingで開けば全部直る」というものではない点です。混在後は、境界位置や混在パターンによっては機械的な変換で復旧できないこともあります。だからこそ、混在させない設計が重要です。

安全な対処フロー:入力と出力を同じルールで揃える

最短で安定させるための方針はシンプルです。

  • 入力ファイルの文字コードを正しく読む
  • 出力ファイルの文字コードを決めて統一する
  • 追記が必要なら、既存ファイルの文字コードと必ず一致させる

手順1:入力ファイルの文字コードを確認する

まずはinp2.txtが何の文字コードかを確認します。開発現場では次の方法が現実的です。

  • Visual Studio Codeなど、文字コード表示・変更ができるエディタで確認する(右下にUTF-8/UTF-16などが表示される)
  • Notepad(メモ帳)で開き、名前を付けて保存の画面で現在の文字コードを確認する
  • BOMの有無をプログラムでチェックする(BOMがある場合のみ確実)

BOMがあるファイルなら、StreamReaderの「BOMから自動判定」を使うと安全です。たとえば次のようにします。

// BOMがあれば従い、なければ既定のUTF-8として読む
using var reader = new StreamReader(@"z:\inp2.txt", detectEncodingFromByteOrderMarks: true);
string text = reader.ReadToEnd();

ただし、BOMがないファイル(典型的にはShift_JISやBOMなしUTF-8)は自動判定が難しいため、生成元が分かるなら最初からEncodingを決め打ちした方が確実です。

手順2:出力ファイルの文字コードを決める(おすすめはUTF-8)

新規に作るテキスト出力(ログ、エクスポート、加工結果など)は、受け取り側に制約がない限りUTF-8に統一するのがトラブルが少ないです。理由は次の通りです。

  • Webや各種ツールの標準で、OS・環境差が出にくい
  • 英数字中心ならUTF-16よりファイルサイズが小さくなりやすい
  • Gitなどの差分表示、検索、パイプ処理との相性が良い

UTF-8の「BOMあり/なし」は運用で決めてください。社内で古いツールが混じる場合はBOMありの方が誤判定されにくいことがあります。一方、BOMなしを標準とする環境も多いので、プロジェクトの方針に合わせます。

// UTF-8(BOMなし)で新規作成/追記
var utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
using var writer = new StreamWriter(@"z:\oup2.txt", append: allowappend, encoding: utf8NoBom);
writer.WriteLine("出力テスト");

手順3:可能なら「追記をやめて作り直す」のが最も確実

追記が絡むと混在リスクが跳ね上がるため、運用上許されるなら一度ファイルを作り直すのが最も安全です。たとえば処理の最後で出力を確定するタイプなら、次のように「一時ファイルに全部書いて置き換える」方式が堅牢です。

string outPath = @"z:\oup2.txt";
string tmpPath = outPath + ".tmp";

var utf8NoBom = new UTF8Encoding(false);

// 既存を読み、必要なら変換してから全量を書き直す
using (var writer = new StreamWriter(tmpPath, append: false, encoding: utf8NoBom))
{
// 例:行単位で処理して書く
foreach (var line in File.ReadLines(@"z:\inp2.txt", Encoding.UTF8))
{
writer.WriteLine(line);
}
}

// 置き換え(上書き)
File.Copy(tmpPath, outPath, overwrite: true);
File.Delete(tmpPath);

この方式なら、途中で処理が失敗しても元ファイルが残りやすく、混在ファイルを作りにくいという利点があります。

どうしても追記する場合のチェックポイント

ログのように追記が前提のファイルでは、次のポイントを押さえると事故が減ります。

  • 追記先ファイルの文字コードを「仕様」として固定し、コードにも明示する
  • 別プロセス/別モジュールが同じファイルへ別Encodingで書かないようにする
  • 追記前にファイルを新規作成し、最初の一回で文字コード(とBOMの有無)を確定させる
  • 既存ファイルの文字コードが不明なら、追記ではなく作り直しを選ぶ

改行コードも要確認:CRLF/LFで「行が崩れる」ケース

文字化けとは別に、改行コードの違いで「行が崩れた」と感じることがあります。Windowsの多くの環境はCRLF(\r\n)ですが、Linux/一部ツールはLF(\n)です。

StreamWriterのWriteLineは既定でEnvironment.NewLineを使うため、Windows上ならCRLFになります。受け取り側がLF前提の場合は、明示的に改行を指定するのも一手です。

using var writer = new StreamWriter(@"z:\oup2.txt", append: false, encoding: Encoding.UTF8);
writer.NewLine = "\n"; // LFに統一
writer.WriteLine("line1");
writer.WriteLine("line2");

ただし、今回のように「文字化けも同時に起きている」場合は、まず文字コード不一致を疑うのが近道です。

症状別:原因の当たりを付ける早見表

症状よくある原因確認ポイント解決策
日本語が????/�になる読み込みEncodingが違う(UTF-8をUTF-16で読む等)入力ファイルの文字コードをエディタで確認StreamReaderのEncodingを正しいものに合わせる
前半は正常、途中から崩れる追記で文字コードが混在追記している箇所・別プログラムの存在出力を作り直して文字コードを統一、追記Encodingも固定
1文字ごとに空白/変な記号UTF-16をANSI/UTF-8として開いているファイルがUTF-16LE/BEか、BOMがあるかUTF-16として開くか、UTF-8へ変換して運用統一
改行が変、行がつながる改行コード差(CRLF/LF)または読み込み時の解釈ズレ表示ツールがLFを扱えるか、バイナリで改行を確認writer.NewLineで統一、まずは文字コード一致を確認

実例:質問のコードを壊れにくい形に書き換える

ポイントは「入力のEncodingを正しく」「出力のEncodingを統一」「追記なら既存と同じ」の3つです。たとえばUTF-8に統一する例は次の通りです。

using System.Text;

bool allowAppend = true;

var utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);

// 入力は UTF-8(BOMが付く場合もある)として読みたい
using var reader = new StreamReader(
    @"z:\inp2.txt",
    encoding: Encoding.UTF8,
    detectEncodingFromByteOrderMarks: true);

// 出力も UTF-8(BOMなし)に統一して書く
using var writer = new StreamWriter(
    @"z:\oup2.txt",
    append: allowAppend,
    encoding: utf8NoBom);

string? line;
while ((line = reader.ReadLine()) != null)
{
    // 必要な加工をここに書く
    writer.WriteLine(line);
}

もし入力がShift_JISなど明確に分かっている場合は、入力だけをそのEncodingに置き換えます(出力はUTF-8などに統一してよい)。

BOMを見て判定する簡易コード(BOMがある場合のみ有効)

「このファイルはUTF-8?UTF-16?」の切り分けに、BOMのチェックが役立ちます。BOMがある場合はかなり確実ですが、BOMがない場合は判定できない点に注意してください。

static Encoding? DetectEncodingFromBom(string path)
{
    byte[] bom = new byte[4];
    using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
    int read = fs.Read(bom, 0, bom.Length);


// UTF-8 BOM: EF BB BF
if (read >= 3 && bom[0] == 0xEF && bom[1] == 0xBB && bom[2] == 0xBF)
    return new UTF8Encoding(encoderShouldEmitUTF8Identifier: true);

// UTF-16 LE BOM: FF FE
if (read >= 2 && bom[0] == 0xFF && bom[1] == 0xFE)
    return Encoding.Unicode;

// UTF-16 BE BOM: FE FF
if (read >= 2 && bom[0] == 0xFE && bom[1] == 0xFF)
    return Encoding.BigEndianUnicode;

// UTF-32 LE BOM: FF FE 00 00
if (read >= 4 && bom[0] == 0xFF && bom[1] == 0xFE && bom[2] == 0x00 && bom[3] == 0x00)
    return Encoding.UTF32;

return null; // BOMなしは不明


}

判定結果がnullなら、「BOMがない=不明」です。ここで無理に推測すると事故が増えるため、生成元の仕様を確認してEncodingを決め打ちするか、入力ファイル側をUTF-8に統一する方が現実的です。

.NET(Core/5+/6+/8+)でShift_JISを扱うときの注意

日本語の現場では「ANSI=Shift_JIS(CP932)」がまだ残っています。ところが、.NET(特にCore以降)では、コードページ系のEncodingが標準で使えない構成があります。その場合は、プロジェクトでSystem.Text.Encoding.CodePagesを参照し、次のようにプロバイダ登録を行います。

using System.Text;

Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
var sjis = Encoding.GetEncoding(932); // CP932(Shift_JIS系)

この設定がない状態でShift_JISを指定しようとすると例外になることがあるため、レガシー文字コードを扱うシステムでは早めに組み込みます。

すでに混在してしまったファイルをどうするか

もし「追記で文字コードが混ざった」可能性が高い場合、まずはそのファイルをバックアップしてください。混在ファイルは、編集・保存を繰り返すほど復旧が難しくなることがあります。

  • 混在前のバックアップがあるなら、復元してから正しいEncodingで再生成する
  • 混在部分の開始位置が分かるなら、そこを境に分割し、それぞれ正しいEncodingで読み直して作り直す
  • 開始位置が不明なら、ログ生成側のプログラムを修正して「これ以上混在させない」ことを先に行う

「混在したまま正しく読める万能なEncoding」は存在しません。最終的には、データの生成元の仕様に立ち返って作り直すのが最短ルートになることが多いです。

運用での再発防止:文字コードを「仕様」にする

最後に、同じトラブルを繰り返さないための実務的なルールをまとめます。

  • 入出力の文字コードをドキュメント化し、コードでも明示する(暗黙の既定に頼らない)
  • 「ANSI」「Unicode」という曖昧語を避け、UTF-8 / UTF-16 / CP932のように具体名で会話する
  • 追記ログは特に、最初の作成時点で文字コードを固定し、他モジュールからも同じEncodingで書く
  • 外部から来るファイルは、可能なら受け取り直後にUTF-8へ正規化して内部処理を単純にする
  • エディタや検証手順をチームで統一する(例:VS Codeで文字コード表示を確認する)

StreamWriter/StreamReader自体は「指定どおりに変換している」だけなので、トラブルはほぼ設計と運用の問題です。文字コードの統一と、追記時のEncoding固定を徹底すれば、文字化けや行崩れは安定して解消できます。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次