Licence CC BY-NC-ND

Les expressions régulières

Dans ce chapitre, nous allons traiter des expressions régulières. Le système d’expressions régulières est un système très puissant qui nous permet de faire des opérations sur les chaînes de caractères. Avec lui, il est possible de faire ceci.

  1. Chercher un motif (pattern en anglais) dans la chaîne (par exemple, chercher les expressions entre guillemets).
  2. Traiter (éventuellement) les éléments trouvés. Nous pouvons les remplacer, les extraire, les récupérer, etc.

Par exemple, nous pourrons faire une expression régulière qui vérifie qu’une adresse courriel est valide.

Construire une expression régulière

Pour commencer, il nous faut voir ce qu’est une expression régulière. Nous avons vu en introduction ce à quoi elles pouvaient servir, mais nous ne savons toujours pas comment elles se présentent. En fait, nous pouvons voir une expression régulière comme un modèle. Certaines chaînes correspondent au modèle, d’autres non. Par exemple, le mot « chat » correspond au modèle des mots qui commencent par un « c » et se terminent par un « t », mais ne correspond pas à celui des mots qui commencent par un « c » et comportent un « y ».

Chercher un motif dans une chaîne signifie simplement vérifier que la chaîne testée correspond au modèle. Il ne nous reste plus qu’à voir à quoi ressemblent ces modèles et comment les écrire.

Notons qu’une expression régulière est de la classe Regexp (pour regular expression).

Tester la présence d’un mot dans une chaîne

Le moyen le plus simple d’écrire une expression régulière est de l’écrire entre /. Pour tester si un mot correspond à cette expression, nous utilisons ensuite le symbole =~. Par exemple, pour vérifier qu’une chaîne contient le mot « chat », nous écrirons ceci.

/chat/ =~ 'Le chat est sur la table.'

Ici, /chat/ est une expression régulière, et ce code vérifie que cette expression est présente dans la chaîne.

Cette expression ne renvoie pas un booléen comme nous pouvions nous y attendre. Elle renvoie l’indice du caractère où la correspondance a été établie. Ici, le c est le quatrième caractère, soit le caractère d’indice 3.

Et si notre chaîne ne correspond pas au modèle, il se passe quoi ?

Essayons, et voyons ce qui se passe.

/chat/ =~ 'Le chien est sur la table.'

La chaîne ne comprend pas le mot « chat », l’expression renvoie nil.

Notons que nous pouvons inverser l’écriture. Il est équivalent (à un détail près) d’écrire 'str' =~ /regex/ et /regex/ =~ 'str'. Nous pouvons donc choisir celle qui nous plaît le plus.

Avec !~, nous faisons l’opération inverse, à savoir que nous vérifions que notre chaîne ne contient pas le modèle testé. Cette fois, c’est un booléen qui est retourné (true si la chaîne ne vérifie pas le modèle, false sinon).

/chat/ !~ 'Le chien est sur la table.'  # => true
/chat/ !~ 'Le chat est sur la table.'   # => false

La méthode match? permet également de savoir si une chaîne correspond au modèle. Elle s’utilise indifféremment sur les chaînes de caractères ou les expressions régulières et renvoie True s’il y a correspondance et false sinon.

/chat/.match('Le chien est sur la table.')  # => false
'Le chat est sur la table.'.match(/chat/)   # => true

Ces exemples ne sont présents que pour introduire les expressions régulières. Si dans un programme, nous volons vérifier la présence d’une chaîne dans une autre chaîne, nous utiliserons plutôt cette syntaxe.

str = 'Le chat est sur la table.'
str['chat']   # => 'chat'

Les expressions régulières sont à utiliser dans le cas où l’on a un motif plus compliqué à rechercher.

Dans le cas où l’on a une expression régulière simple et qu’on veut obtenir la sous-chaîne qui y correspond dans une chaîne, nous pouvons utiliser la syntaxe str[regex]. Elle renvoie nil si la chaîne ne vérifie pas l’expression régulière et renvoie la partie de la chaîne qui la vérifie sinon.

str = 'Le chat est sur la table'
str[/chien/]   # nil
str[/chat/]    # chat 

Notons que nous pouvons également créer une expression régulière avec la syntaxe %r. Dans ce cas, nous privilégierons les accolades comme délimiteur. Nous n’utiliserons cette syntaxe que quand notre expression régulière contiendra le caractère /.

Les symboles

Maintenant que nous connaissons la syntaxe de base, nous pouvons nous pouvons nous lancer à la découverte de ce qui fait la puissance des expressions régulières.

