C# Tips | ファイル・ディレクトリ操作:テキスト一括置換

C sharp C#
スポンサーリンク

はじめに 「テキスト一括置換」は業務の“地味だけど超重要”作業

業務システムを書いていると、こんなことがよく起きます。

古いコードや設定ファイルのパスを一気に変えたい。
ログファイルや CSV の中の特定の文字列をまとめて置き換えたい。
テキストベースのテンプレートから、プレースホルダ({DATE} など)を実データに差し替えたい。

手作業でやるとミスるし、件数が多いと現実的ではありません。
そこで登場するのが「テキスト一括置換」ユーティリティです。

ここでは、プログラミング初心者向けに、
「単一ファイルの置換」から「ディレクトリ配下のファイルを一括置換」まで、
実務でそのまま使える形を、例題付きでかみ砕いて解説します。


基本の一歩 単一ファイルのテキスト置換

一番シンプルな「全部読み込んでから置換」

小さめのテキストファイル(数 MB 程度まで)なら、
「全部読み込む → string.Replace → 上書き保存」が一番分かりやすいです。

using System;
using System.IO;
using System.Text;

public static class TextReplaceUtil
{
    public static void ReplaceInFile(
        string path,
        string oldValue,
        string newValue,
        Encoding encoding)
    {
        if (!File.Exists(path))
        {
            throw new FileNotFoundException("ファイルが存在しません。", path);
        }

        string text = File.ReadAllText(path, encoding);

        string replaced = text.Replace(oldValue, newValue);

        File.WriteAllText(path, replaced, encoding);
    }
}
C#

使い方の例です。

string path = @"C:\data\config.txt";

TextReplaceUtil.ReplaceInFile(
    path,
    oldValue: "http://old-server",
    newValue: "https://new-server",
    encoding: Encoding.UTF8);
C#

ここでの重要ポイントは三つです。

一つ目は、「エンコーディングを明示している」ことです。
UTF-8 なのか Shift_JIS なのかを間違えると文字化けします。
ファイルのエンコーディングが分かっているなら、必ず指定しましょう。

二つ目は、「string.Replace は“全部置換”」だということです。
同じ文字列が何度出てきても、すべて置き換えられます。
部分的に制御したい場合(例:最初の 1 回だけ)は別のロジックが必要になります。

三つ目は、「読み込みと書き込みで同じエンコーディングを使っている」ことです。
読み込みは UTF-8、書き込みはデフォルトエンコーディング、のようにバラバラにすると、
ファイルの中身が壊れる原因になります。


安全性を高めるための工夫 一時ファイル+置き換え

直接上書きは怖いので「一時ファイル経由」にする

上の実装はシンプルですが、
書き込み中にエラーが起きた場合、ファイルが中途半端な状態で残るリスクがあります。

業務で大事な設定ファイルや CSV を扱うときは、
「一時ファイルに書き出してから、最後に入れ替える」ほうが安全です。

using System;
using System.IO;
using System.Text;

public static class SafeTextReplaceUtil
{
    public static void ReplaceInFileSafely(
        string path,
        string oldValue,
        string newValue,
        Encoding encoding)
    {
        if (!File.Exists(path))
        {
            throw new FileNotFoundException("ファイルが存在しません。", path);
        }

        string tempPath = path + ".tmp";

        string text = File.ReadAllText(path, encoding);
        string replaced = text.Replace(oldValue, newValue);

        File.WriteAllText(tempPath, replaced, encoding);

        File.Copy(tempPath, path, overwrite: true);
        File.Delete(tempPath);
    }
}
C#

このパターンのポイントは、「元ファイルは最後の最後まで残しておく」ことです。
書き込み途中で落ちても、元ファイルは壊れません。
最後の Copy が成功した時点で、初めて中身が入れ替わります。


ディレクトリ配下のファイルを一括置換する

指定フォルダ以下の全テキストファイルに対して置換

業務では、「プロジェクト全体の設定ファイルを一気に書き換えたい」
「ログフォルダ配下の全ファイルで特定の文字列をマスクしたい」
といった「複数ファイル一括置換」がよくあります。

まずは、「拡張子を指定して、ディレクトリ配下のファイルを一括置換する」ユーティリティを書いてみます。

using System;
using System.IO;
using System.Text;

public static class BulkTextReplaceUtil
{
    public static void ReplaceInDirectory(
        string rootDirectory,
        string searchPattern,
        string oldValue,
        string newValue,
        Encoding encoding)
    {
        if (!Directory.Exists(rootDirectory))
        {
            throw new DirectoryNotFoundException($"ディレクトリが存在しません: {rootDirectory}");
        }

        foreach (var file in Directory.GetFiles(rootDirectory, searchPattern, SearchOption.AllDirectories))
        {
            SafeTextReplaceUtil.ReplaceInFileSafely(file, oldValue, newValue, encoding);
        }
    }
}
C#

使い方の例です。

BulkTextReplaceUtil.ReplaceInDirectory(
    rootDirectory: @"C:\projects\config",
    searchPattern: "*.config",
    oldValue: "Server=OLD-SQL;",
    newValue: "Server=NEW-SQL;",
    encoding: Encoding.UTF8);
C#

ここでの重要ポイントは、「SearchOption.AllDirectories を指定している」ことです。
これにより、サブディレクトリも含めて再帰的に処理されます。

