Regex : les captures groupes avec .Net

| Reading time ~4 minutes

Introduction

Dans cet article, je vais vous parler d’Expressions régulières communément appelées regex ou regexp.

Pour la suite de cet article, je vais considérer que vous savez manipuler et utiliser les expressions régulières. Je ferai d’autres articles sur les REGEXs, mais aujourd’hui, je souhaite parler d’une particularité du moteur d’expressions régulières .Net

Capturer des groupes

Pour le début de cette démonstration, je vais utiliser le moteur d’expressions régulières natif à powershell.

Vous savez que l’on peut capturer un groupe dans une chaine de caractère.

Ci-dessous un exemple dans lequel je vais récupérer un groupe :

$string = 'param1;param2;param3;param4;param5;'

$string -match '([a-z0-9]+);'

$matches

Name                           Value
----                           -----
1                              param1
0                              param1;

Dans cet exemple il n’y a qu’un seul groupe demandé, celui ci est bien délimité par les parenthèses (). Le moteur de regex Powershell me renvoie donc la valeure du groupe trouvé lors du premier match dans ma chaine de caractères.

  • groupe : param1

Cela correspond bien à ma demande.

Dans ma chaine de caractères, il y a une répétition de ma recherche, je vais donc indiquer dans ma REGEX que mon pattern se répète plusieurs fois.

$string = 'param1;param2;param3;param4;param5;'

$string -match '(?:([a-z0-9]+);)+'

$matches

Name                           Value
----                           -----
1                              param5
0                              param1;param2;param3;param4;param5; 

Le résultat contient cette fois-ci le groupe du dernier match du pattern dans ma chaine de caractères.

  • groupe : param5

Je pense que comme moi, vous vous attendiez à avoir le groupe de chaque match.

Résultat espéré :

Match 1
groupe : param1

Match 2
groupe : param2

Match 3
groupe : param3

Match 4
groupe : param4

Match 5
groupe : param5

y a-t-il une erreur dans ma regex ?

On peut se rendre compte que mon expression régulière est bonne, car elle renvoie correctement en Matches[1] une des valeurs attendues ( param5 ) et le Matches[0] me renvoie bien la totalité de ma chaine de caractères qui match avec la REGEX.

En fait, en cas de quantification d’un pattern, le moteur d’expressions régulières de powershell comme tous les moteurs d’expressions régulières (PHP,JAVA,PERL, PYTHON, etc..) ne créent pas plusieurs groupes de capture, mais au lieu de cela, ils renvoient le dernier groupe correspondant à la capture.

Passons maintenant à la raison d’être de cet article !

Avec le moteur regex .NET, tout cela change ! En effet le moteur .NET est le seul à garder une trace de toutes les captures qu’il a faites lors des matchs.

De façon normale, le moteur .NET indiquera toujours que le groupe capturé est param5. Mais si vous creusez plus profondément dans ce groupe, .Net retournera également une collection avec toutes les valeurs que le groupe a capturées successivement compte tenu du quantificateur +.

Cette fonctionnalité change tout !! Elle vous permet d’analyser facilement les chaînes avec un nombre inconnu de jetons.

Faisons un test avec le moteur .Net :

$string = 'param1;param2;param3;param4;param5;'

$resultat = [regex]::Match($string ,'(?:([a-z0-9]+);)+')

Regardons la différence entre le moteur powershell et le moteur .Net

#Powershell
$Matches.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Hashtable                                System.Object

#.Net
$Resultat.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Match                                    System.Text.RegularExpressions.Group

Regardons de plus près la variable $Resultat

$Resultat | gm

TypeName : System.Text.RegularExpressions.Match

Name        MemberType Definition                                                      
----        ---------- ----------                                                      
Equals      Method     bool Equals(System.Object obj)                                  
GetHashCode Method     int GetHashCode()                                               
GetType     Method     type GetType()                                                  
NextMatch   Method     System.Text.RegularExpressions.Match NextMatch()                
Result      Method     string Result(string replacement)                               
ToString    Method     string ToString()                                               
Captures    Property   System.Text.RegularExpressions.CaptureCollection Captures {get;}
Groups      Property   System.Text.RegularExpressions.GroupCollection Groups {get;}    
Index       Property   int Index {get;}                                                
Length      Property   int Length {get;}                                               
Name        Property   string Name {get;}                                              
Success     Property   bool Success {get;}                                             
Value       Property   string Value {get;}  

