連載
» 2017年02月08日 05時00分 UPDATE

.NET TIPS:正規表現を使って文字列を置換するには?[C#/VB]

RegexクラスのReplaceメソッドで、正規表現にマッチする部分を置換したり削除したりする方法を解説。また、高度な正規表現の使い方も取り上げる。

[山本康彦,BluewaterSoft/Microsoft MVP for Windows Development]
「.NET TIPS」のインデックス

連載目次

 ある文字列の中で、正規表現パターンに一致する全ての部分文字列を置換するには、Regexクラス(System.Text.RegularExpressions名前空間)のReplaceメソッドを利用する。本稿では、Replaceメソッドの使い方を解説するとともに、少し高度な正規表現の書き方も紹介する。

 正規表現の基本的な書き方については、「.NET TIPS:正規表現を使って文字列がパターンに一致するか調べるには?[C#/VB]」と「.NET TIPS: 正規表現を使ってパターンに一致する全ての文字列を抽出するには?[C#/VB]」をご覧いただきたい。また、RegexクラスのReplaceメソッドは.NET Frameworkの初期からあるものだが、掲載したサンプルコードの全てを試すにはVisual Studio 2015以降が必要である。

正規表現を使って文字列を置換するには?

 RegexクラスのReplace静的メソッドを使えばよい(次のコード)。

 なお、RegexクラスにはReplaceインスタンスメソッドもある。また、Replaceメソッドにオプション引数を渡す方法は、IsMatchメソッドやMatchesメソッドなどと同じである。静的メソッドとインスタンスメソッドの使い分けや、オプション引数を渡す方法については、「.NET TIPS:正規表現を使って文字列がパターンに一致するか調べるには?[C#/VB]」をご覧いただきたい。

using System.Text.RegularExpressions;
……省略……
string result
  = Regex.Replace("{置換対象文字列}", "{正規表現パターン}", "{置換パターン}");

Imports System.Text.RegularExpressions
……省略……
Dim result As String _
  = Regex.Replace("{置換対象文字列}", "{正規表現パターン}", "{置換パターン}")

Replace静的メソッドの使い方(上:C#、下:VB)
第1引数に置換対象の文字列を与え、第2引数には置換する部分文字列を特定するための正規表現パターンを与え、第3引数には置き換える「置換パターン」を与える。

 上に示した書式だけを見ると、Stringクラス(System名前空間)のReplaceメソッドと同じように思える。だが、置換対象部分の指定に正規表現パターンが使えることに加えて、置換する文字列として「置換パターン」も指定できることで、柔軟で多彩な置換処理が可能になっているのだ。

実際の例

 まずは、正規表現パターンも置換パターンも使わないコンソールアプリの例を示そう(次のコード)。この場合は、StringクラスのReplaceメソッドと同じ結果が得られる。

using System.Text.RegularExpressions;
using static System.Console;

class Program
{
  static void Main(string[] args)
  {
    // 「猫」を「子犬」に置換
    // StringクラスのReplaceメソッド
    WriteLine("吾輩はである。吾輩もである。".Replace("猫", "子犬"));
    // 出力:吾輩は子犬である。吾輩も子犬である。

    // RegexクラスのReplace静的メソッド
    WriteLine(Regex.Replace("吾輩はである。吾輩もである。", "猫", "子犬"));
    // 出力:吾輩は子犬である。吾輩も子犬である。

#if DEBUG
    ReadKey();
#endif
  }
}

Imports System.Text.RegularExpressions
Imports System.Console

Module Module1
  Sub Main()
    ' 「猫」を「子犬」に置換
    ' StringクラスのReplaceメソッド
    WriteLine("吾輩はである。吾輩もである。".Replace("猫", "子犬"))
    ' 出力:吾輩は子犬である。吾輩も子犬である。

    ' RegexクラスのReplace静的メソッド
    WriteLine(Regex.Replace("吾輩はである。吾輩もである。", "猫", "子犬"))
    ' 出力:吾輩は子犬である。吾輩も子犬である。

#If DEBUG Then
    ReadKey()
#End If
  End Sub
End Module

RegexクラスのReplace静的メソッドを使う例(上:C#、下:VB)
StringクラスのReplaceメソッドも、RegexクラスのReplace静的メソッドも、対象文字列内で見つかった全ての部分文字列を置換する。この例では、「猫」が2箇所とも「子犬」に置き換わっている。
なお、C#コードの冒頭にある「using static System.Console;」という書き方は、Visual Studio 2015からのものだ。詳しくは、「.NET TIPS:構文:クラス名を書かずに静的メソッドを呼び出すには?[C# 6.0]」をご覧いただきたい。同様な機能がVBには以前から備わっており、「.NET TIPS:VB.NETでクラス名を省略してメソッドや定数を利用するには?」で解説している。
また、「Main」メソッド末尾にReadKeyメソッドを置く意味は、「.NET TIPS:Visual Studioでコンソール・アプリケーションのデバッグ実行時にコマンド・プロンプトを閉じないようにするには?」をご覧いただきたい。

 上のコードでは冒頭で2つの名前空間をインポートしている。同じインポートが以降のサンプルコードでも必要であるが、以降では省略させていただく。

パターンに一致した部分を削除するには?

 置換する文字列として空文字(=String.Empty)を与えればよい。

 Replace静的メソッドをよく使うのは、正規表現パターンに一致した部分文字列を削除する処理だ。置換パターンとして空文字を与えれば、パターンに一致した部分を削除したことになるのである(次のコード)。

// パターンに一致した部分を削除する
// 「【」と「】」を削除する
WriteLine(Regex.Replace("吾輩は【猫】である。吾輩は【子犬】である。",
                        "[【】]", string.Empty));
// 出力:吾輩は猫である。吾輩は子犬である。

' パターンに一致した部分を削除する
' 「【」と「】」を削除する
WriteLine(Regex.Replace("吾輩は【猫】である。吾輩は【子犬】である。",
                        "[【】]", String.Empty))
' 出力:吾輩は猫である。吾輩は子犬である。

パターンに一致した部分を削除する例(上:C#、下:VB)
正規表現「[【】]」は、「【」と「】」のどちらか1文字という意味だ。つまり、このReplaceメソッドの呼び出しでは、「【」と「】」を空文字(=String.Empty)に置き換える(=削除する)ことになる。

 このような削除処理として、HTMLやXMLのデータからタグ部分を取り除いてテキストだけを残す処理がある。その方法を「.NET TIPS:正規表現を使って文字列から部分文字列を取り除くには?[C#、VB]」で詳しく解説しているので、ご覧いただきたい。

繰り返し置換するには?

 拡張メソッドを作るとよい。

 置換処理を繰り返すとき、StringクラスのReplaceメソッドは連続したメソッドチェーンの形で書ける。RegexクラスのReplace静的メソッドではチェーンできないので、一時変数に格納してから次のReplace静的メソッドを書くことになる。しかし、次のコードのように拡張メソッドを作っておけば、すっきりメソッドチェーンとして書けるのである。

public static class RegexExtension
{
  public static string RegexReplace(this string input,
                                    string pattern, string replacement)
  {
    return Regex.Replace(input, pattern, replacement);
  }
}

……省略……

// 置換の繰り返しの例:「猫」を「子犬」に変え、「【」と「】」を削る
// String.Replaceならメソッドチェーンで書ける
WriteLine("吾輩は【猫】である。吾輩も【猫】である。"
          .Replace("猫", "子犬")
          .Replace("【", string.Empty)
          .Replace("】", string.Empty)
          );
// 出力:吾輩は子犬である。吾輩も子犬である。

// Regexではチェーンできないので、一時変数への代入が必要
string work = Regex.Replace("吾輩は【猫】である。吾輩も【猫】である。",
                            "猫", "子犬");
string result = Regex.Replace(work, "[【】]", string.Empty);
WriteLine(result);
// 出力:吾輩は子犬である。吾輩も子犬である。

// でも、Regexだって拡張メソッドを書けばメソッドチェーンが可能
WriteLine("吾輩は【猫】である。吾輩も【猫】である。"
          .RegexReplace("猫", "子犬")
          .RegexReplace("[【】]", string.Empty)
          );
// 出力:吾輩は子犬である。吾輩も子犬である。

Public Module RegexExtension
  <System.Runtime.CompilerServices.Extension()>
  Public Function RegexReplace(input As String, pattern As String,
                               replacement As String) As String
    Return Regex.Replace(input, pattern, replacement)
  End Function
End Module

……省略……

' 置換の繰り返しの例:「猫」を「子犬」に変え、「【」と「】」を削る
' String.Replaceならメソッドチェーンで書ける
WriteLine("吾輩は【猫】である。吾輩も【猫】である。" _
          .Replace("猫", "子犬") _
          .Replace("【", String.Empty) _
          .Replace("】", String.Empty)
          )
' 出力:吾輩は子犬である。吾輩も子犬である。

' Regexではチェーンできないので、一時変数への代入が必要
Dim work As String = Regex.Replace("吾輩は【猫】である。吾輩も【猫】である。",
                                   "猫", "子犬")
Dim result As String = Regex.Replace(work, "[【】]", String.Empty)
WriteLine(result)
' 出力:吾輩は子犬である。吾輩も子犬である。

' でも、Regexだって拡張メソッドを書けばメソッドチェーンが可能
WriteLine("吾輩は【猫】である。吾輩も【猫】である。" _
          .RegexReplace("猫", "子犬") _
          .RegexReplace("[【】]", String.Empty)
          )
' 出力:吾輩は子犬である。吾輩も子犬である。

Replace静的メソッドを使いやすくする拡張メソッドとその使用例(上:C#、下:VB)
拡張メソッド「RegexReplace」を作っておくと、このようにメソッドチェーンで書ける。一時変数への代入が不要になるので、コーディングが楽になり、コードも読みやすくなる。
ただし、この置換処理を繰り返し実行すると分かっているときは(例えば繰り返しループ内などでは)、Replace静的メソッドではなく、コンパイルしたRegexオブジェクトのインスタンスメソッドを使うべきである。その場合は、Regexオブジェクトを拡張メソッドの第2引数(呼び出すところでは第1引数)として渡すようにする。
なお、拡張メソッドについては、MSDNの「拡張メソッド (C# プログラミング ガイド)」/「拡張メソッド (Visual Basic)」を参照。

置換パターン:一致した部分文字列全体を表す「$0」/「$&」

 置き換える文字として、見つけた文字列を使いたいことがある。例えば、パターンの前後にかっこを付けたいといった場合だ。

 そのようなときに使えるのが、置換パターン内の置換要素だ。パターンに一致した部分文字列の全体は、「$0」または「$&」という記述で置換文字列に埋め込むことができる(次のコード)。

// 一致したパターンの前後にカギかっこを付ける
WriteLine(Regex.Replace("吾輩は猫である。吾輩は子犬である。",
                        ".+?。", "「$0」"));
// 出力:「吾輩は猫である。」「吾輩は子犬である。」
WriteLine(Regex.Replace("吾輩は猫である。吾輩は子犬である。",
                        ".+?。", "「$&」"));
// 出力:「吾輩は猫である。」「吾輩は子犬である。」

' 一致したパターンの前後にカギかっこを付ける
WriteLine(Regex.Replace("吾輩は猫である。吾輩は子犬である。",
                        ".+?。", "「$0」"))
' 出力:「吾輩は猫である。」「吾輩は子犬である。」
WriteLine(Regex.Replace("吾輩は猫である。吾輩は子犬である。",
                        ".+?。", "「$&」"))
' 出力:「吾輩は猫である。」「吾輩は子犬である。」

置換要素「$0」/「$&」を使う例(上:C#、下:VB)
正規表現「.+?。」は「任意の文字が続き、最初の『。』が現れるまで」という意味なので、「吾輩は猫である。」と「吾輩は子犬である。」の2箇所がマッチする。それぞれが置換パターンの$0(または$&)に代入されるので、ご覧のような置換結果になる。

置換パターン:一致したグループを表す「$N」/「${グループ名}」

 上の例で「$0」が一致したパターンの全体であった。では「$1」以降はというと、正規表現パターンで指定したグループを表すのだ。または、グループに付けた名前を番号の代わりに指定してもよい(次のコード)。

// キャプチャーしたグループを置換パターンで使う
WriteLine(Regex.Replace(
            "www.foo.comとftp.foo.comは置換し、mail.foo.comは置換しない",
            @"(www|ftp)\.foo\.", "$1.bar."));
// 出力:www.bar.comとftp.bar.comは置換し、mail.foo.comは置換しない
WriteLine(Regex.Replace(
            "www.foo.comとftp.foo.comは置換し、mail.foo.comは置換しない",
            @"(?<host>www|ftp)\.foo\.", "${host}.bar."));
// 出力:www.bar.comとftp.bar.comは置換し、mail.foo.comは置換しない

' キャプチャーしたグループを置換パターンで使う
WriteLine(Regex.Replace(
            "www.foo.comとftp.foo.comは置換し、mail.foo.comは置換しない",
            "(www|ftp)\.foo\.", "$1.bar."))
' 出力:www.bar.comとftp.bar.comは置換し、mail.foo.comは置換しない
WriteLine(Regex.Replace(
            "www.foo.comとftp.foo.comは置換し、mail.foo.comは置換しない",
            "(?<host>www|ftp)\.foo\.", "${host}.bar."))
' 出力:www.bar.comとftp.bar.comは置換し、mail.foo.comは置換しない

置換要素「$N」/「${グループ名}」を使う例(上:C#、下:VB)
正規表現「(www|ftp)\.foo\.」は、「www.foo.」と「ftp.foo.」にマッチする(「mail.foo.com」にはマッチしない)。マッチしたときに、最初のグループとして「www」または「ftp」がキャプチャーされる。2つ目の正規表現「(?<host>www|ftp)\.foo\.」は、マッチする範囲は同じだが、最初のグループには「host」という名前が付く。
置換パターンの中では、「$1」または「${host}」として、キャプチャーしたグループの内容を代入している。従って置換文字列は、(置換対象文字列「www.foo.」に対しては)「www.bar.」、または、(「ftp.foo.」に対しては)「ftp.bar.」ということになる。
なお、同じことは「www.foo.」と「ftp.foo.」を2回に分けて置換してもできるし、後述する肯定後読みアサーションを使う方法もある。

正規表現パターン:前方参照「\N」/「\k<名前>」

 キャプチャーしたグループを参照することは、正規表現パターン内でも前方参照構成体としてサポートされている。同じ文字列が複数回現れることを表現できるのだ。例えば次のコードのようにすると、連続した文字を1文字に変換できる。

// 正規表現パターン内の前方参照「\N」/「\k<名前>」
WriteLine(Regex.Replace("吾輩は猫猫猫である。", @"(.)\1+", "$1"));
// 出力:吾輩は猫である。
WriteLine(Regex.Replace("吾輩は猫猫猫である。", @"(?<c>.)\k<c>+", "${c}"));
// 出力:吾輩は猫である。

' 正規表現パターン内の前方参照「\N」/「\k<名前>」
WriteLine(Regex.Replace("吾輩は猫猫猫である。", "(.)\1+", "$1"))
' 出力:吾輩は猫である。
WriteLine(Regex.Replace("吾輩は猫猫猫である。", "(?<c>.)\k<c>+", "${c}"))
' 出力:吾輩は猫である。

前方参照構成体「\N」/「\k<名前>」を使う例(上:C#、下:VB)
正規表現「(.)\1+」は、「任意の一文字があり、それと同じ文字が1文字以上続く」という意味だ。最初の1文字(=「(.)」の部分)として「猫」がマッチした場合、この正規表現は「(猫)猫+」(「猫」が2文字以上続く)という意味になる。従って、置換対象文字列の中の「猫猫猫」の3文字が置換対象になり、その最初の文字「猫」が最初のグループとしてキャプチャーされる。置換パターンは「$1」あるいは「${c}」、すなわちキャプチャーした最初のグループ(グループ名は「c」)「猫」になる。

正規表現パターン:肯定アサーション「(?<=)」/「(?=)」

 前回は否定先読みアサーションを紹介した。ここでは、その真偽値が反対になる肯定先読みアサーション「(?=)」と、さらに探索方向が逆になる肯定後読みアサーション「(?<=)」を解説する。

 例題として、「(吾輩|私)は○○である」の「○○」だけを「人間」に置き換えるという処理を考えてみよう。

 これは、置換パターンで$Nを使っても実現できる(次のコード)。

// 「(吾輩|私)は○○である」の「○○」だけを置き換える
WriteLine(Regex.Replace("吾輩は猫である。吾輩は子犬である。私は鳥である。",
                        "(吾輩は|私は).+?(である)", "$1人間$2"));
// 出力:吾輩は人間である。吾輩は人間である。私は人間である。

' 「(吾輩|私)は○○である」の「○○」だけを置き換える
WriteLine(Regex.Replace("吾輩は猫である。吾輩は子犬である。私は鳥である。",
                        "(吾輩は|私は).+?(である)", "$1人間$2"))
' 出力:吾輩は人間である。吾輩は人間である。私は人間である。

置換パターンで実現した例(上:C#、下:VB)
この書き方でも実用上の問題はない。ただ、「○○の部分だけを置き換える」という意図はちょっと分かりづらい。

 上の書き方で気になるのは、コードを読むときに「置換パターンの$1/$2に入るのは何か?」と考えなければならないところだ。処理の目的は「○○」を「人間」に置き換えるだけであるから、置換パターン$1/$2に何が入るかを考えるのは余計なことなのだ。

 そこで、「○○」の部分だけをマッチングできないかと考えてみる。前方に「吾輩は|私は」があり、後方に「である」があるところが置換したい「○○」の部分だ。「○○」の部分のマッチングを計算している時点で考えると、そこから振り返って見れば「吾輩は|私は」があるはずだし(肯定後読みアサーション=positive lookbehind assertion)、そこから先を見れば「である」があるはずだ(肯定先読みアサーション=positive lookahead assertion)。「後読み」といわれてもちょっと分からないかもしれないが、「look behind」(振り返って見る)ことである。

 肯定後読みアサーション「(?<=)」と肯定先読みアサーション「(?=)」で囲めば、「○○」の部分だけをマッチさせられる。そのことをまずMatchesメソッドを使って確認してみよう(次のコード)。

var results = Regex.Matches("吾輩は猫である。吾輩は子犬である。私は鳥である。",
                            "(?<=吾輩は|私は).+?(?=である)");
foreach (Match m in results)
  WriteLine(m);
// 出力:
// 猫
// 子犬
// 鳥

Dim results = Regex.Matches("吾輩は猫である。吾輩は子犬である。私は鳥である。",
                            "(?<=吾輩は|私は).+?(?=である)")
For Each m In results
  WriteLine(m)
Next
' 出力:
' 猫
' 子犬
' 鳥

置換したい「○○」の部分だけを肯定アサーションでマッチさせる例(上:C#、下:VB)
置換するときの正規表現パターンが難しいときは、このようにしてMatchesメソッド(あるいはMatchメソッド)を使って、正規表現パターンが正しく書けていることをまず確認するとよい。

 肯定アサーションで囲めば置換したい「○○」の部分だけをマッチングできると分かったので、後はその部分をReplace静的メソッドで「人間」に置換するだけだ(次のコード)。

// 「(吾輩|私)は○○である」の「○○」だけを置き換える
WriteLine(Regex.Replace("吾輩は猫である。吾輩は子犬である。私は鳥である。",
                        "(?<=吾輩は|私は).+?(?=である)", "人間"));
// 出力:吾輩は人間である。吾輩は人間である。私は人間である。

' 「(吾輩|私)は○○である」の「○○」だけを置き換える
WriteLine(Regex.Replace("吾輩は猫である。吾輩は子犬である。私は鳥である。",
                        "(?<=吾輩は|私は).+?(?=である)", "人間"))
' 出力:吾輩は人間である。吾輩は人間である。私は人間である。

肯定アサーションで実現した例(上:C#、下:VB)
このように書けば、「○○」の部分だけを「人間」に置き換えるという意図がストレートに伝わる(ただし、肯定アサーションを知っていれば、である)。

まとめ

 正規表現を使って文字列中のパターンに一致する部分文字列を置換するには、RegexクラスのReplace静的メソッドを使えばよい(同じ処理を繰り返し実行すると分かっているなら、前々回のIsMatchメソッドと同様にコンパイルオプション付きでRegexクラスのインスタンスを作り、Replaceインスタンスメソッドを使う)。

 マッチさせる正規表現パターンが複雑なときは、まずMatchesメソッド(あるいはMatchメソッド)を使って先に正規表現パターンの正しさを確認するとよい。

 また、本稿では高度な正規表現の一部も簡単に紹介した。詳しくはMSDNの「.NET Framework の正規表現」をご覧いただきたい。

利用可能バージョン:.NET Framework 1.1以降(サンプルコードにはそれ以降の構文も含む)
カテゴリ:クラス・ライブラリ 処理対象:文字列
使用ライブラリ:Regexクラス(System.Text.RegularExpressions名前空間)
使用ライブラリ:Matchクラス(System.Text.RegularExpressions名前空間)
使用ライブラリ:MatchCollectionクラス(System.Text.RegularExpressions名前空間)
関連TIPS:正規表現を使ってパターンに一致する全ての文字列を抽出するには?[C#/VB]
関連TIPS:正規表現を使って文字列がパターンに一致するか調べるには?[C#/VB]
関連TIPS:構文:クラス名を書かずに静的メソッドを呼び出すには?[C# 6.0]
関連TIPS:VB.NETでクラス名を省略してメソッドや定数を利用するには?
関連TIPS:Visual Studioでコンソール・アプリケーションのデバッグ実行時にコマンド・プロンプトを閉じないようにするには?
関連TIPS:正規表現を使って部分文字列を取得するには?[C#、VB]
関連TIPS:正規表現のパターン内にコメント文を記述するには?[C#、VB]
関連TIPS:正規表現を使って文字列から部分文字列を取り除くには?[C#、VB]


「.NET TIPS」のインデックス

.NET TIPS

Copyright© 1999-2017 Digital Advantage Corp. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

Focus

- PR -

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。