また、「どのファイルを対象にするか」を searchPattern で制御できるようにしているので、
"*.txt""*.csv" など、用途に応じて柔軟に使えます。


正規表現を使った「パターン置換」

単純な文字列ではなく「パターン」を置き換えたい

例えば、次のような要件を考えてみます。

日付っぽい文字列(2025-01-01 など)を全部 XXXX-XX-XX にマスクしたい。
電話番号のような数字列を ***-****-**** に置き換えたい。

こういうときは、string.Replace ではなく「正規表現(Regex)」を使うと便利です。

using System;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;

public static class RegexTextReplaceUtil
{
    public static void ReplaceInFileByRegex(
        string path,
        string pattern,
        string replacement,
        Encoding encoding)
    {
        if (!File.Exists(path))
        {
            throw new FileNotFoundException("ファイルが存在しません。", path);
        }

        string text = File.ReadAllText(path, encoding);

        string replaced = Regex.Replace(
            text,
            pattern,
            replacement,
            RegexOptions.Multiline);

        File.WriteAllText(path, replaced, encoding);
    }
}
C#

使い方の例です(簡単な日付マスク)。

string path = @"C:\logs\access.log";

RegexTextReplaceUtil.ReplaceInFileByRegex(
    path,
    pattern: @"\d{4}-\d{2}-\d{2}",
    replacement: "XXXX-XX-XX",
    encoding: Encoding.UTF8);

正規表現は強力ですが、そのぶん「やりすぎると意図しないところまで置換してしまう」危険もあります。
業務で使うときは、テスト用のファイルで十分に試してから本番に適用するのが鉄則です。


パフォーマンスとメモリを意識するなら

巨大ファイルに対して「行ごとに置換」する

数百 MB〜数 GB のログファイルなどを扱う場合、
ReadAllText で全部読み込むのは現実的ではありません。

その場合は、「1 行ずつ読みながら置換して、書き出す」スタイルにします。

using System;
using System.IO;
using System.Text;

public static class StreamingTextReplaceUtil
{
    public static void ReplaceInFileStreaming(
        string path,
        string oldValue,
        string newValue,
        Encoding encoding)
    {
        if (!File.Exists(path))
        {
            throw new FileNotFoundException("ファイルが存在しません。", path);
        }

        string tempPath = path + ".tmp";

        using (var reader = new StreamReader(path, encoding))
        using (var writer = new StreamWriter(tempPath, false, encoding))
        {
            string? line;
            while ((line = reader.ReadLine()) is not null)
            {
                string replacedLine = line.Replace(oldValue, newValue);
                writer.WriteLine(replacedLine);
            }
        }

        File.Copy(tempPath, path, overwrite: true);
        File.Delete(tempPath);
    }
}
C#

この方法なら、「常に 1 行分+α」しかメモリを使わないので、
巨大ファイルでも安定して処理できます。

ただし、「ファイル全体をまたいだ置換(複数行にまたがるパターン)」には向きません。
行単位で完結する置換に向いた方法です。


実務ユーティリティとしての設計ポイント

「何をどこまでやるユーティリティか」をはっきりさせる

テキスト一括置換ユーティリティは、便利だからといって何でも詰め込みすぎると、
「結局どう動くのか分からないブラックボックス」になりがちです。

例えば、次のように役割を分けておくと、コードもテストもシンプルになります。

単一ファイルを対象に、単純な文字列置換をする。
単一ファイルを対象に、正規表現で置換する。
ディレクトリ配下の複数ファイルに対して、上記の処理を繰り返す。

そして、「エンコーディング」「一時ファイルを使うかどうか」「ログを出すかどうか」などは、
引数やラッパーメソッドで制御できるようにしておくと、再利用性が高まります。

ログとバックアップをどうするか

業務で怖いのは、「間違った置換をしてしまって、元に戻せない」ことです。

重要なファイルに対して一括置換を行うときは、例えば次のような運用を組み合わせると安心です。

処理前に対象ディレクトリを丸ごとバックアップしておく。
ユーティリティ側で、「元ファイルを .bak などの拡張子でコピーしてから上書きする」。
どのファイルに対して、どんな置換を行ったかをログに残す。

ユーティリティの中で全部やる必要はありませんが、
「一括置換は危険な操作である」という前提を忘れないことが大事です。


まとめ 実務で使える「テキスト一括置換」ユーティリティの考え方

テキスト一括置換は、「手作業だと絶対にミスるところ」を機械に任せるための、強力な道具です。

押さえておきたいポイントは次の通りです。

単一ファイルなら、「読み込む → string.Replace → 書き込む」が基本形。
重要ファイルには、一時ファイル経由で安全に上書きするパターンを使う。
ディレクトリ配下の一括置換は、「対象ファイルの列挙」と「1 ファイル置換」をきれいに分けて設計する。
複雑なパターン置換には正規表現を使うが、やりすぎると危険なのでテスト必須。
巨大ファイルには、行単位で読み書きするストリーミング方式が有効。

ここまで押さえておけば、「怖いから手で置換している」状態から抜け出して、
「安全で再利用可能なテキスト一括置換ユーティリティ」を自分の道具箱に入れられるようになります。

タイトルとURLをコピーしました