C#の正規表現を良く使うのだが、メソッドが多くて良く混乱する。
主な物は以下の通り。
IsMatch(String) | Regex コンストラクターで指定された正規表現に一致する箇所が、指定した入力文字列内に見つかるかどうかを示します。 |
Match(String) | 指定した入力文字列内で、Regex コンストラクターで指定された正規表現と最初に一致する対象を 1 つ検索します。 |
Matches(String) | 指定した入力文字列内で、正規表現と一致する対象をすべて検索します。 |
Replace(String, String) | 指定した入力文字列内で正規表現パターンに一致するすべての文字列を、指定した置換文字列に置換します。 |
Split(String) | Regex コンストラクターで指定された正規表現パターンに定義されている位置で、指定した入力文字列を分割し、部分文字列の配列に格納します。 |
引用元 https://msdn.microsoft.com/ja-jp/library/system.text.regularexpressions.regex(v=vs.110).aspx
この表に掲載したのは上記MSDNに掲載されているものの一部であるが、同じメソッドでも引数の数が異なるものが数種類あるので、ますますややこしい。
この記事では、XML形式のデータから特定のデータを取り出す処理を、Match() と Matches() を使ってやってみた。
どちらでも同じ結果が得られたが、ワテの場合には後者の Matches() のほうが好み。
その辺りの理由も説明したい。
C#でMatch・Matchesを使う
正規表現パターンにマッチした部分文字列を取り出すメソッドには、
- Match()
- Matches()
と言う二種類があるのだが、今でもよく混乱する。
Match関数
上表で太字で書いているように単数形のMatch()場合には、
「指定した入力文字列内で、正規表現と最初に一致する対象を 1 つ検索」
と言う事だ。なので二番目以降を取り出したい場合は、引き続き自分でループ処理をする必要がある。
Matches関数
一方、複数形のMatches()を使うと
「指定した入力文字列内で、正規表現と一致する対象をすべて検索」
出来るので一回で全部の結果が取り出せる。
ただし、取り出した後でデータを順番に処理したい場合には、ループ処理が必要になる。
Match()とMatches()のどちらを使っても、取り出せる情報は同じなので、ワテの場合には最近は専ら後者のMatches()を使うようにしている。一発で全部取り出せるからだ。
もう少し詳しく言うと、
マッチした一つ目の結果だけで良い場合にはMatch()を使っている(こう言う状況はあまり多く無い)。
マッチした全ての結果を使いたい場合にはMatches()を使っている。
と言う感じかな。
しかしながら、Matches()のみを使っていてもその使い方や、取得した結果から目的のデータを取り出す操作で、毎回、過去のコードを流用している。
ややこしくて文法が覚えられないので、過去コードを流用するのが手っ取り早いからだ。
今回は、ワテの備忘録としてMatch()やMatches()の使い方をまとめてみた。
あくまでワテ流なので、もっとスマートは方法がありましたら教えて下さい。
C#でMatch・Matchesを使うサンプル
例えば、こんなxmlが有った場合に、特定のタグで囲まれた文字列のみ取り出したいという状況は時々ある。
<Item> <ASIN>0131856340</ASIN> <BrowseNodes> <BrowseNode> <BrowseNodeId>11232</BrowseNodeId> <Name> Social Sciences</Name> <Ancestors> <BrowseNode> <BrowseNodeId>53</BrowseNodeId> <Name>Nonfiction</Name> <Ancestors> <BrowseNode> <BrowseNodeId>1000</BrowseNodeId> <Name>Subjects</Name> <Ancestors> <BrowseNode> <BrowseNodeId>283155</BrowseNodeId> <Name>Books</Name> </BrowseNode> </Ancestors> </BrowseNode> </Ancestors> </BrowseNode> </Ancestors> <Children> <BrowseNode> <BrowseNodeId>11233</BrowseNodeId> <Name>Anthropology</Name> </BrowseNode> <BrowseNode> <BrowseNodeId>11242</BrowseNodeId> <Name>Archaeology</Name> </BrowseNode> <BrowseNode> <BrowseNodeId>3048861</BrowseNodeId> <Name>Children's Studies</Name> </BrowseNode> </Children> </BrowseNode> </BrowseNodes> </Item>
引用元 https://images-na.ssl-images-amazon.com/images/G/09/associates/paapi/dg/index.html?FindingBrowseNodes.html
これはAmazon Product Advertising APIから取得したBrowseNodeInfo レスポンスグループの例だ。
このXMLデータから、赤と緑のタグで囲まれた文字列をペアで取り出したい。
<BrowseNodeId>11232</BrowseNodeId> <Name> Social Sciences</Name>
つまり、以下のような文字列を取り出したい。
11232 Social Sciences
上記の例ではこのペアが7組あるので、それらを全部取得したい。
そういう場合にはXMLのパーサーとかLINQ to XMLを使う手もあるが、ここでは正規表現で処理してみる。
Match()でXMLを処理する場合
即席で作ってみた。
public static void MatchTest() { var pathNamePJ = Directory.GetParent(Directory.GetCurrentDirectory()).Parent.FullName; var pathFNameXml = Path.Combine(pathNamePJ, "XMLFile_Amazon.xml"); var xmlStr = File.ReadAllText(pathFNameXml); var rgx = new Regex(@"<BrowseNodeId>([0-9]+)</BrowseNodeId>\s*<Name>(.*?)</Name>"); var match = rgx.Match(xmlStr); int i = 0; while (match.Success) { var groups_i = match.Groups; for (int j = 0; j < groups_i.Count; j++) { var group_i_j = groups_i[j].Value; Console.WriteLine("matches[{0}].Groups[{1}].Value= ┫{2}┣", i, j, group_i_j); } Console.WriteLine("---------------------------------------------------"); match = match.NextMatch(); // 次に一致する対象を検索 i++; } }
実行結果は以下の通り。
matches[0].Groups[0].Value= ┫<BrowseNodeId>11232</BrowseNodeId> <Name> Social Sciences</Name>┣ matches[0].Groups[1].Value= ┫11232┣ matches[0].Groups[2].Value= ┫ Social Sciences┣ --------------------------------------------------- matches[1].Groups[0].Value= ┫<BrowseNodeId>53</BrowseNodeId> <Name>Nonfiction</Name>┣ matches[1].Groups[1].Value= ┫53┣ matches[1].Groups[2].Value= ┫Nonfiction┣ --------------------------------------------------- matches[2].Groups[0].Value= ┫<BrowseNodeId>1000</BrowseNodeId> <Name>Subjects</Name>┣ matches[2].Groups[1].Value= ┫1000┣ matches[2].Groups[2].Value= ┫Subjects┣ --------------------------------------------------- matches[3].Groups[0].Value= ┫<BrowseNodeId>283155</BrowseNodeId> <Name>Books</Name>┣ matches[3].Groups[1].Value= ┫283155┣ matches[3].Groups[2].Value= ┫Books┣ --------------------------------------------------- matches[4].Groups[0].Value= ┫<BrowseNodeId>11233</BrowseNodeId> <Name>Anthropology</Name>┣ matches[4].Groups[1].Value= ┫11233┣ matches[4].Groups[2].Value= ┫Anthropology┣ --------------------------------------------------- matches[5].Groups[0].Value= ┫<BrowseNodeId>11242</BrowseNodeId> <Name>Archaeology</Name>┣ matches[5].Groups[1].Value= ┫11242┣ matches[5].Groups[2].Value= ┫Archaeology┣ --------------------------------------------------- matches[6].Groups[0].Value= ┫<BrowseNodeId>3048861</BrowseNodeId> <Name>Children's Studies</Name>┣ matches[6].Groups[1].Value= ┫3048861┣ matches[6].Groups[2].Value= ┫Children's Studies┣ --------------------------------------------------- 続行するには何かキーを押してください . . .
マッチしたグループの中身は以下の通り。
Group[0] マッチした全体の文字列
Group[1] キャプチャした1番目の文字列 ([0-9]+)
Group[2] キャプチャした2番目の文字列 (.*?)
と言う事で、Group[1]とGroup[2]が目的の文字列となる。
正規表現の最小マッチを使う
ここで用いた正規表現パターンは以下の通り。
var rgx = new Regex(@"<BrowseNodeId>([0-9]+)</BrowseNodeId>\s*<Name>(.*?)</Name>");
@マークはC#で文字列を扱う場合に文字列の先頭にこのアットマークを付けておくと\記号をエスケープしなくても良い。
もし@を付けない場合には\\のように円記号を二個連続で書く必要がある。
なので、ファイルのパス名を記述したり、正規表現パターンを記述したりする場合に@を使うと便利だ。と言ってもこの例では円記号が無いので@は無くても良いが。
で、正規表現の文字列を分解すると以下の通り。
比較的シンプルなパターンであるが念のために説明すると、
<BrowseNodeId>
([0-9]+) 数字が1個以上連続する
</BrowseNodeId>
\s* 空白、タブが0個以上連続する
<Name>
(.*?) 任意の文字 . が0個以上連続する。最短マッチ記号の?付き
</Name>
こんなふうになる。
肝心な点は、最小マッチ記号?を付けた事だ。
もしこのクエスチョンマーク記号を付けないと、
<Name>(.*)</Name>
となるが、そうすると以下のように、最初の<Name>と、最末尾の</Name>でマッチしてしまい、
<Item> <ASIN>0131856340</ASIN> <BrowseNodes> <BrowseNode> <BrowseNodeId>11232</BrowseNodeId> <Name> ・・・ ・・・ ・・・ Children's Studies</Name> </BrowseNode> </Children> </BrowseNode> </BrowseNodes> </Item>
目的の動作をしなくなってしまう。
なので、xmlやhtmlなどのタグを正規表現で処理する場合には、この最短マッチ?がとっても役に立つ。?をキャプチャの()の外に出して、
<Name>(.*)?</Name>
と書いても良いと思う。
いやあ、それにしても正規表現を考えた人は、一体全体どんなに頭がいいんだろうと思う。
さて、次にMatches()の場合をやってみる。
Matches()でXMLを処理する場合
同じく即席で作ってみた。
public static void MatchesTest() { //現在のVisual Studio Projectのトップディレクトリを取得 var pathNamePJ = Directory.GetParent(Directory.GetCurrentDirectory()).Parent.FullName; var pathFNameXml = Path.Combine(pathNamePJ, "XMLFile1b_AmazonASIN.xml"); var xmlStr = File.ReadAllText(pathFNameXml); var rgx = new Regex(@"<BrowseNodeId>([0-9]+)</BrowseNodeId>\s*<Name>(.*?)</Name>"); var matches = rgx.Matches(xmlStr); for (int i = 0; i < matches.Count; i++) { var group_i = matches[i].Groups; for (int j = 0; j < group_i.Count; j++) { var group_i_j = group_i[j].Value; Console.WriteLine("matches[{0}].Groups[{1}].Value= ┫{2}┣", i, j, group_i_j); } Console.WriteLine("---------------------------------------------------"); } }
Match()の場合とほとんど同じで、違うのはマッチ結果を取り出すforループの中の記述のみだ。
実行結果は以下の通り。
matches[0].Groups[0].Value= ┫<BrowseNodeId>11232</BrowseNodeId> <Name> Social Sciences</Name>┣ matches[0].Groups[1].Value= ┫11232┣ matches[0].Groups[2].Value= ┫ Social Sciences┣ --------------------------------------------------- matches[1].Groups[0].Value= ┫<BrowseNodeId>53</BrowseNodeId> <Name>Nonfiction</Name>┣ matches[1].Groups[1].Value= ┫53┣ matches[1].Groups[2].Value= ┫Nonfiction┣ --------------------------------------------------- matches[2].Groups[0].Value= ┫<BrowseNodeId>1000</BrowseNodeId> <Name>Subjects</Name>┣ matches[2].Groups[1].Value= ┫1000┣ matches[2].Groups[2].Value= ┫Subjects┣ --------------------------------------------------- matches[3].Groups[0].Value= ┫<BrowseNodeId>283155</BrowseNodeId> <Name>Books</Name>┣ matches[3].Groups[1].Value= ┫283155┣ matches[3].Groups[2].Value= ┫Books┣ --------------------------------------------------- matches[4].Groups[0].Value= ┫<BrowseNodeId>11233</BrowseNodeId> <Name>Anthropology</Name>┣ matches[4].Groups[1].Value= ┫11233┣ matches[4].Groups[2].Value= ┫Anthropology┣ --------------------------------------------------- matches[5].Groups[0].Value= ┫<BrowseNodeId>11242</BrowseNodeId> <Name>Archaeology</Name>┣ matches[5].Groups[1].Value= ┫11242┣ matches[5].Groups[2].Value= ┫Archaeology┣ --------------------------------------------------- matches[6].Groups[0].Value= ┫<BrowseNodeId>3048861</BrowseNodeId> <Name>Children's Studies</Name>┣ matches[6].Groups[1].Value= ┫3048861┣ matches[6].Groups[2].Value= ┫Children's Studies┣ --------------------------------------------------- 続行するには何かキーを押してください . . .
最初に実行したTestMatch()の場合と同じ結果が得られた。
Groupの中のCapturesがややこしい
これで良いのだが、ややこしいのは上の例では使わなかったのだが、MatchやMatchesでキャプチャするとGroupの中にさらに
Captures
というメンバがあり、その中にも取得した文字列が入っているのだ。
Matchesの例を下に示す。
やっている事は今までの例と同じだが、forループを使わずに取得したデータを取り出しているだけだ。
public static void DumpMatchesItem() { var pathNamePJ = Directory.GetParent(Directory.GetCurrentDirectory()).Parent.FullName; var pathFNameXml = Path.Combine(pathNamePJ, "XMLFile1b_AmazonASIN.xml"); var xmlStr = File.ReadAllText(pathFNameXml); var rgx = new Regex(@"<BrowseNodeId>([0-9]+)</BrowseNodeId>\s*<Name>(.*?)</Name>"); var matches = rgx.Matches(xmlStr); var a0 = matches.Count; // =7 全部で七個のペアが見付かった。 var a1 = matches[0].Value; // var a2 = matches[0].Captures.Count; // =1 var a3 = matches[0].Captures[0].Value; // var a4 = matches[0].Groups.Count; // =3 グループの要素は3個 var a5 = matches[0].Groups[0].Value; // 最初のグループ(j=0) var a6 = matches[0].Groups[0].Captures.Count; // =1 var a7 = matches[0].Groups[0].Captures[0].Value; // var a8 = matches[0].Groups[1].Value; // ="11232" 二番目のグループ(j=1) var a9 = matches[0].Groups[1].Captures.Count; // =1 var aA = matches[0].Groups[1].Captures[0].Value; // ="11232" var aB = matches[0].Groups[2].Value; // =" Social Sciences"三番グループ(j=2) var aC = matches[0].Groups[2].Captures.Count; // =1 var aD = matches[0].Groups[2].Captures[0].Value; // =" Social Sciences" // i j // 以下、i=1,2,...,6まである。 }
この中で、
matches[j].Group[j].Captures[k].Value
のように、三つも配列があるので、ワテの場合、必ずわけワカメ状態になる。
なおこの例では、このCaptures.Countはどれも1で、Captures[0].Valueの値はGroup[j].Valueと一致している。
でも、正規表現のパターンによっては、このCaptures.Countは1より大きな値になり、Capturesに複数のデータが入る場合がある。
もう訳分からんくらいややこしくなるので、このあたりがワテがこのMatchやMatchesの使い方を覚えられない理由だ。
と言う事で、ワテの場合には、比較的シンプルな正規表現パターンを書くようにしている。
そうすると上記の例のようにCaptures.Value=1となり、分かり易いからだ。
さて、AKBを聴くかな。
深い意味は無い。
まとめ
以上、ワテが苦手とするC#の正規表現のMatch, Matchesメソッドについてまとめてみた。
- ワテの場合、Matches()のみを使うようにしている。理由は一回実行すると正規表現パターンにマッチするデータが全部取得出来るから分かり易いので。
- Match()や Matches()でキャプチャした場合には、Group[0]はスキップしてGroup[1]以降に個々のキャプチャデータが入っていると覚えておけば良いのかな。
- Group.Capturesが配列になってさらにデータが格納されるような複雑なパターンはあまり書かないようにしている。
ここで紹介した関数は即席で作った関数なので、間違っている部分などありましたらご指摘いただけると有り難いです。
C#関連本を読む
こういう本格的な解説本が理解出来れば完璧だ。
あるいは、こういうお手軽な本を一冊持っておくと便利かも。
コンパクトで手軽に利用できるお勧めの一冊だ。
コメント