Les quantificateurs

Jusqu’à maintenant, nous testions un nombre exact de caractères. Mais nous pouvons aussi chercher si quelque chose apparaît plusieurs fois dans une chaîne.

Il y a trois symboles qui servent de quantificateur.

  • Le symbole * indique que le caractère qu’il suit doit être présent zéro, une, ou plusieurs fois.
  • Le symbole + indique que le caractère qu’il suit doit être présent au moins une fois.
  • Le symbole ? indique que le caractère qu’il suit doit être présent au plus une fois.

Ainsi, l’expression régulière chats? est valide pour les mots chat et chats.

Ces caractères sont appelés métacaractères. Ce sont des caractères qui ont une signification particulière dans la construction des motifs de recherche. Nous ne pouvons donc pas les utiliser tels quel si nous voulons les rechercher. Pour les rechercher, nous les échappons avec une barre oblique (par exemple, la présence d’un point d’interrogation se teste avec \?).

Nous pouvons également rechercher un caractère un certain nombre de fois grâce aux intervalles de reconnaissance. Pour cela, nous utilisons les accolades (qui sont aussi des métacaractères). Nous pouvons les utiliser de plusieurs manières.

  • a{n} signifie que a doit être présent n fois.
  • a{n,} signifie que a doit être présent au moins n fois.
  • a{,n} signifie que a doit être présent au plus n fois.
  • a{n,m} signifie que a doit être présent entre n et m fois.

Nous ne pouvons pas utiliser d’espaces dans les intervalles de reconnaissance. "aaa" =~ /{2, }/ renverra nil contrairement à "aaa" =~ /{2,}/ qui renverra 0.

Avec tout ça, nous pouvons déjà écrire des expression régulières intéressantes. Par exemple, nous pouvons tester l’existence de caractères entre < et > dans une chaîne de caractères avec l’expression <.*>. Nous regardons s’il y a < puis n’importe quels caractères et enfin >.

Les ancrages

Parfois, nous voulons chercher quelque chose à une position particulière de la chaîne vérifiée. Par exemple, nous pouvons vouloir vérifier que notre chaîne se termine par « er ». Pour cela, nous allons utiliser \z.

"Aller" =~ /er\z/  # => 3
"Finir" =~ /er\z/  # => nil

Ce symbole permet de préciser la recherche et il n’est pas le seul.

  • \A signifie le début de la chaîne.
  • \Z signifie tout comme \z la fin de la chaîne. Cependant, dans le cas où la chaîne se termine par un retour à la ligne, \Z ne prend pas en compte ce retour à la ligne. Ainsi, "Aller\n" =~ /er\z/ vaut nil alors que "Aller\n" =~ /er\Z/ vaut 3.
  • ^ signifie le début d’une ligne. Si, notre chaîne est composée de plusieurs lignes, ce symbole permet de vérifier ce qui se trouve au début de chacune de ces lignes.
  • $ signifie la fin d’une ligne.

Le dollar, l’accent circonflexe et la barre oblique sont, comme nous pouvions nous en douter, des métacaractères. Si nous voulons tester la présence d’un de ces caractères, il ne faudra pas oublier de l’échapper.

Nous pouvons par exemple imaginer un fichier où l’on range des gens avec leur numéro. Ce fichier est organisé de la manière suivante.

NOM PRÉNOM NUMÉRO
NOM PRÉNOM NUMÉRO
etc.

Imaginons la situation suivante. Nous voulons récupérer le numéro d’une personne dont le nom se termine par « arnaj », mais nous avons oublié la première lettre de son nom. Pour connaître le nom de la personne et récupérer son numéro, nous cherchons dans le fichier une ligne qui correspond à ce qu’on cherche (pour cela, nous pourrons au préalable charger tout le contenu du fichier dans une chaîne de caractères ou encore créer un tableau avec une case par ligne du fichier).

regex = /^.arnaj /
str =~ regex

Nous cherchons, au début d’une ligne, n’importe quelle caractère suivie de « arnaj ».

Les classes de caractères

Les classes de caractères nous permettent de tester plusieurs caractères. Nous pouvons regarder si un caractère est dans une liste de caractères en utilisant les crochets. Ainsi, [oe] signifie « un o ou un e ». Pour rechercher « bon » ou « ben », nous utiliserons l’expression régulière b[oe]n.

'bon' =~ /b[oe]n/  # => 0
'ben' =~ /b[oe]n/  # => 0

Nous pouvons spécifier un intervalle plutôt que de lister toutes les possibilités grâce au symbole -. Ainsi, nous pouvons rechercher un caractère entre a et m ou un chiffre entre 2 et 7 sans écrire toutes ces lettres.

