2017年7月17日 星期一

[.NET] REGEX 利用 Group 捕捉(Capture)重複字串(Repetitive Pattern)

Regular Expression(正規表達式),是一種抽象語言,用來表達字串的結構,比方[A-Za-z]+能表達所有的英文單字,應用上常用來驗證輸入,或是捕捉(Capture)具有特徵的字串片段,利用Regular Expression可以大幅簡化字串處理的繁瑣作業

這次的問題如下,假設某種字串格式的具體內容是以前綴(prefix)後綴(post-fix)框住,怎麼利用正規表示式驗證並捕捉(Capture)重複的片段?

好比以下類似座標的紀錄檔:
Model:M1
Coordinates:
X1.62 Y1.01
X9.53 Y2.3
X4.94 Y2.1
X8.55 Y3.15
X2.56 Y1.25

X5.57 Y2.72

首先分析一下文本的結構:
  • 標頭如同前兩行,形式與內容固定
  • 重複資料部分按行表達
  • 單筆座標以X<浮點數> Y<浮點數>表示
    • 座標值是我們最在意的部分
    • 希望可以一組一組捕捉分離,才好批次後處理解析數值
模式字串(Pattern)的設計想法如下:
  • 浮點數的表達不用傷腦筋,網路上很容易查到
  • 座標值的部分用()群組住單組X,Y座標(也是一種Symbol),後面以+表達此組Symbol會重複1到N次
  • 單組座標內又再次以()群組住浮點數字串表達
  • 考慮強健性,安插些泛空白字元在各個單元交接處
於是我們交出了以下的模式字串 :

"Model:[\\w]+[\\s]+Coordinates:[\\s]+(X([-+]?[0-9]*\\.?[0-9]+)[\\s]*Y([-+]?[0-9]*\\.?[0-9]+)[\\s]*)+"

其中"[-+]?[0-9]*\.?[0-9]+"就是浮點數的表達

這樣丟下去REGEX跑字串,模式字串中的()可以透過Match.Group(Index)存取,在這裡的模式字串我們共用到三個(),按照()在模式字串裡的順序,Group依序是:

  • Index-0:原始字串
  • Index-1:單組座標的群組
  • Index-2:X座標浮點數字串
  • Index-3:Y座標浮點數字串



String __input = 
"Model:M1 Coordinates: X1.62 Y1.01 X9.53 Y2.3 X4.94 Y2.1 X8.55 Y3.15 X2.56 Y1.25 X5.57 Y2.72";
String __patternUnnamed = 
"Model:[\\\\w]+[\\\\s]+Coordinates:[\\\\s]+(X([-+]?[0-9]*\\\\.?[0-9]+)[\\\\s]*Y([-+]?[0-9]*\\\\.?[0-9]+)[\\\\s]*)+";
List __coords = new List();
Match __matchUnnamed = Regex.Match(__input,__patternUnnamed);
//-----------------------------------
//  Unnamed         
//-----------------------------------
__coords.Clear();
for (int i = 0; i < __matchUnnamed.Groups[1].Captures.Count;i++)
{
// use X,Y group restore each coordinates
float[] eachCoord = new float[]{0,0};
eachCoord[0] = float.Parse(__matchUnnamed.Groups[2].Captures[i].Value);
eachCoord[1] = float.Parse(__matchUnnamed.Groups[3].Captures[i].Value);
__coords.Add(eachCoord);
}
//check all coordinates
Console.WriteLine(\"Unnamed\");
foreach (float[] item in __coords)
{
Console.WriteLine(String.Format(\"{0},{1}\",
item[0],
item[1]));
}
若模式字串裡()太多,或是覺得用INDEX標定群組可讀性太差,此時可以套入具名群組(Named Group)的概念,把()所圈住的Symbol具名化:

Model:[\\w]+[\\s]+Coordinates:[\\s]+(?<coord>X(?<X>[-+]?[0-9]*\\.?[0-9]+)[\\s]*Y(?<Y>[-+]?[0-9]*\\.?[0-9]+)[\\s]*)+

具名化後,便可以用Match.Group(Name)方式存取群組了

String __patternNamed = 
"Model:[\\\\w]+[\\\\s]+Coordinates:[\\\\s]+(?X(?[-+]?[0-9]*\\\\.?[0-9]+)[\\\\s]*Y(?[-+]?[0-9]*\\\\.?[0-9]+)[\\\\s]*)+";
Match __matchNamed = Regex.Match(__input,__patternNamed);
//-----------------------------------
//  Named
//-----------------------------------
//now we get the \"coord\" group
__coords.Clear();
for (int i = 0; i < __matchNamed.Groups[\"coord\"].Captures.Count; i++)
{
// use X,Y group restore each coordinates
float[] eachCoord = new float[]{0,0};
eachCoord[0] = float.Parse(__matchNamed.Groups[\"X\"].Captures[i].Value);
eachCoord[1] = float.Parse(__matchNamed.Groups[\"Y\"].Captures[i].Value);
__coords.Add(eachCoord);
}


後記1:.NET的正規表達式相容於Perl5,也與這裡相容,可以先快速的先驗表達式
後記2:據說.NET的Regular Expression模組是難得可以幫你Capture住群組值(Value)的函式庫

程式碼Repo

沒有留言:

張貼留言