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

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

RegexクラスのSplitメソッドを使用して、正規表現にマッチする部分をセパレーターとして、文字列を分割する方法を解説する。

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

連載目次

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

 RegexクラスのSplitメソッドは.NET Frameworkの初期からあるものだが、掲載したサンプルコードの全てを試すにはVisual Studio 2015以降が必要である。

 なお、正規表現の書き方については、以下の.NET TIPSをご覧いただきたい。

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

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

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

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

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

Split静的メソッドの使い方(上:C#、下:VB)
第1引数に分割対象の文字列を与え、第2引数にはセパレーターとなる正規表現パターンを与える。分割した結果は、文字列の配列として返される。

 上に示した書式だけを見ると、Stringクラス(System名前空間)のSplitメソッドと同じように思える。だが、セパレーターの指定に正規表現パターンが使えることで、柔軟で多彩な分割処理が可能になっているのだ。後述するように、CSVデータを項目に分解する処理なども行える。

実際の例

 まずは、正規表現パターンを使わないコンソールアプリの例を示そう(次のコード)。この場合は、StringクラスのSplitメソッドと同様の結果が得られる。コンソールに出力するための「DisplayAll」拡張メソッドをわざわざ作っているのは、以降のサンプルコードでも利用するためである。

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

public static class StringArrayExtension
{
  // 配列中の文字列を「'」で囲み、先頭に番号を付けて表示する
  public static void DisplayAll(
    this System.Collections.Generic.IEnumerable<string> words)
  {
    int count = 0;
    foreach (var s in words)
      WriteLine($"{++count}:'{s}'");
  }
}

class Program
{
  static void Main(string[] args)
  {
    // StringクラスのSplitメソッド
    "吾輩は猫である。吾輩は子犬である。".Split('。')
      .DisplayAll();
    // 出力
    // 1:'吾輩は猫である'
    // 2:'吾輩は子犬である'
    // 3:''

    // RegexクラスのSplit静的メソッド
    Regex.Split("吾輩は猫である。吾輩は子犬である。", "。")
      .DisplayAll();
    // 出力
    // 1:'吾輩は猫である'
    // 2:'吾輩は子犬である'
    // 3:''

#if DEBUG
    ReadKey();
#endif
  }
}

Imports System.Text.RegularExpressions
Imports System.Console

Module StringArrayExtension
  ' 配列中の文字列を「'」で囲み、先頭に番号を付けて表示する
  <System.Runtime.CompilerServices.Extension()>
  Public Sub DisplayAll(words As IEnumerable(Of String))
    Dim count As Integer = 0
    For Each s In words
      count += 1
      WriteLine($"{count}:'{s}'")
    Next
  End Sub
End Module

Module Module1
  Sub Main()
    ' StringクラスのSplitメソッド
    Dim s = "吾輩は猫である。吾輩は子犬である。"
    s.Split("。"c).DisplayAll()
    ' 1:'吾輩は猫である'
    ' 2:'吾輩は子犬である'
    ' 3:''

    ' RegexクラスのSplit静的メソッド
    Regex.Split("吾輩は猫である。吾輩は子犬である。", "。") _
      .DisplayAll()
    ' 1:'吾輩は猫である'
    ' 2:'吾輩は子犬である'
    ' 3:''

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

RegexクラスのSplit静的メソッドを使う例(上:C#、下:VB)
「吾輩は猫である。吾輩は子犬である。」という文字列を、「。」をセパレーターとして分割している。文字列末尾の「。」もセパレーターであるから、その後ろにも空の項目があると判断される(出力の3番目)。
なお、C#コードの冒頭にある「using static System.Console;」という書き方は、Visual Studio 2015からのものだ。詳しくは、「.NET TIPS:構文:クラス名を書かずに静的メソッドを呼び出すには?[C# 6.0]」をご覧いただきたい。同様な機能がVBには以前から備わっており、「.NET TIPS:VB.NETでクラス名を省略してメソッドや定数を利用するには?」で解説している。
また、「Main」メソッド末尾にReadKeyメソッドを置く意味は、「.NET TIPS:Visual Studioでコンソール・アプリケーションのデバッグ実行時にコマンド・プロンプトを閉じないようにするには?」をご覧いただきたい。
拡張メソッドについては、MSDNの「拡張メソッド (C# プログラミング ガイド)」/「拡張メソッド (Visual Basic)」を参照してほしい。

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

セパレーターとして2文字以上を使うには?

 RegexクラスのSplitメソッドでは、正規表現にマッチする文字列がセパレーターになるので、連続した2文字以上の文字列をセパレーターとして使うこともできる。

 例えば、連続して2文字以上続く英数字をセパレーターとして使うには次のコードのようにする(1文字の英数字はセパレーターにしない)。

Regex.Split("吾輩は1匹の猫であるAbc吾輩は1匹の子犬である987",
            "[A-Z0-9][A-Z0-9]+", RegexOptions.IgnoreCase)
  .DisplayAll();
// 出力
// 1:'吾輩は1匹の猫である'
// 2:'吾輩は1匹の子犬である'
// 3:''

Regex.Split("吾輩は1匹の猫であるAbc吾輩は1匹の子犬である987",
            "[A-Z0-9][A-Z0-9]+", RegexOptions.IgnoreCase) _
  .DisplayAll()
' 出力
' 1:'吾輩は1匹の猫である'
' 2:'吾輩は1匹の子犬である'
' 3:''

連続して2文字以上続く英数字をセパレーターとして使う例(上:C#、下:VB)
正規表現「[A-Z0-9][A-Z0-9]+」は、アルファベットの大文字か数字が2文字以上続くという意味だ(「[A-Z0-9]{2,}」でもよい)。さらにオプションとしてRegexOptions.IgnoreCaseを与えているので、アルファベットの小文字も含まれる。
英数字2文字以上の部分がセパレーターとなり、1文字の部分(この例では「1匹」の「1」)では分割されない。

セパレーターも結果に含めるには?

 セパレーターの一部または全部を結果に含めることもできる。それには、正規表現の中で、結果として取り出したい部分をかっこで囲ってグループにする。

 例えば、上の例で「Abc」と「987」がセパレーターになった。その先頭の文字「A」と「9」も分割結果に含めるには、次のコードのようにする。

Regex.Split("吾輩は1匹の猫であるAbc吾輩は1匹の子犬である987",
            "([A-Z0-9])[A-Z0-9]+", RegexOptions.IgnoreCase)
  .DisplayAll();
// 出力
// 1:'吾輩は1匹の猫である'
// 2:'A'
// 3:'吾輩は1匹の子犬である'
// 4:'9'
// 5:''

Regex.Split("吾輩は1匹の猫であるAbc吾輩は1匹の子犬である987",
            "([A-Z0-9])[A-Z0-9]+", RegexOptions.IgnoreCase) _
  .DisplayAll()
' 出力
' 1:'吾輩は1匹の猫である'
' 2:'A'
' 3:'吾輩は1匹の子犬である'
' 4:'9'
' 5:''

セパレーターの一部を分割結果に含める例(上:C#、下:VB)
正規表現「([A-Z0-9])[A-Z0-9]+」の意味は先の例とほぼ同じだが、1文字目をグループに指定している。
グループとしてキャプチャーされたセパレーターの一部が、独立した項目として分割結果に含まれる(複数のグループを作れば、それぞれが別の項目として分割される)。

正規表現パターン:キャプチャーしないグループ化構成体「(?:)」

 キャプチャーされたグループが結果に含まれるのは、意図と異なることもある。例えば、セパレーターとして「である。」と「であるが、」の2つを指定しようとして次のようなコードを書くと、OR条件の範囲を限定するために付けたかっこもグループとしてキャプチャーされてしまう。

Regex.Split("吾輩は猫である。吾輩は子犬であるが、猫ではない。",
            "である(。|が、)")
  .DisplayAll();
// 出力
// 1:'吾輩は猫'
// 2:'。'
// 3:'吾輩は子犬'
// 4:'が、'
// 5:'猫ではない。'

Regex.Split("吾輩は猫である。吾輩は子犬であるが、猫ではない。",
            "である(。|が、)") _
  .DisplayAll()
' 出力
' 1:'吾輩は猫'
' 2:'。'
' 3:'吾輩は子犬'
' 4:'が、'
' 5:'猫ではない。'

セパレーターの一部が意図に反して分割結果に含まれてしまう例(上:C#、下:VB)
セパレーターの正規表現「である(。|が、)」は、「である。」または「であるが、」という意味だ。分割した結果としては、出力例の1番目/3番目/5番目の3つだけがほしいのである。
ところが、OR条件の範囲を限定するためのかっこがグループとしてキャプチャーされてしまい、「。」(2番目)と「が、」(4番目)も出力されてしまった。

 非キャプチャーグループ化構成体?:」は、かっこの中をキャプチャーさせないようにする(次のコード)。

Regex.Split("吾輩は猫である。吾輩は子犬であるが、猫ではない。",
            "である(?:。|が、)")
  .DisplayAll();
// 出力
// 1:'吾輩は猫'
// 2:'吾輩は子犬'
// 3:'猫ではない。'

Regex.Split("吾輩は猫である。吾輩は子犬であるが、猫ではない。",
            "である(?:。|が、)") _
  .DisplayAll()
' 出力
' 1:'吾輩は猫'
' 2:'吾輩は子犬'
' 3:'猫ではない。'

セパレーターのかっこ内を分割結果に含ませない例(上:C#、下:VB)
セパレーターの正規表現「である(?:。|が、)」の意味は先ほどとほぼ同じだが、非キャプチャーグループ指定「?:」により、かっこの中がキャプチャーされない。

CSVデータを分割する例

 最後に、CSVのデータを分割する例を紹介しよう(次のコード)。

 この正規表現の解読は、読者の皆さんへの宿題としたい。なお、これだけでは完璧とはいえないので注意してほしい。例えば、項目を囲む引用符「"」と項目中のエスケープされた引用符「""」はそのまま出力される(後処理として、項目の先頭と末尾から「"」を削る処理と、項目中の「""」を「"」に置換する処理が必要である)。

var csv = "猫, \"子犬\", , \"123,000\", 途中に\"\"引用符 , "
          + "途中に\r\n改行";
Regex.Split(csv, "\\s*,\\s*(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)").DisplayAll();
// 出力
// 1:'猫'
// 2:'"子犬"'
// 3:''
// 4:'"123,000"'
// 5:'途中に""引用符'
// 6:'途中に
// 改行'

Dim csv = "猫, ""子犬"", , ""123,000"", 途中に""""引用符 , " _
          + "途中に" + vbCrLf + "改行"
Regex.Split(csv, "\s*,\s*(?=(?:[^""]*""[^""]*"")*[^""]*$)").DisplayAll()
' 出力
' 1:'猫'
' 2:'"子犬"'
' 3:''
' 4:'"123,000"'
' 5:'途中に""引用符'
' 6:'途中に
' 改行'

CSVのデータを分割する例(上:C#、下:VB)
CSVのデータは、項目ごとに「,」で区切られている。ただし、「"」で囲った中にある「,」では区切らない。そのルールで分割するロジックを書くと複雑なコードになってしまうが、正規表現を使えば1行で分割できるのだ。
なお、CSVの規格「RFC 4180」では項目の前後の空白も項目に含めるべきとなっているが、ここではあえて削るようにしている。
また、CSVデータの分割には、Visual BasicのTextFieldParserクラス(Microsoft.VisualBasic.FileIO名前空間)を利用する方法もある(項目途中の改行には対応していない)。その方法は「.NET TIPS:CSVファイルを読み込むには?[2.0のみ、C#、VB]」をご覧いただきたい。その際、StringオブジェクトをTextFieldParserクラスに処理させるには、次のようにしてMemoryStreamオブジェクト(System.IO名前空間)に変換しておく。
Dim stream = New MemoryStream(System.Text.Encoding.UTF8.GetBytes(csv))
また、C#からVisual Basicの機能を使う方法は、「.NET TIPS:VB.NET固有の関数をC#で使用するには?」をご覧いただきたい。

まとめ

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

 また、本稿では正規表現を使ってCSVデータを分割する例も紹介した。正規表現について詳しくはMSDNの「.NET Framework の正規表現」をご覧いただきたい。

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

.NET TIPS

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

@IT Special

- PR -

TechTargetジャパン

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

RSSについて

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

メールマガジン登録

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