'bon' =~ /b[a-m]n/     # => 0
'187' =~ /[2-7]/       # => 2
'abo34' =~ /[a-m2-7]/  # => 0

Avec la dernière expression, nous recherchons un caractère entre a et m ou entre 2 et 7.

Ceci nous permet de faire les vérifications suivantes.

  • Vérifier qu’un caractère est alphabétique, avec [a-zA-Z].
  • Vérifier qu’un caractère est alphabétique et minuscule avec [a-z].
  • Vérifier qu’un caractère est numérique avec [0-9].
  • Vérifier qu’un caractère est alphanumérique, avec [a-zA-Z0-9].

Avec ce que l’on sait, nous pouvons chercher l’existence d’une balise <hn> dans un texte avec n un chiffre entre 1 et 6 (balises de titres en HTML) avec l’expression régulière <h[1-6]>.

Si nous voulons placer - dans notre classe de caractères, il nous faudra le placer en premier. Dans le cas contraire, il sera considéré comme un séparateur pour un intervalle. Ainsi [-15] signifie « - », « 1 » ou « 5 », alors que [1-5] signifie un chiffre entre « 1 » et « 5 ».

Le complémentaire

Parfois, il est plus simple de donner la liste des caractères qu’on ne veut pas plutôt que ceux qu’on veut. Et c’est possible. Par exemple, si nous voulons n’importe quel caractère sauf la lettre a, nous écrirons [^a]. En utilisant le symbole ^, nous indiquons que nous voulons tous les caractères sauf ceux énumérés.

"voiture" ~= /[^aeiouy]/ # => 0

Nous pouvons déjà construire des expressions compliquées.

regex = / [aeiouy]{2}[^aeiouy][aeiouy]{3} /

Ici, la chaîne sera valide si elle contient un mot de deux voyelles suivies d’une consonne puis de trois voyelles, ce mot devant être entouré d’espaces.

"Voici une phrase." =~ regex  # => nil
"Un oiseau vole." =~ regex    # => 2
Classes préexistantes

Mais pour nous faciliter la vie, des classes ont été inventées. Ainsi, nous pouvons vérifier plus facilement qu’un caractère est alphabétique, est numérique, etc.

  • Pour indiquer que le symbole peut être n’importe quel caractère, nous utilisons le symbole . (il est vérifié pour tous les caractères sauf le retour à la ligne). Le point est l’un des métacaractères les plus utiles. Ainsi, pour vérifier qu’un mot de trois lettres a le o comme lettre du milieu, nous utiliserons .o..
'bon' =~ /.o./  # => 0
'ben' =~ /.o./  # => 0
  • \w signifie un caractère alphanumérique. Il est équivalent à [a-zA-Z0-9_] (notons qu’il y a les lettres mais aussi le tiret bas).
  • \d signifie un caractère numérique. Il est équivalent à [0-9].
  • \s signifie un caractère d’espacement. Il est équivalent à [ \t\r\n\f\v].

En mettant la lettre en majuscule, nous obtenons la classe complémentaire. Ainsi, \W signifie un caractère non alphanumérique (équivalent à [^a-zA-Z0-9_]) et \D signifie un caractère non numérique (équivalent à [^0-9])

En utilisant cela, nous pouvons faire une expression régulière qui vérifie qu’une chaîne contient un numéro de téléphone.

regex = /0\d{9}/
str = "0123456789"
str =~ regex # => 0

Nous cherchons d’abord un zéro, puis neuf chiffres quelconques. Nous pouvons faire mieux et vérifier qu’une chaîne contient quelque chose comme ça <nom> : <numéro>.

regex = /\w+ : 0\d{9}/
str = "Karnaj : 0123456789"
str =~ regex  # => 0

Nous avons rajouté \w+ : avant le numéro : nous cherchons un mot et les deux-points avant le numéro.

Nous dispose de plusieurs autres classes. Pour avoir plus d’informations à leur sujet, nous pouvons aller regarder la documentation. Nous noterons les classes suivantes.

  • [[:alpha:]] signifie un caractère numérique. Contrairement à \w, il ne prend pas en compte le tiret bas. Il est donc équivalent à [a-zA-Z]
  • [[:alnum:]] signifie un caractère alphanumérique. Tout comme [[:alpha:]], il ne prend pas en compte le tiret bas, et est donc équivalent à [a-zA-Z0-9].
  • [[:lower:]] signifie un caractère alphabétique en minuscule (équivalent à [a-z]) et [[:upper:]] signifie un caractère alphabétique en majuscule (équivalent à [A-Z]).
  • [[:punct:]] signifie un symbole de ponctuation.

