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つの固定規格ではありません。
| よくある呼び方 | 実体(例) | 特徴 | 注意点 |
|---|---|---|---|
| ANSI | Windowsの既定コードページ(日本語ならCP932=Shift_JIS系) | 古いツールや業務システムで今でも多い | PC環境で変わる。BOMがないので自動判定もしにくい |
| Unicode(曖昧) | Unicodeという文字集合(UTF-8/UTF-16/UTF-32などで表現) | 「Unicode=万能」のように誤解されやすい | 実際に必要なのは「どのUTFか」の指定 |
| Encoding.Unicode | UTF-16LE(.NETの既定の“Unicode”) | 英数字でも2バイト単位になりやすい | UTF-8/ANSIのファイルに対して使うとほぼ壊れる |
| UTF-8 | UTF-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固定を徹底すれば、文字化けや行崩れは安定して解消できます。

コメント