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

.NET TIPS:正規表現を使ってパターンに一致する全ての文字列を抽出するには?[C#/VB]

RegexクラスのMatchesメソッドを使い、ある文字列に含まれている特定のパターンを抽出する方法と、マッチする範囲を限定していく方法を解説する。

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

連載目次

 ある文字列から、正規表現パターンに一致する全ての部分文字列を取り出すには、Regexクラス(System.Text.RegularExpressions名前空間)のMatchesメソッドかMatchメソッドを利用する。本稿では、Matchesメソッドの使い方を解説するとともに、ちょっと高度な正規表現の書き方も紹介する。

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

正規表現を使ってパターンに一致する全ての文字列を抽出するには?

 RegexクラスのMatches静的メソッドを使えばよい。パターンに一致する全ての文字列を抽出するには、foreachループ(C#)/For Eachループ(VB)を使って列挙する(次のコード)。

using System.Text.RegularExpressions;
……省略……
MatchCollection results
  = Regex.Matches("{検査対象文字列}", "{正規表現パターン}");
// 全てを列挙する
foreach (Match m in results) // Matchと型を明示(varは不可)
{
  int index = m.Index; // 発見した文字列の開始位置
  string value = m.Value; // 発見した文字列
}
// 最初の1つだけが欲しいとき
var first = results[0]; // これはvarでよい
int firstIndex = first.Index;
string firstString = first.Value;

Imports System.Text.RegularExpressions
……省略……
Dim results As MatchCollection _
  = Regex.Matches("{検査対象文字列}", "{正規表現パターン}")
' 全てを列挙する
For Each m As Match In results ' Matchと型を明示(Option Strict On時)
  Dim index As Integer = m.Index ' 発見した文字列の開始位置
  Dim value As String = m.Value ' 発見した文字列
Next
' 最初の1つだけが欲しいとき
Dim first = results(0) ' これはDimだけでよい
Dim firstIndex As Integer = first.Index
Dim firstString As String = first.Value

Matches静的メソッドの使い方(上:C#、下:VB)
第1引数に対象の文字列を与え、第2引数には正規表現パターンを表す文字列を与える。
パターンと一致する文字列がMatchCollectionコレクション(System.Text.RegularExpressions名前空間)に格納されて返される。MatchCollectionコレクションに格納されているものは、Matchオブジェクト(System.Text.RegularExpressions名前空間)であり、そのIndexプロパティには発見した文字列の開始位置(ゼロ始まりの整数)が、Valueプロパティには発見した文字列が入っている。
なお、RegexクラスはジェネリックやLINQなどが登場する以前からあるものなので、MatchCollectionコレクションを列挙するとobject型が返される。そのため、foreachループを書くときに、ループ変数の型をMatchと明示しなければならない(VBではOption StrictをOnにしている場合)。

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

実際の例

 実際の例として、「MatchもMatchesもパターンにマッチした文字列を取り出す」という文字列から「Match」または「マッチ」という文字列を取り出してコンソールに表示してみよう。次のコードのようになる。

 ここで注目しておいてほしいのは、7文字目から始まる「Matches」の中の「Match」もマッチしているところだ。

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

class Program
{
  static void Main(string[] args)
  {
    string sentence = "MatchもMatchesもパターンにマッチした文字列を取り出す";
    MatchCollection results = Regex.Matches(sentence, "Match|マッチ");
    foreach (Match m in results)
    {
      int index = m.Index; // 発見した文字列の開始位置
      string value = m.Value; // 発見した文字列
      WriteLine($"{index + 1}文字目から:{value}");
      // 出力:
      // 1文字目から:Match
      // 7文字目から:Match
      // 20文字目から:マッチ
    }
#if DEBUG
    ReadKey();
#endif
  }
}

Imports System.Text.RegularExpressions
Imports System.Console

Module Module1

  Sub Main()
    Dim sentence As String _
      = "MatchもMatchesもパターンにマッチした文字列を取り出す"
    Dim results As MatchCollection _
      = Regex.Matches(sentence, "Match|マッチ")
    For Each m As Match In results 
      Dim index As Integer = m.Index ' 発見した文字列の開始位置
      Dim value As String = m.Value ' 発見した文字列
      WriteLine($"{index + 1}文字目から:{value}")
      ' 出力:
      ' 1文字目から:Match
      ' 7文字目から:Match
      ' 20文字目から:マッチ
    Next

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

Matches静的メソッドを使う例(上:C#、下:VB)
C#コードの冒頭にある「using static System.Console;」という書き方は、Visual Studio 2015からのものだ。詳しくは、「.NET TIPS:構文:クラス名を書かずに静的メソッドを呼び出すには?[C# 6.0]」をご覧いただきたい。同様な機能がVBには以前から備わっており、「.NET TIPS:VB.NETでクラス名を省略してメソッドや定数を利用するには?」で解説している。
また、「Main」メソッド末尾にReadKeyメソッドを置く意味は、「.NET TIPS:Visual Studioでコンソール・アプリケーションのデバッグ実行時にコマンド・プロンプトを閉じないようにするには?」をご覧いただきたい。
WriteLineメソッドの引数に与えている先頭に「$」記号が付いた文字列については、「.NET TIPS:数値を右詰めや0埋めで文字列化するには?[C#、VB]」の後半(「C# 6.0/VB 14で追加された補間文字列機能を使用する」)を見てほしい。

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

 なお、Match静的メソッドを使っても、上と同じ結果が得られる(次のコード)。Matchクラスには、上で示したIndexプロパティとValueプロパティだけでなく、発見した文字列をこのように次々と列挙していくなどといった、さまざまな機能が盛り込まれているのだ。しかし、こちらのコードは上で示したコードよりもちょっと読みにくいように思う。

string sentence = "MatchもMatchesもパターンにマッチした文字列を取り出す";
Match m = Regex.Match(sentence, "Match|マッチ");
while (m.Success)
{
  int index = m.Index; // 発見した文字列の開始位置
  string value = m.Value; // 発見した文字列
  WriteLine($"{index + 1}文字目から:{value}");
  // 出力:
  // 1文字目から:Match
  // 7文字目から:Match
  // 20文字目から:マッチ

  m = m.NextMatch();
}

Dim sentence As String _
  = "MatchもMatchesもパターンにマッチした文字列を取り出す"
Dim m As Match = Regex.Match(sentence, "Match|マッチ")
While m.Success
  Dim index As Integer = m.Index ' 発見した文字列の開始位置
  Dim value As String = m.Value ' 発見した文字列
  WriteLine($"{index + 1}文字目から:{value}")
  ' 出力:
  ' 1文字目から:Match
  ' 7文字目から:Match
  ' 20文字目から:マッチ

  m = m.NextMatch()
End While

Match静的メソッドでも同じ結果が得られる(上:C#、下:VB)
Matchメソッドは、最初に見つかった部分のMatchオブジェクトを返してくる。MatchオブジェクトのNextMatchメソッドを呼びだすと、その次に見つかったMatchオブジェクトが返る。ただし、見つかった文字列がないときは、返されたMatchオブジェクトのSuccessプロパティがfalseになっている。

Matchesで抽出した結果をLINQにつなげるには?

 マッチした全ての文字列をforeach/For Eachで列挙できるのであれば、どうせならLINQにつなげたいと思う。ところが、RegexクラスはLINQ登場以前に設計されたため、例えばIEnumerable<Match>といったようなジェネリックコレクションを返すメソッドは持っていないのだ。そのため、MatchesメソッドにLINQ拡張メソッドをつなげられないのである。

 そんなときは、次のコードのような拡張メソッドを作るとよい。

using System.Collections.Generic;
using System.Text.RegularExpressions;

public static class RegexExtension
{
  public static IEnumerable<Match> AsEnumerable(this MatchCollection mc)
  {
    foreach (Match m in mc)
      yield return m;
  }
}

Imports System.Text.RegularExpressions

Public Module RegexExtension
  <System.Runtime.CompilerServices.Extension()>
  Public Iterator Function AsEnumerable(mc As MatchCollection) _
      As IEnumerable(Of Match)
    For Each m As Match In mc
      Yield m
    Next
  End Function
End Module

Matchesメソッドを使いやすくする拡張メソッドの例(上:C#、下:VB)
拡張メソッドについては、MSDNの「拡張メソッド (C# プログラミング ガイド)」/「拡張メソッド (Visual Basic)」を参照。また、VBの「Iterator Function」宣言と Yield ステートメントについては、MSDNの「反復子 (Visual Basic)」を参照(Visual Studio 2012からの機能)。

 このAsEnumerable拡張メソッドを使うと、例えばマッチした文字列を小文字に変換するコードは次のようにLINQのSelect拡張メソッドを使って書ける。

using System.Collections.Generic;
using System.Linq;
……省略……
string sentence = "MatchもMatchesもパターンにマッチした文字列を取り出す";
MatchCollection results = Regex.Matches(sentence, "Match|マッチ");
foreach (var s in results.AsEnumerable()
                          .Select(m => m.Value.ToLower()))
  WriteLine(s);
  // 出力:
  // match
  // match
  // マッチ

Dim sentence As String _
  = "MatchもMatchesもパターンにマッチした文字列を取り出す"
Dim results As MatchCollection _
  = Regex.Matches(sentence, "Match|マッチ")
For Each s In results.AsEnumerable() _
                     .Select(Function(m) m.Value.ToLower())
  WriteLine(s)
  ' 出力:
  ' match
  ' match
  ' マッチ
Next

拡張メソッドの使用例(上:C#、下:VB)
LINQのSelect拡張メソッドについては「.NET TIPS:LINQ:コレクション内のオブジェクトが持つ数値を集計するには?[C#、VB]」を参照。

英単語の完全一致を指定できる否定先読みアサーション「(?!)」

 さて、ここまでの例では、「Match」にマッチする文字列として「Matches」の中の「Match」まで抽出されてしまっていた。「Matches」の中の「Match」にはマッチしないようにするにはどうしたらよいだろうか?

 正規表現パターンの中で否定先読みアサーション(?!)」を使うと、その後に続く文字が「(?!)」の中のパターンと一致したら比較を打ち切る(マッチしなかったとする)。例えば、「(?![a-zA-Z])」と書くと、そこにアルファベットがあったらマッチしないのである。「Match(?![a-zA-Z])」とすれば、文字列中の「Match」の後ろにアルファベットがあったらマッチしないことになる。

 「Match」は抽出したいが「Matches」は含まれてほしくないというとき、英文であればワード境界アンカー「\b」を使えばよい。しかし、英単語と日本語の間に空白を置かない日本語の文章では、「\b」は役に立たないのだ。そこで否定先読みアサーションを使って、英単語の終わりを判断させるのである(次のコード)。

string sentence = "MatchもMatchesもパターンにマッチした文字列を取り出す";
foreach (var m in Regex.Matches(sentence, "Match(?![a-zA-Z])|マッチ"))
  WriteLine(m);
  // 出力:
  // Match
  // マッチ

Dim sentence As String _
  = "MatchもMatchesもパターンにマッチした文字列を取り出す"
For Each m In Regex.Matches(sentence, "Match(?![a-zA-Z])|マッチ")
  WriteLine(m)
  ' 出力:
  ' Match
  ' マッチ
Next

英単語の完全一致を指定する例(上:C#、下:VB)
否定先読みアサーション「(?!)」を使うと、指定したパターン(ここでは「[a-zA-Z]」=英字)がその場所にあったらマッチング失敗となる。この例では、「Matches」の5文字目「h」を検査しているときに、1文字先の「e」を先読みして、マッチしないと判断される。
否定先読みアサーションに指定したパターンは、抽出される文字列には含まれない。
なお、逆の意味になる肯定先読みアサーション「(?=)」もある。

最短一致を指定する「?」

 ところで次のような例を考えてみよう。「吾輩は猫である。吾輩は犬である。」という文字列から、「吾輩は〜である」というパターンを抽出したい。一目瞭然、そういうパターンが2つ並んでいるのだが……。次のコードのような正規表現では、うまくいかないのだ。

string sentence = "吾輩は猫である。吾輩は犬である。";
foreach (var m in Regex.Matches(sentence, "吾輩は.+である"))
  WriteLine(m);
  // 出力:
  // 吾輩は猫である。吾輩は犬である

Dim sentence As String = "吾輩は猫である。吾輩は犬である。"
For Each m In Regex.Matches(sentence, "吾輩は.+である")
  WriteLine(m)
  ' 出力:
  ' 吾輩は猫である。吾輩は犬である
Next

想定外に長い範囲を抽出してしまう正規表現の例(上:C#、下:VB)
「"吾輩は.+である"」という正規表現で、「吾輩は猫である」と「吾輩は犬である」の2つにマッチするように思える。しかし結果は、「吾輩は猫である。吾輩は犬である」という長い文字列が抽出されてしまった。

 この失敗は、正規表現のパターン一致はなるべく長くなるように検索されることによるものだ(最長一致)。HTMLやXMLのタグを解析するときに、よくやってしまいがちである。想定通りの結果を得るには、パターンに一致したなるべく短い文字列を返してもらうように指定する(最短一致)。

 最短一致にするには、量指定子「+」/「*」の後ろに「?」を付ける。そうすれば次のコードのようにうまくいく。

string sentence = "吾輩は猫である。吾輩は犬である。";
foreach (var m in Regex.Matches(sentence, "吾輩は.+?である"))
  WriteLine(m);
  // 出力:
  // 吾輩は猫である
  // 吾輩は犬である

Dim sentence As String = "吾輩は猫である。吾輩は犬である。"
For Each m In Regex.Matches(sentence, "吾輩は.+?である")
  WriteLine(m)
  ' 出力:
  ' 吾輩は猫である
  ' 吾輩は犬である
Next

最短一致を指定して想定通りの結果が得られる例(上:C#、下:VB)

部分文字列をキャプチャーするグループ化構成体「()」

 正規表現パターンの中でグループ化構成体()」を使うと、マッチした文字列の中の一部分を「グループ」として後から参照できる。

 例えば、「吾輩は猫である。吾輩は小犬である。」という文字列から、「吾輩は〜である」というパターンを抽出し、さらに「〜」に当たる文字列(「猫」と「子犬」)も取り出したいとしよう。「〜」にあたる文字列の文字数が決まっているならばStringクラスのSubstringメソッドなどで切り出す方法もあるかもしれない。この例のように文字数が可変の場合はどうしたらよいだろうか? パターンの中で「〜」に当たる部分をグループ化構成体「()」で囲っておけば、後からその部分をグループとして取り出せるのである(次のコード)。

string sentence = "吾輩は猫である。吾輩は小犬である。";
foreach (Match m in Regex.Matches(sentence, "吾輩は(.+?)である"))
  WriteLine($"{m.Groups[0].Value} - {m.Groups[1].Value}");
  // 出力:
  // 吾輩は猫である - 猫
  // 吾輩は小犬である - 小犬

Dim sentence As String = "吾輩は猫である。吾輩は小犬である。"
For Each m As Match In Regex.Matches(sentence, "吾輩は(.+?)である")
  WriteLine($"{m.Groups(0).Value} - {m.Groups(1).Value}")
  ' 出力:
  ' 吾輩は猫である - 猫
  ' 吾輩は小犬である - 小犬
Next

グループ化構成体を使って、特定の部分を後から取り出す例(上:C#、下:VB)
MatchクラスのGroupsプロパティはインデクサーになっていて、Groups[0]にはマッチした文字列全体が入っている(つまり、MatchクラスのValueプロパティと同じ)。
Groups[1]以降には、グループ化構成体「()」で指定した部分文字列が入っている。
なお、範囲外の添え字を与えてもエラーにはならない(この例でいうとGroups[2]以降)。そのときは、得られたGroupオブジェクトのSuccessプロパティがfalseになっている。

 グループを指定するには、上のような添え字だけでなく、正規表現パターンで名前を付けておいてそれを使うこともできる(次のコード)。

string sentence = "吾輩は猫である。吾輩は小犬である。";
foreach (Match m in Regex.Matches(sentence, "吾輩は(?<種類>.+?)である"))
  WriteLine(m.Groups["種類"].Value);
  // 出力:
  // 猫
  // 小犬

Dim sentence As String = "吾輩は猫である。吾輩は小犬である。"
For Each m As Match In Regex.Matches(sentence, "吾輩は(?<種類>.+?)である")
  WriteLine(m.Groups("種類").Value)
  ' 出力:
  ' 猫
  ' 小犬
Next

グループ化構成体に名前を付けて使う例(上:C#、下:VB)
グループ化構成体の最初のかっこ「(」の直後に「?<名前>」と書いて、グループに名前を付ける。
MatchクラスのGroupsプロパティのインデクサーとして、その名前を指定できる。
正規表現パターン中に複数のグループを書くときは、名前を付けた方が分かりやすくなる。

 前回、「?」は量指定子(0回または1回)およびオプション指定として紹介した。本稿で紹介したように、「?」は先読みアサーションや最短一致指定、あるいはグループ化構成体の名前付けなどにも使われる。他人(あるいは過去の自分)が書いた正規表現は読みづらいものなのだが、このように同じ記号にさまざまな意味があることも一因だろう。

まとめ

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

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

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

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

RSSについて

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

メールマガジン登録

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