Pour tester qu’une chaîne contient un mot de quatre lettres précédées d’un espace et suivie d’une ponctuation, nous pouvons alors utiliser l’expression régulière suivante.

regex = / [[:alpha:]]{4}[[:punct:]]/
"Ça marche pas avec cette chaîne" =~ regex  # => nil
"Pour elle, oui" =~ regex                   # => 4
L’alternative

Supposons que l’on veuille vérifier qu’un mot contienne un « b » suivi de « eau » ou de « on » (soit vous êtes beau, soit vous êtes bon) puis d’un point. Pour ce faire, nous allons utiliser le métacaractère | qui nous permet de représenter une alternative.

regex = /on|eau\./
"Voici de l’eau."[regex]  # => eau
"C’est un don."[regex]    # => on

En écrivant /bon|eau/, les deux possibilités sont « bon » et « eau ». Si l’on veut que le « b » ne soit pas dans le premier choix, il nous faut isoler les choix en les entourant de parenthèses.

Ainsi, avec l’expression régulière qui suit, nous reconnaissons « bon » ou « beau ».

regex = /b(on|eau)\./
"On est beau."[regex]  # => "beau"
"On est bon."[regex]   # => "bon"

Nous pouvons proposer plus d’un choix, il suffit d’utiliser le métacaractère | autant de fois que nécessaire.

regex = /b(on|eau|rillant)\./
"On est beau."[regex]         # => "beau"
"On est bon."[regex]          # => "bon."
"On est brillant."[regex]     # => "brillant."

S’il n’y a que des lettres comme alternative, nous n’utiliserons pas ceci mais plutôt une classe de caractères.

Des informations supplémentaires

La classe MatchData

Pour le moment, nous n’avons fait que vérifier la correspondance entre une expression régulière et une chaîne de caractères. Pourtant, nous pouvons récupérer plusieurs autres informations sur la correspondance. Nous pouvons notamment « capturer » des expressions, c’est-à-dire récupérer une partie de la chaîne lue qui correspond à une partie de l’expression régulière lue. Pour cela, nous n’allons plus utiliser les opérateurs, mais la méthode match.

regex = %r{<h[1-6]>.*</h[1-6]>}
str = "<h1>Un titre</h1>"
regex.match(str)

La méthode match renvoie une instance de la classe MatchData si la correspondance a été trouvée et nil sinon. Notons que nous pouvons utiliser la méthode match avec les chaînes de caractères, mais aussi avec les expressions régulières (auquel cas le paramètre devra être une chaîne de caractères).

"abcd".match(/.{4}/)
/.{4}/.match("abcdefgh")
/.{4}/.match("abc")    # => nil 

De plus, nous pouvons rechercher une correspondance à partir d’un caractère particulier, en donnant à match comme second argument l’indice de ce caractère dans la chaîne.

/a.{2}/.match("abcdeafg", 3)

Cette fois, on obtient #<MatchData "afg">, la correspondance ayant été recherchée à partir de la quatrième lettre (celle d’indice 3).

L’objet obtenu nous donne accès à l’expression régulière et à la chaîne dont il est issue (grâce aux méthodes regexp et string), et donne également accès à la partie de la chaîne qui correspond à l’expression régulière. Pour l’obtenir, nous allons utiliser la méthode to_s.

m = /a.{2}/.match("abcdeafg", 3)
puts "#{m.to_s} est présent dans #{m.string}" if m

Pour compléter cela, nous pouvons obtenir la partie de la chaîne qui précède la correspondance trouvée (avec la méthode pre_match) et celle qui la suit (grâce à la méthode post_match).

m = /a.{2}/.match("abcdeafg", 3)
puts "#{m.string} se compose de #{m.pre_match}, #{m.to_s} et #{m.post_match}."
Capture d’expressions

La capture d’expression se fait grâce à ce que l’on appelle des « groupes de capture ». Un groupe de capture est une expression entourée de parenthèses dans une expression régulière. L’instance de MatchData obtenue avec la méthode match nous permet ensuite de récupérer la partie de la chaîne correspondant au groupe. Reprenons notre exemple de titre en HTML.

regex = %r{<h([1-6])>(.*)</h([1-6])>}
str = "<h6>Un titre</h6>"
m = regex.match(str)

