User:Lvl/Regex/Chapitre 1
Chapitre 1 -- les bases
Principe
Une "expression régulière" (anglais, "regular expression", ou en abrégé regex) est une façon de représenter un motif particulier de chaînes de caractères, en utilisant une certaine notation informatique. Les motifs sont définis caractère par caractère, et il existe des constructions permettant de préciser plusieurs possibilités ou combinaisons possibles.
Par exemple, il est possible d'écrire une expression régulière signifiant «un ou plusieurs chiffres suivis par un point». Si on fait une recherche avec cette expression régulière sur le texte suivant:
- "Article 16... Prix 12.500.000$."
on trouvera trois morceaux du texte qui correspondent à cette expression régulière: "16.", "12." et "500."
Globalement, il y a deux types de choses à apprendre pour les expressions régulières: il faut penser en terme de combinaisons de caractères (par exemple «un ou plusieurs chiffres, puis un point»), et il faut apprendre la notation horrible attendue par l'ordinateur (par exemple '[0-9]+\.
' ou équivalent).
Dans ce petit cours, je représente les expressions régulières entre guillemets simples 'comme ceci
' pour des raisons de clarté (n'incluez pas ces guillemets lorsque vous utilisez les expressions dans votre logiciel favori!). Les autres fragments de texte seront indiqués entre "guillemets doubles".
Chaîne de caractères ordinaires
L'expression régulière la plus simple est une chaîne de caractères ordinaires.
Par exemple, dans le texte "Bonjour à vous tous", l'expression régulière 'Bon
' correspond aux trois premiers caractères "Bon". L'expression 'us t
' correspond également à un morceau de ce texte. Mais il n'y a pas de texte correspondant à l'expression 'bon
' (parce que les caractères "b" et "B" sont considérés comme étant différents), ni à 'Bon jour
' (il n'y a pas d'espace dans le texte).
Exercice: Combien d'endroits du texte "Bonjour à vous tous" sont reconnus par l'expression régulière 'ous
' ?
- Réponse.
Note sur la syntaxe: en réalité une expression régulière comme 'Bo
' est composée de deux éléments: 'B
' qui reconnaît uniquement la lettre "B", et 'o
' qui reconnaît uniquement la lettre "o". La juxtaposition de ces deux expressions atomiques constitue l'expression 'Bo
', qui signifie un morceau de texte reconnu par le premier élément, suivi immédiatement par un morceau de texte reconnu par le second élément. Ça paraît étrange de décrire ainsi une expression régulière aussi simple, mais c'est également valable pour les expressions plus complexes.
Métacaractères, \
Certains caractères sont réservés à la notation des expressions régulières. Ce sont les métacaractères:
{}[]()^$.|*+?\
Ils sont décrits plus bas, mais pour l'instant il faut savoir que pour reconnaître l'un de ces caractères il faut mettre un backslash devant. Pour reconnaître un point, on utilise '\.
'; pour reconnaître un caractère "+", on utilise '\+
', etc.
- l'expression régulière '
2+2
' ne correspond à aucune chaîne dans le texte "2+2=4" - '
2\+2
' correspond au texte "2+2".
Note sur la syntaxe: '2\+2
' est la juxtaposition de trois atomes: '2
', '\+
' qui reconnaît le caractère "+", et '2
'.
Hormis les métacaractères, tous les autres caractères sont autorisés: a
, 0
, µ
, Ñ
, §
, «
, espace, etc.
Le backslash fait aussi partie des métacaractères, et donc pour spécifier un backslash dans une expression régulière il faut mettre un autre backslash devant:
- '
C:\\autoexec\.bat
' correspond à "C:\autoexec.bat"
Exercice: Une expression régulière correspondant au texte "1^er"?
- Réponse.
Exercice: Une expression régulière pour le texte "H_{2}O"?
- Réponse.
Début, fin de ligne ^
$
Par défaut, une expression régulière va correspondre à des chaînes de caractères situées à n'importe quel endroit de la ligne de notre texte. Mais on peut imposer de restreindre au début ou à la fin de la ligne:
Le chapeau ^
correspond au début de la ligne.
- '
^truc
' ne trouvera que les endroits du texte où les quatre premiers caractères de la ligne sont t, r, u, et c dans cet ordre.
Le dollar $
correspond à la fin de la ligne.
- '
ligne\.$
' correspond aux six caractères "ligne." en fin de ligne.
On peut préciser les deux:
- '
^<tb>$
' correspond à une ligne contenant exactement "<tb>", rien avant, ni après - '
^$
' correspond à une ligne vide (rien entre le début et la fin).
Exercice: Trouver les lignes indentées d'au moins deux espaces
- Réponse.
Exercice: Trouver les lignes avec un espace à la fin
- Réponse.
Exercice: Trouver une ligne commençant par "/*"
- Réponse.
Note sur la syntaxe: Dans l'expression régulière '^\$
', nous avons encore deux atomes: '^
' qui ne reconnaît que le début de la ligne, et '\$
' qui reconnaît un caractère "$".
Exercice: Que fait cette expression régulière: '^\\\$
'
- Réponse.
Classes de caractères [ ]
et [^ ]
J'ai dit au début que les expressions régulières permettaient de représenter d'un coup tout un ensemble de chaînes possibles. Un premier élément de variété est la classe de caractères.
Une classe de caractères est un élément d'expression régulière qui correspond à un caractère choisi parmi un ensemble de caractères possibles. On représente cela avec les crochets [
…]
en précisant les caractères possibles entre les crochets.
La syntaxe s'exprime le mieux par des exemples:
- '
[abc123]
' un caractère choisi parmi "a", "b", "c", "1", "2", "3".
À l'intérieur des crochets on peut représenter un intervalle de caractères avec le signe -
:
- '
[0-9]
' un caractère choisi parmi "0", "1", …, "9", dans l'ordre des codes de caractères, c'est-à-dire un chiffre. - '
[A-Za-z]
' un caractère parmi "A" … "Z", "a" … "z", c'est-à-dire une lettre majuscule ou minuscule sans accents ni cédille.
Il y a aussi la possibilité d'inverser l'ensemble en mettant un ^
comme premier caractère juste après le crochet ouvrant:
- '
[^abc]
' signifie n'importe quel caractère sauf "a", "b", "c" - '
[^0-9]
' signifie n'importe quel caractère sauf un chiffre. - etc.
Donc: '[Oo]ui
' est une expression régulière constituée de trois morceaux: '[Oo]
' qui reconnaît un caractère parmi "O" et "o"; 'u
' qui reconnaît un "u" et 'i
' qui reconnaît un "i". '[Oo]ui
' reconnaît "Oui" et "oui".
'[Nn][Oo][Nn]
' reconnaît "non", "Non", "NON" (mais aussi "nON", "NOn", etc.)
Note sur la syntaxe: Je suppose qu'il est inutile de rappeler que '[Nn][Oo][Nn]
' est constitué de trois atomes, '[Nn]
' suivi de '[Oo]
' suivi de '[Nn]
'.
Exercice: Trouver des mots avec deux majuscules ou plus suivies d'une minuscule, comme "TRuc", "MAchin", "CHOse" (on ignorera les problèmes d'accents pour l'instant).
- Réponse.
Les métacaractères n'ont pas besoin d'un backslash à l'intérieur des classes de caractères; mais on emploie le backslash pour protéger les caractères qui peuvent poser problème à l'intérieur des crochets, c'est à dire les caractères "-", "^", "]" et "\":
- '
[\]a]bc
' reconnaît "abc" ou "]bc". - '
[a\-c]d
' reconnaît "ad", "-d" ou "cd" - '
[[\]\\]
' correspond à un caractère parmi "[", "]", "\".
- (Détail: il n'y a pas besoin de mettre de backslash devant
-
si c'est le premier ou le dernier caractère de la classe de caractères, c'est-à-dire juste avant le]
ou juste après le[
ou[^
; de même pour^
, s'il n'est pas juste après le crochet ouvrant ce n'est pas nécessaire de mettre un\
:- '
[-ab]
', '[ab-]
' et '[a\-b]
' sont tous équivalents. - '
[\^a]
', '[a\^]
' et '[a^]
' sont équivalents.
- '
- Vous pouvez même vous passer du backslash avant
]
s'il est immediatement après[
ou[^
! (c'était employé dans la toute première version des expressions régulières, à la préhistoire de l'informatique quand les backslash n'avaient pas de signification spéciale entre les crochets):- '
[]u]
' est equivalent à '[\]u]
' et '[u\]]
'
- '
- Mais c'est du pur pédantisme, dans le doute je vous recommande d'utiliser backslash pour des raisons de clarté.)
Exercice: Quelles sont d'autres façons d'écrire '[abc123]
'?
- Réponse.
Exercice: Quelle expression régulière correspond à un caractère quelconque parmi "^" et "_"?
- Réponse.
Exercice: Une expression régulière reconnaissant n'importe quel caractère sauf "\"?
- Réponse.
Exercice: Trouver un "q" suivi d'un caractère autre que "u"?
- Réponse.
Exercice: Trouver un signe de ponctuation précédé d'un espace superflu
- Réponse.
Exercice: Expression régulière correspondant à un point suivi d'autre chose qu'un espace, un autre point, ou une parenthèse fermante?
- Réponse.
Exercice: Trouver deux espaces consécutifs (en ignorant les espaces en début de ligne) Indice: Pour ignorer les espaces en début de ligne, il suffit de trouver deux espaces précédés par un caractère autre que l'espace.
- Réponse.
Exercice: Trouver les points de suspension avec seulement deux points (hors début et fin de ligne):
- Réponse.
N'importe quel caractère .
Enfin, le point .
correspond à la classe de tous les caractères possibles (sauf le retour à la ligne). ainsi:
- '
e..e
' reconnaît un "e", suivi de deux caractères quelconques, suivi d'un "e"; - '
^...$
' reconnaît une ligne contenant exactement trois caractères
Exercice: Trouver une ligne dont le troisième caractère est un chiffre
- Réponse.
Exercice: Trouver une ligne dont l'avant-dernier caractère est un "a"
- Réponse.
Alternative |
On peut composer une expression régulière reconnaissant soit une chose, soit une autre en utilisant deux sous-expressions séparées par une barre verticale |
.
- '
chat|chien
' reconnaît "chat" ou "chien".
Les diverses branches de l'alternative peuvent utiliser toutes les possibilités des expressions régulières. Par exemple
- '
^a|b.b|[aeiouy]$
'
reconnaît un "a" en début de ligne (c'est '^a
'), ou deux "b" séparés par un caractère quelconque ('b.b
'), ou une voyelle en fin de ligne ('[aeiouy]$
').
Exercice: Que fait cette expression: '^/[*#]$|^[*#]/$
'
- Réponse.
Regroupement ( )
Si on veut reconnaître "petit chat" ou "petit chien", on peut bien sûr mettre 'petit chat|petit chien
', mais il serait plus simple de limiter la portée de l'alternative et d'en exclure les parties communes.
Pour ne faire porter l'alternative que sur une partie du motif complet, on utilise les parenthèses pour isoler l'alternative dans une sous-expression:
- '
petit (chat|chien)
' reconnaît "petit chat" ou "petit chien"
Décomposons: cette expression se compose de:
- '
petit
' qui reconnaît "petit " (avec espace à la fin); - '
(
…)
' une sous-expression entre parenthèses, à savoir:- '
chat|chien
' c'est-à-dire l'alternative entre 'chat
' et 'chien
'.
- '
On peut combiner dans tous les sens:
- '
(petit|(très |)grand) (chat|chien)
' reconnaît "petit chat", "très grand chat", "grand chien", etc.
Exercice: Reconnaître les années de 1900 à 2099 inclus
- Réponse.
Exercice: Que fait cette expression régulière: '<(/|)([ib]|sc)>'
- Réponse.
Remplacement
Jusqu'ici nous avons surtout effectué des recherches. Mais il est possible d'effectuer des remplacements très puissants, en récupérant, dans le texte de remplacement, une partie du texte reconnu par l'expression régulière.
On utilise pour cela une sous-expression en mettant une partie de l'expression régulière entre des parenthèses. On peut rappeler le texte correspondant à la première sous-expression avec "$1" dans la chaîne de remplacement, et de même "$2" pour la deuxième sous-expression, etc. jusqu'à "$9".
Exemple: 'petit (chat)
' => "grand $1" pour remplacer "petit chat" par "grand chat".
Exemple: remplacer ahle et ihle par able et ible:
- '
([ai])hle
' => '$1ble'
Exemple: déplacer à gauche d'une parenthèse ou d'un guillemet fermant
- '
([»)])(</i>)
' => '$2$1'
Note: les expressions régulières se lisent toujours de gauche à droite. Ici on a donc dans l'ordre (
, à l'intérieur de cette parenthèse la classe de caractères [»)]
, puis la parenthèse fermante )
, etc. Un )
présent à l'intérieur d'une classe de caractères, ou bien protégé par un backslash \)
ne termine pas la sous-expression délimitée par les parenthèses ( )
.
Exemple: retirer l'espace avant les signes de ponctuation usuels
- '
([,?;.:!])
' => "$1"
Exercice: La même chose, y compris avant ")", "]"
- Réponse.
Exercice: Remplacer un espace entre deux chiffres par " "
- Réponse.
- (On peut imbriquer les groupes. Le numéro correspond à l'ordre d'apparition des parenthèses ouvrantes dans l'expression régulière. Ainsi, $1 sera le groupe ayant la parenthèse ouvrante la plus à gauche, $2 celui ayant la parenthèse ouvrante suivante, etc. Voici un exemple arbitraire avec les numéros de groupes indiqués au-dessous:
(ab(cd|ef)((gh)|i))
1 2 34
- Donc, si cette expression régulière reconnaît du texte, $2 contiendra "cd" ou "ef", $3 contiendra "gh" ou "i", $4 contiendra "gh" ou rien du tout (si c'est la branche '
i
' de l'alternative qui a permis à l'expression régulière de reconnaître le texte), et $1 contiendra "abcdgh", "abcdi", "abefgh" ou "abefi".)
Exemple: '(CHAPITRE ([1-9]))
' => "<h2><a name="ch_$2">$1</a></h2>"
Quantificateurs ?
*
+
{ }
Les quantificateurs ?
, *
, +
et { }
permettent de spécifier un nombre de répétitions possible pour un motif. Les quantificateurs se placent juste après l'atome (caractère, caractère précédé par backslash, classe de caractères ou sous-expression) dont ils précisent le nombre de répétitions possible. Ils ont la signification suivante:
- a
?
signifie l'atome a présent 1 ou 0 fois - a
*
signifie l'atome a présent 0 ou plusieurs fois - a
+
signifie l'atome a présent 1 ou plusieurs fois - a
{
n,
m}
signifie l'atome a présent au moins n fois, mais pas plus que m fois. - a
{
n,}
signifie l'atome a présent au moins n fois - a
{
n}
signifie l'atome a présent exactement n fois
C'est plus clair sur un exemple:
l'expression régulière 'co*l
' est formée trois morceaux: 'c
', 'o*
' signifiant un "o" présent zéro fois ou plus, et 'l
'. Elle correspond aux occurrences "cl", "col" et "cool" dans le texte suivant:
- «le club des bricoleurs anti-alcooliques.»
Pour ne reconnaître que "col" et "cool", nous pourrions utiliser 'co+l
', ou 'coo*l
', ou 'co{1,2}l
', etc.
Note: Ce sont bien les expressions régulières que les quantificateurs répètent, pas le texte particulier reconnu par ces expressions. Cela signifie que '[AB]{2}
' est equivalent à '[AB][AB]
' (c.-à-d. un '[AB]
' suivi par un '[AB]
'), et reconnaîtra "AA", "AB", "BA" et "BB". Il est possible de demander la répétition d'un texte particulier (par exemple, rechercher trois lettres consécutives identiques) mais comme c'est très rarement employé, je n'en parlerai qu'à la prochaine leçon.
Autres exemples:
- '
[a-z]+ +\.*
' une ou plusieurs lettres minuscules, suivies d'un ou plusieurs espaces, suivis éventuellement d'un nombre quelconque de points.
- un nombre supérieur ou égal à 1: '
[1-9][0-9]*
' - quatre espaces: '
{4}
' - <sc> ou </sc><nowiki>: '<code style="padding: 2px"><nowiki>code>/?sc></code>'
: un appel de note comme "[2]", "[13]", etc.: '<code style="padding: 2px"><nowiki>\[[0-9]+]'
- (note: cela correspond aussi à des notes étranges comme "[0]" ou "[0001]")
- une ligne de 73 caractères ou plus: '
.{73,}
'
Exercice: Remplacer les points de suspension comprenant quatre points ou plus par trois points "...".
- Réponse.
- Réponse.
Le quantificateur se met également après une sous-expression: par exemple,
- '
(truc-){4}truc
'
reconnaît uniquement la chaîne "truc-truc-truc-truc-truc".
Exercice: Trouver les lignes indentées d'un nombre impair d'espaces
- Réponse.
Un exemple compliqué
Essayons de construire une expression pour représenter les nombres. Commençons par les nombres entiers d'abord: 0, 1, 25, etc.
L'expression
- '
[0-9]+
'
est un bon début, mais il reconnaît aussi "00", ce qui n'est pas voulu.
On s'en sort considérant à part le nombre 0, et en imposant un chiffre autre que
zéro en tête des autres nombres, c'est-à-dire [1-9]
suivi d'un nombre
quelconque de chiffres:
- '
0|[1-9][0-9]*
'
Si on veut ajouter des chiffres après la virgule: ',[0-9]+
' (et pas *
car il faut au moins un chiffre après la virgule):
- '
(0|[1-9][0-9]*)(,[0-9]+)?
'
Et si on veut aussi autoriser -2,5 ? On pourrait mettre '-?
' devant, mais ça donnerait aussi "-0", ce qui n'est pas terrible: donc ça peut donner quelque chose comme:
- '
(0|-?[1-9][0-9]*)(,[0-9]+)?|-0,[0-9]+
'
À noter qu'on a encore -0,00 que l'on peut vouloir exclure: il faudrait alors imposer au moins un chiffre différent de zéro:
- '
(0|-?[1-9][0-9]*)(,[0-9]+)?|-0,[0-9]*[1-9][0-9]*
'
Pour récapituler, nous avons construit notre expression régulière:
- en spécifiant la tâche en détail,
- en identifiant ce qui est reconnu en trop, pour réduire le champ,
- en découpant le problème en plus petites parties,
- en notant les petites parties dans la notation des expressions régulières,
- en assemblant entre elles ces expressions régulières.
Notez aussi à quel point les choses peuvent vite devenir très compliquées si on veut être trop précis. Avec les expressions régulières souvent il est plus facile (et on court moins de risque d'erreur) d'utiliser une expression simple qui couvre 90% des cas, et de faire les 10% restant à la main.
Le danger des quantificateurs gourmands
Les quantificateurs sont gourmands: ils cherchent à reconnaître le plus de texte possible. En général dans les exemples au dessus, ce n'était pas très grave quand on se limitait à multiplier des caractères précis comme des chiffres ou des signes de ponctuation. Mais les difficultés commencent quand on utilise des quantificateurs sur des classes de caractères arbitraires.
Prenons un exemple sur le texte
- "Le chien va bien"
et cherchons '(.*)hien(.*)
'
On aura $1 = "Le c" et $2 = " va bien".
Mais si en fait on utilise l'expression régulière '(.*)ien(.*)
',
on obtient alors $1 = "Le chien va b", et $2 = "".
Le premier quantificateur .*
consomme la plus grande partie possible de la chaîne tout en laissant à la suite de l'expression la possibilité d'être reconnue.
Que s'est-il passé? Simulons ce que fait l'ordinateur quand il recherche '(.*)ien(.*)
'
- on commence le plus à gauche possible, à la lettre L.
.*
est gourmand, on va donc lire tout le texte "Le chien va bien".- on essaie ensuite de reconnaître la suite de l'expression régulière, on cherche donc un '
i
'; comme il n'y a pas de i après la fin du texte, on revient en arrière d'un cran, à la lettre "n". - "n" ne correspond toujours pas à un '
i
', on continue à revenir en arrière. - finalement on a reculé de 3 cases, sur le "i"; on reconnaît le '
i
', 'e
' et 'n
'. - Enfin le texte vide restant peut être reconnu par '
.*
', donc c'est fini.
Comment éviter les problèmes avec les quantificateurs gourmands?
Si on cherche à reconnaître une ligne entière, ou à chercher quelque chose qui ne peut apparaître qu'une seule fois dans une même ligne, on n'a en général pas de problème; mais si on veut remplacer une partie de ligne par une autre, il faut absolument se limiter et éviter que les quantificateurs reconnaissent plus de texte que ce que nous voulons. Il faut pour cela restreindre les possibilités de reconnaissance de l'expression régulière au plus juste.
Exemple: "entre <i>un</i> et <i>plusieurs</i>."
si on remplace '<i>(.*)</i>
' par "<em>$1</em>
", le .*
va avaler tout le texte "un</i> et <i>plusieurs" et donner "entre <em>un</i> et <i>plusieurs</em>."
Donc, nous devons trouver un moyen d'interdire à .*
de dépasser le "</i>", par exemple en retirant "<" des caractères autorisés dans le motif répété:
- '
<i>([^<]*)</i>
'
C'est la façon la plus simple de borner les répétitions.
- (Remarque: ça ne marche pas s'il y a des balises imbriquées <i>...<sup>...</sup>...</i>. Pour ça il faut utiliser des notions plus avancées que nous verrons plus tard.)
Remarquons qu'on se complique parfois la vie: il suffisait
de remplacer '(</?)i>
' par '$1em>
'... ou même de remplacer 'i>
' par 'em>
'!
Exercice: Retirer les notes commençant par [** et finissant par ]
- Réponse.
Exercice: Remplacer ^ suivi de lettres et chiffres par la même chose avec { } autour des lettres et chiffres (par exemple ^me. => ^{me}.)
- Réponse.
Exercice: Remplacer ^{truc} par <sup>truc</sup>
- Réponse.
Exercice: Remplacer <a name="truc"> par <a name="truc" id="truc">
- Réponse.
Résumé
Une expression régulière est une façon d'exprimer un motif de texte dans une syntaxe concise (et peu lisible).
Nous avons vu comment exprimer les motifs élémentaires:
- en spécifiant des caractères directement '
a
', ou précédés par un backslash si ce sont des métacaractères '\.
'; - en indiquant des classes de caractères '
[a-z]
', '[^abc]
' et '.
'; - en précisant le début
^
ou la fin$
de la ligne.
et comment combiner ces motifs élémentaires:
- par simple juxtaposition '
ab
'; - en donnant des alternatives '
a|b
'; - en regroupant des sous-expressions entre parenthèses
(
…)
- par répétition avec les quantificateurs
*
?
+
{}
Nous avons vu que les sous-expressions sont numérotées et qu'on peut récupérer le texte reconnu par une sous-expression lors du remplacement avec $1
Enfin, nous avons vu que les quantificateurs sont gourmands et qu'il faut prendre des précautions pour éviter de reconnaître trop de texte.