Suite à ce résultat, nous allons utiliser la propriété Groups afin d’obtenir le ou les groupes renvoyés par notre recherche.

Groups      Property   System.Text.RegularExpressions.GroupCollection Groups {get;} 

Afin de bien comprendre, je vais refaire l’exercice en mettant en comparaison les resultats du moteur powershell.

$string = 'param1;param2;param3;param4;param5;'

#Powershell
$string -match '(?:([a-z0-9]+);)+'

  $matches.Count
  ## on obtient -> 2
  ## il n'y a donc que 2 index possibles 0 et 1

  $matches[0]
  # on obtient -> param1;param2;param3;param4;param5;

  $matches[1]
  # on obtient -> param5

#.Net
$resultat = [regex]::Match($string ,'(?:([a-z0-9]+);)+')

  $resultat.Groups.Count
  # on obtient -> 2
  #il n'y a donc que 2 index possibles 0 et 1

  $resultat.Groups[0].Value
  # on obtient -> param1;param2;param3;param4;param5;

  $resultat.Groups[1].Value
  # on obtient -> param5

Nous avons donc exactement le meme résultat avec les 2 méthodes.

Oui, mais….

Faisons un petit focus sur le résultat du groupe. Pour cela, nous allons regarder la structure de l’object $resultat.Groups[1]

$resultat.Groups[1] | gm

  TypeName : System.Text.RegularExpressions.Group

Name        MemberType Definition                                                      
----        ---------- ----------                                                      
Equals      Method     bool Equals(System.Object obj)                                  
GetHashCode Method     int GetHashCode()                                               
GetType     Method     type GetType()                                                  
ToString    Method     string ToString()                                               
Captures    Property   System.Text.RegularExpressions.CaptureCollection Captures {get;}
Index       Property   int Index {get;}                                                
Length      Property   int Length {get;}                                               
Name        Property   string Name {get;}                                              
Success     Property   bool Success {get;}                                             
Value       Property   string Value {get;}   

Parmi les méthodes et propriétés, celle qui va nous intéresser et qui est introduite par le moteur .Net est :

Captures    Property   System.Text.RegularExpressions.CaptureCollection Captures {get;}

Cette propriété va nous permettre d’obtenir toutes les valeurs que le groupe recherché a prises au fur et à mesure de l’analyse de la chaine de caractères par le moteur REGEX .NET

Démonstration :

$string = 'param1;param2;param3;param4;param5;'

$resultat = [regex]::Match($string ,'(?:([a-z0-9]+);)+')

  $resultat.Groups[1].Captures.Count
  # on obtient -> 5
  #il y a donc 5 index possibles : 0 1 2 3 4

  $resultat.Groups[1].Captures[0].Value
  # on obtient -> param1

  $resultat.Groups[1].Captures[1].Value
  # on obtient -> param2

  $resultat.Groups[1].Captures[2].Value
  # on obtient -> param3

  $resultat.Groups[1].Captures[3].Value
  # on obtient -> param4

  $resultat.Groups[1].Captures[4].Value
  # on obtient -> param5

And voila !

On retrouve toutes les valeurs prises par le groupe et l’on obtient ainsi toutes les valeurs que nous voulions récupérer.

Conclusion

Cette propriété Captures ouvre la porte à de jolies possibilités en termes de scripting. J’ai pris dans cet article un exemple simple pour la démonstration, mais on peut aller beaucoup plus loin avec une expression régulière plus complexe.

Cela montre encore une fois la puissance des expressions régulières ainsi que celle du moteur .Net, à saluer au passage.

Musique pour coder : selection de sites

Published on July 13, 2018

RegexStorm : .Net regex testeur

Published on July 12, 2018