Nous pouvons voir que m a changé. Nous pouvons accéder aux différents groupes capturés en utilisant les crochets. L’élément d’indice 0 est la chaîne toute entière et les indices des chaînes capturées commencent à partir de l’indice 1. Ainsi, m[1] donne "6", m[2] donne "Un titre" et m[3] donne "6".

Même si une instance de MatchData ressemble en ce sens a un tableau, ce n’en est pas un et en fait, elle ne sait même pas comment énumérer ses éléments (elle n’a pas de méthode each).

On peut néanmoins la transformer en tableau en utilisant la méthode to_a. Cependant, la chaîne totale ne nous intéresse parfois pas. En utilisant la méthode captures, on a accès aux différents groupes capturés. Elle retourne un tableau contenant les chaînes capturées.

m.captures  # => ["6", "Un titre", "6"] 

Par exemple, on peut créer une fonction pour vérifier qu’une chaîne contient un titre correct en HTML. On vérifie que la chaîne vérifie l’expression régulière. Si oui, on vérifie également que les deux chiffres des balises sont les mêmes.

class String
  def html_title?
    regex = %r{<h([1-6])>.*</h([1-6])>}
    m = match(regex)
    false unless m
    l = m.captures
    l[0] == l[1]
  end
end

Pour représenter une alternative, nous utilisons également des parenthèses, et avec ces parenthèses, on récupère également un groupe. Cependant, ce n’est pas toujours notre but. Heureusement, il existe un moyen pour ne pas capturer un groupe. Il suffit d’écrire ?: après la parenthèse ouvrante.

regex = /b(?:on|eau)\./
l = "On est beau.".match(regex).captures
puts l # => []

De manière générale, quand nous n’avons pas besoin du résultat d’un groupe, il vaut mieux ne pas capturer l’expression de ce groupe.

Nommer les groupes

Dans certaines expressions compliquées où l’on récupère plusieurs groupes, nommer ces groupes peut être une bonne idée. Et c’est possible. Pour cela, on utilise la syntaxe ?<name> dans le groupe qui aura alors pour nom name.

regex = %r{<h[?<nb_1>1-6]>.*</h[?<nb_2>1-6]>}
str = "<h1>Un titre</h1>"
str.match(regex)

Cette fois, l’objet obtenu contient des données supplémentaires. On voit en utilisant irb que nb_1 et nb_2 valent 1. La méthode named_captures nous permet alors d’obtenir un hachage dont les clés sont les noms des groupes et les valeurs la partie de la chaîne y correspondant. On écrit alors la méthode titre_html de la manière suivante.

class String
  def html_title?
    regex = %r{<h[?<nb_1>1-6]>.*</h[?<nb_2>1-6]>}
    m = match(regex)
    false unless m
    l = m.named_captures
    l[nb_1] == l[nb_2]
  end
end

Notons de plus la fonction names qui renvoie la liste des noms des groupes.

Contexte de comparaison

Les expressions régulières ne se limitent pas à ça puisqu’on peut rajouter un contexte à notre recherche. Par exemple, nous pouvons rechercher quelque chose au début ou à la fin d’une chaîne grâce à \A et \Z. Ces deux symboles sont des assertions. Les autres ancrages sont aussi des assertions, mais il s’agit là d’assertions plutôt simples.

Nous avons la possibilité de préciser un contexte beaucoup plus complexe. Prenons par exemple une phrase comme celle là.

Chez nous, les gens ont généralement deux prénoms, et on les appelle rarement par les deux. On préfère utiliser le premier prénom ou un surnom, mais quand on utilise le nom (par exemple, dans les situations formelles), il faut obligatoirement utiliser les deux prénoms.

Notre but est d’obtenir les occurrences de « noms », mais seulement dans les mots « prénom » (pour cela, nous allons utiliser la méthode scan qui peut aussi prendre une expression régulière en paramètre).

Dans notre expression, nous allons utiliser une assertion arrière positive (positive lookbehind assertion en anglais). Cette assertion est dite « arrière positive », car on vérifie la présence de quelque chose avant ce que l’on veut récupérer. Elle se construit avec (?<=pattern). Notre expression est alors la suivante.

regex = /(?<=pré)nom/
str.scan(regex)
=> ["nom", "nom", "nom"]

On obtient trois occurrences, les mot « nom » et « surnoms » n’ont pas été pris en compte.

Il y a également des assertions arrière négative (dans ce cas, on vérifie la non-présence de quelque chose) et des assertions avants (dans ce cas, on vérifie la présence ou la non-présence après ce qu’on cherche). Les quatre assertions que nous venons de voir sont appelés lookaround assertions puisqu’elles permettent de « regarder ce qui se passe autour ».

Voici un tableau récapitulatif de ces assertions.

Nom français

Nom anglais

Motif

Assertion avant positive

positive lookahead assertion

(?=pattern)

Assertion avant négative

negative lookahead assertion

(?!pattern)

Assertion arrière positive

positive lookbehind assertion

(?<=pattern)

Assertion arrière négative

negative lookbehind assertion

(?<!pattern)

Les lookaround assertions.

Nous pouvons remarquer que les symboles ne sont pas choisies au hasard, = indique que l’assertion est positive, ! qu’elle est négative et < signifie qu’il s’agit d’une assertion arrière.

Utilisons ces assertions pour récupérer les occurrences de « nom » qui commencent par un « pré » et ne se terminent pas par un « s ».

regex = /(?<=pré)nom(?!s)/
str.scan(regex)
=> ["nom"]

Cette fois, on n’obtient plus qu’une seule occurrence.

Et encore ?

Plusieurs comportements ?

Les expressions régulières peuvent avoir plusieurs comportements, et c’est à nous de choisir laquelle nous convient. Il y a trois types de comportements.

La gourmandise

Par défaut, une expression régulière est paresseuse. Cela signifie qu’elle essaye de trouver la plus grande correspondance possible. Un exemple simple de ce comportement est l’utilisation de l’expression régulière .+\.. On veut n’importe quoi, puis un point.

str = 'Une phrase. Une autre phrase.'
str[/.+\./] => "Une phrase. Une autre phrase."

Alors que « Une phrase. » correspond à l’expression, c’est toute l’expression qui est renvoyée. Avec une expression régulière gourmande, on cherche la plus grande correspondance possible. On teste toute la chaîne, si ça ne correspond pas, on enlève un caractère, etc. On continue ainsi jusqu’à ce que l’expression soit vérifiée (ou qu’il ne reste plus de mots).

Ce recul progressif consomme donc des ressources (même s’il est optimisé). Une expression régulière gourmande est donc… Gourmande en ressources.

La paresse

Dans le cas où on ne recherche pas la plus grande correspondance, on peut utiliser une expression régulière paresseuse. Comme son nom l’indique, une expression paresseuse en fait le moins possible et retourne la correspondance minimale. Avec une expression paresseuse, la correspondance est d’abord testée au début de la chaîne avec le nombre minimum de caractères.

Pour utiliser un quantificateur fainéant, il suffit de lui ajouter un point d’interrogation. Ainsi, +? est la version fainéante de +. Regardons notre exemple précédent avec lui.

str = 'Une phrase. Une autre phrase.'
str[/.+?\./] => "Une phrase."

Cette fois, nous obtenons la correspondance minimale. Les expressions fainéantes consomment en général moins de ressources.

La possessivité

Un troisième « mode » existe, il s’agit du mode possessif. Celui-ci tente de trouver la plus grande correspondance possible, comme le mode gourmand, mais si elle ne correspond pas, il échoue directement sans tenter autre chose.

Son avantage est donc cet échec rapide qui permet de consommer moins de ressources que le mode gourmand. Cependant, il peut donc arriver qu’il échoue alors qu’une correspondance existe, comme nous allons le voir.

Pour utiliser un quantificateur possessif, nous allons cette fois lui ajouter un symbole +. Regardons cet exemple qui échoue.

str = 'Une phrase. Une autre phrase'.
str[/.++\./] => nil

Ici, nous n’obtenons aucun résultat. En effet, le .++ va essayer de correspondre avec toute la chaîne, puis une correspondance va être testée entre \. et ce qui reste (une chaîne vide) et cette dernière correspondance va échouer. Là où avec le quantificateur gourmand nous serions retourné en arrière, avec le quantificateur possessif nous échouons directement.

Les quantificateurs possessifs sont rarement utilisés, mais qui sait. Ils sont parfois utilisés lors de la recherche d’une petite expression dans une plus grande expression.

Variables globales pour les expressions régulières

Les expressions régulières viennent avec un gros lot de variables globales. Pour commencer, il y a toutes les variables globales $n avec n un nombre. Elles contiennent le n-ième groupe qui a été récupéré la dernière fois qu’une expression régulière a été testée (que ce soit avec match, avec =~ ou même avec []).

'abc'[/(.)(.)/]
puts $1  => a
puts $2  => b
'abc'[/(.)/]
puts $1  => a
puts $2  => nil

Remarquons dans l’exemple précédent que $2 n’a pas gardé sa valeur après le second test. Comme nous l’avons dit, $n contient le n-ième groupe récupérée lors du dernier test.

Ces variables globales permettent de se passer de l’utilisation de match, et ce sont pas les seules puisque l’instance de MatchData associée à la dernière expression régulière testée est disponible grâce à la variable globale $~. De la même manière, $& permet d’avoir accès au dernier texte vérifié.

str = '<h1>Un titre</h1>'
str =~ %r{<h([1-6])>(.*)</h([1-6])>}
print $&  => "<h1>Un titre</h1>"
print $~[1]

Nous disposons encore d’autres variables globales comme $` qui correspond à la méthode pre_match de l’instance de MatchData, $' qui correspond lui à la méthode post_match et $+ qui correspond à l’expression associée au dernier groupe capturé.

Que faut-il utiliser, les variables globales, la méthode match, autre chose ?

Les variables globales comme $1 sont tirées du langage Perl et nous allons éviter de les utiliser et préférer utiliser match quand c’est possible. Si malgré tout nous avons besoin de récupérer des informations sur la dernière expression vérifiée et que match n’a pas été utilisée, nous pouvons utiliser la méthode last_match de la classe Regexp. Elle nous renvoie l’instance de MatchData associée au dernier test. On peut même lui passer en paramètre un entier i, pour avoir l’élément d’indice i de ce MatchData.

str = '<h1>Un titre</h1>'
str =~ %r{<h([1-6])>(.*)</h([1-6])>}
print Regexp.last_match(0)

Comme nous pouvons le voir, nous avons une multitude de moyens pour nous passer de l’utilisation des variables globales, utilisons-les. Si nous voyons ces variables, c’est parce qu’il faut savoir qu’elles existent et qu’elles peuvent être croisées dans certains codes.

Tous les problèmes ne sont pas des clous

Les expressions régulières font partie de ces choses qui ont l’air un peu magique et semblent résoudre tous les problèmes une fois qu’on sait s’en servir. En effet, elles sont très puissantes et sont en fait quasiment un autre langage qui, une fois maîtrisé, offre de nombreuses possibilités.

Néanmoins, il faut garder en tête qu’elles sont consommatrices de ressources. Ainsi, il faut éviter de les utiliser à tort et à travers, car non, tous les problèmes ne sont pas des clous. Par exemple, pour savoir si une chaîne contient une autre chaîne, on utilisera plutôt chaîne_1[chaîne_2].

De même, si nous pouvons utiliser scan avec une chaîne de caractères plutôt qu’une expression régulière, il ne faut pas hésiter à le faire. Notons que tout comme scan, gsub et sub peuvent s’utiliser avec des expressions régulières et que là encore nous allons préférer les utiliser avec des chaînes de caractères lorsque c’est possible et utiliser les fonctions adaptées.

str = '10 - 11 - 12 - 13'

# Pour remplacer un caractère par un autre, on utilise `tr`.
str.tr('-', '|')
=> "10 | 11 | 12 | 13"

# Pour remplacer un groupe de caractères fixés, on utilise
# gsub avec une chaîne de caractères.
str.gsub(' -', ',')
=> "10, 11, 12, 13"

# Pour remplacer un modèle par un autre, on utilise
# gsub avec une expression régulière et un bloc si nécessaire.
str.gsub(/\d+/) { |n| (n.to_i + 100).to_s }
=> "110, 111, 112, 113"

Nous allons finir sur cette citation qui résume ce que nous racontons là.

Some people, when confronted with a problem, think "I know, I’ll use regular expressions." Now they have two problems.

Jamie Zawinski

En français : Des gens, quand ils font face à un problème, se disent « je sais, je vais utiliser les expressions régulières ». Ils ont alors deux problèmes.

Exercices

Il est temps de pratiquer un peu et de voir par la même occasion à quel point les expressions régulières peuvent être puissantes.

Exercice 1

Le premier exercice est, comme d’habitude, plutôt simple. Notre objectif va être de valider une plaque d’immatriculation. Nous allons demander à l’utilisateur une chaîne de caractères et lui afficher si elle correspond à une plaque d’immatriculation valide ou pas. Une plaque est valide si elle suit le modèle « AA-111-AA » (donc deux lettres, un tiret, trois chiffres, un tiret, et de nouveau deux lettres).

Correction.

regex = /\A[A-Z]{2}-\d{3}-[A-Z]{2}\z/

print "Entrez la plaque : "
str = gets.chomp
print str.match?(regex) ? "Plaque valide." : "Plaque invalide."

Ici, un point qu’il ne fallait pas oublier est la présence de \A et de \z. Sans eux, la chaîne « je suis AA-111-AA. » serait considérée comme valide.

Exercice 2

Le but de cet exercice est de gérer un carnet d’adresse. Dans ce carnet, on gardera les noms, numéros de téléphone, et adresse courriel. Nous pouvons faire le programme plus ou moins complet (ajout et retrait d’un contact au carnet, modification, etc.). Dans tous les cas, nous devrons vérifier que son nom et son adresse courriel sont bien valides. C’est une bonne occasion pour travailler plusieurs notions et pas seulement les expressions régulières.

Correction.

class String
  def mail?
    match?(/\A[\w.-]+@[\w.-]{2,}\.[a-z]{2,4}\z/)
  end

  def phone?
    match?(/\A0\d{9}\z/)
  end

  def alpha?
    match?(/\A[[:alpha:]]+\z/)
  end
end

class Contact
  attr_reader :name
  attr_accessor :mail, :phone

  def initialize(name, mail, phone)
    @name = name
    @mail = mail
    @phone = phone
  end

  def to_s
    "#{@name} - #{@phone} - #{@mail}"
  end
end

def get_contact
  print "Entrez le nom du contact :"
  name = gets.chomp until name.alpha?
  print "Entrez son numéro de téléphone : "
  phone = gets.chomp until phone.phone?
  print "Entrez son adresse courriel : "
  mail = gets.chomp until mail.mail?
end

Il n’y a pas trop de difficulté ici, nous avons des méthodes pour vérifier qu’une chaîne est un nom, un numéro de téléphone ou une adresse courriel, et nous créons une classe pour les contacts.

Exercice 3

Après un exercice complet, mais finalement pas très axé sur les expressions régulières, en voici un qui donne à réfléchir. Ici, nous allons vérifier qu’une expression mathématique est correcte syntaxiquement et sémantiquement. Il nous faudra donc nous assurer de ces trois choses.

  1. Il n’y a que des caractères autorisés.
  2. L’expression est correctement parenthésée.
  3. Tous les termes sont bien placés (par exemple, il n’y a pas un + à côté d’un *.

Vu que nous aurons peut-être besoin d’expressions régulières complexes, précisons ici que l’interpolation fonctionne aussi dans les expressions régulières.

two_char = "[A-Z]{2}"
even_digit = "[02468]"
regex = %r{#{two_char} - #{even_digit} #{even_digit}}
"CD - 6 2".match(regex)

Correction.

nombre = '\d+(\.\d+)?'
opérateur = '[+-/*]'
variable = '[A-Z]'

atomique = "(#{nombre}|#{variable})"
atomique_signé = "[+-]?(#{nombre}|#{variable})"

expression = "#{atomique_signé}(#{opérateur}(#{atomique}|\\(#{atomique_signé}\\)))*"
expression_parenthésée = "\\(#{expression}\\)"

regex =  %r{#{expression_parenthésée}}

str = gets
str.gsub!(/\s+/, '')
str.gsub!(regex, "X") until str.scan(regex).empty?

puts str
puts str[/\A#{expression}\z/] ? "Valide." : "Invalide."

L’idée ici est qu’une expression correcte est composée d’expressions elles-mêmes correctes. Ainsi, nous remplaçons toutes les expressions de la forme (n1 op1 n2 op2 n3 op3 n4 ...) par la variable X. Et on fait ceci tant que des expressions de ce type existent. Ensuite, si l’expression est correcte, il nous restera une seule expression valide. Testons le principe sur un exemple.

((2 + 3) * (3 - 5 + 8)) + (1 + 2 * (3 + 8))
(X * X) + (1 + 2 * X)
X + X

À noter qu’ici, nous ne considérons pas les expressions de la forme (1 + 2)(3 + 4) (où le signe * n’est pas mis).


Et c’est terminé pour ce chapitre. Bien sûr, les motifs des expressions régulières ne sont pas à connaître par cœur.

  • Les expressions régulières permettent de vérifier qu’une chaîne correspond à un modèle.
  • Elles permettent également de récupérer certains motifs dans une chaîne (avec scan par exemple).
  • Nous ne devons pas abuser des variables globales associées aux expressions régulières.
  • Les expressions régulières ne permettent pas de résoudre tous les problèmes.

Pour finir, nous pouvons aller visiter les pages de documentation de MatchData et de Regexp. En particulier, les parties « options », « free spacing mode and comments » et « encoding » qui nous proposent quelques options pour modifier un peu nos expressions régulières (par exemple les écrire sur plusieurs lignes et y mettre des commentaires).