Wiki de QtFR.org

Des tutos, des articles et des trucs et astuces

Outils pour utilisateurs

Outils du site


les_expressions_rationnelles

1. Description

Les expressions rationnelles sont un langage formel permettant de décrire d'autres langages formels, et dans notre cas des chaînes de caractères.

  • On les utilise pour repérer des portions de chaîne de caractères (plus ou moins génériques) dans un texte.
  • On peut aussi les utiliser pour remplacer ces portions de chaîne par d'autre.
  • On peut ainsi remplacer tous les mots 'et' d'un texte par l'esperluette (le '&').

Comme il est possible de remplacer une portion de texte par un autre, on peut y ajouter du texte, mais aussi le supprimer (car ce n'est finalement qu'un remplacement par une chaîne vide).

Une expression rationnelle est constituée d'un motif (ou pattern en Anglais). Ce motif est une suite de caractères typographiques décrivant la portion de texte à trouver avant de lui appliquer le traitement.

Encore une petite chose, les informaticiens étant des fainéants (j'en fait largement partie), ils ont tendance à abréger “expression rationnelle” ou “expression régulière”. Comme il sont souvent bilingue yaourt↔anglais, ils utilisent souvent le diminutif du nom anglais “regular expression” qui est : “reg exp” ou “regexp”. On voit assez souvent ceci sans le p final, cela devient donc “regex”. J'ai même vu “rex” utilisée sur certains forums…

Dans le reste du tutoriel, j'utiliserai regex.

2. Créer un motif

2.1. Qu'est ce qu'un motif ?

Il faut comprendre que les regex constituent un langage à part entière, donc certains mots sont porteurs de sens, tout comme les 'if', 'while' et autre 'cout' le sont pour le c++ .
Nous verrons très bientôt ces mots qui ne sont constitués que d'un caractère et sont appelés méta-caractères.

Une regex est décrite par un motif qui est une chaîne de caractères typographiques. Pour prendre une image, le motif de notre expression rationnelle est un peu comme une liste de course… A l'exception des méta-caractères, chaque caractère est pris pour lui-même. Ainsi le motif “chats” ne trouvera que le mots chats dans le texte parcouru. On peut aussi utiliser les caractères spéciaux comme “\n” ou “\t”.

On utilise aussi des sous-motifs. Non, ce ne sont pas des motifs dégénérés c'est une partie du motif, ce sous-motif est entouré par des parenthèses.

Mais alors, on ne peut trouver que des mots bien définis ? Heureusement non…

2.2. Un peu de généricité

Le caractère générique

C'est ici que ça devient intéressant ! Le point (le “.”) désigne n'importe quel caractère sauf le retour à la ligne. Ainsi le motif “b.n” trouvera les mots “ban”, “ben”, “bon” et même les mots “bGn”, “b\tn” (les lettres b et n séparées par une tabulation)… Vous l'aurez compris, le point représente un seul caractère.

Mais comment rechercher un point ?
Bonne question, n'est-ce-pas ? Puisque le point ne sera pas interprété comme un caractère quelconque, comment dire au moteur d'expression rationnelle que l'on recherche spécifiquement un point ? Et bien, on utilise un caractère d'échappement : l'anti-slash, le “\”. Ainsi le motif “\.” trouvera un point.

On ne peut pas être plus précis ?

Les classes de caractères

Oui ! Heureusement.
Il existe ce qu'on appelle les classes de caractères. Il en existe deux sortes :

  • celles incluant des caractères, que j'appellerais simplement “classe”,
  • et celles, vous vous en doutez, excluant des caractères appelées classes complémentées.

Les classes sont définies par une liste de caractères entre crochets : “[ABCDE]” par exemple. Attention tous les caractères entre les crochets sont pris pour ce qu'ils sont, ainsi le point sera considéré comme un point (et non comme le méta-caractère).
Cela signale au moteur qu'il faut rechercher un des caractères dans la liste. Donc, le motif précédent recherchera le caractère “A” ou “B” ou “C” ou “D” ou “E”.

Les classes complémentées sont définies par une liste de caractères entre un crochet ouvrant et un circonflexe, et un crochet fermant : “[^aeiouy]” par exemple.
Le motif précédent trouvera tout caractère à l'exception de ceux de la liste, donc le motif précédent trouvera tous les caractères à l'exception des voyelles minuscules.

Si je veux rechercher une lettre minuscule, je dois taper l'entièreté de l'alphabet ? Non, on peut utiliser des plages de caractères : on donne le premier caractère de la plage, un tiret et le dernier caractère de la plage.
De fait, si je veux trouver une lettre minuscule, il suffit d'utiliser le motif : “[a-z]”.
Il en va de même pour les classes d'exclusion. Par exemple, si l'on veut éviter les chiffres, il suffit d'utiliser le motif : “[^0-9]”. On peut utiliser plusieurs plages dans la même classe : le motif “[a-eA-E]” trouvera toutes les lettres a, b, c, d et e qu'elles soient en majuscule ou en minuscule.

Attention, pour utiliser le tiret ('-') dans une classe, on a l'habitude de le placer en tête de la liste des caractères, contrairement au circonflexe ('^').

Les classes de caractères prédéfinies

Quoi ? On s'est pris la tête avec les classes qu'il faut détailler soi-même alors qu'il y en a des prédéfinies ? Oui, il existe des classes prédéfinies, je vais vous les présenter avant de discuter des avantages et des inconvénients des classes prédéfinies.

Voici les différentes classes définies par les expression rationnelles :

  • [:alpha:] : n'importe quelle lettre
  • [:digit:] : n'importe quel chiffre
  • [:xdigit:] : caractères hexadécimaux
  • [:alnum:] : n'importe quelle lettre ou chiffre
  • [:space:] : n'importe quel espace blanc
  • [:punct:] : n'importe quel signe de ponctuation
  • [:lower:] : n'importe quelle lettre en minuscule
  • [:upper:] : n'importe quelle lettre capitale
  • [:blank:] : espace ou tabulation
  • [:graph:] : caractères affichables et imprimables
  • [:cntrl:] : caractères d'échappement
  • [:print:] : caractères imprimables exceptés ceux de contrôle

Chose promise, chose due ! Voyons les avantages et les inconvénients des classes prédéfinies.
Bien entendu, le principal avantage des classes prédéfinies, c'est justement leur prédéfinition, on a immédiatement un ensemble de caractères dont la définition est connue.
Par contre, l'inconvénient est leur prédéfinition, on veut parfois ne trouver que les cinq première lettres, ou encore uniquement les voyelles… c'est là que les classes que l'on définit sont très utiles.

2.3. Combien ?

Maintenant que nous avons vu comment être “générique” dans nos motifs, nous verrons comment quantifier les caractères.
Qu'appelle-t-on quantifier ? Quantifier permet de dire au moteur d'expression combien de caractère on recherche, parce qu'il est plus “facile” de dire “recherche les groupes de 15 chiffres” plutôt que de dire “recherche les groupes composés d'un chiffre suivi d'un chiffre suivi d'un chiffre suivi d'un chiffre suivi d'un chiffre suivi d'un chiffre suivi d'un chiffre suivi d'un chiffre suivi d'un chiffre suivi d'un chiffre suivi d'un chiffre suivi d'un chiffre suivi d'un chiffre suivi d'un chiffre suivi d'un chiffre”… Non ?
Il existe des méta-caractères de quantification : le “*”, le “+” et le “?”. Ils sont placés juste après le caractère ou groupe de caractères qu'on veut quantifier. “Qu'est ce qu'un groupe de caractère ?” allez-vous me demander, je vous répondrai qu'un groupe de caractères est un sous-motif.

  • Le “*” signifie qu'il peut ou non avoir le caractère (ou groupe de caractère) autant de fois que l'on veut.
  • Le “+” signifie qu'il faut avoir au moins une fois le caractère (ou groupe de caractère).
  • Le “?” signifie qu'il peut ou non avoir une seule fois le caractère (ou groupe de caractère).

Il est possible d'être plus fin dans ces quantifications en utilisant les intervalles de reconnaissances. On peut ainsi définir finement la plage du nombre de fois où l'on veut trouver le caractère, finement jusqu'à donner le nombre exact de fois ! Comment fait-on cela ? En utilisant les accolades (“{” et “}”) entre lesquels on donne les bornes de l'intervalle.
Voici quelques petites précisions : les bornes 0 et infinité n'ont pas besoin d'être écrites (d'ailleurs on ne pourrait pas écrire l'infini) et on n'écrit pas deux fois la même borne (pour donner un nombre exact de fois). Ainsi :

  • wh?é+” trouvera les mots “wé”, “whé”, “wéé”, “wééé”… (en passant par “wéééééééééééééééééééééééééééé” ) et tous ces mêmes mots avec le h entre le 'w' et le premier 'é'.
  • waza{,5}” trouvera les mots “waz”, “waza”, “wazaa”, “wazaaa”, “wazaaaa” et “wazaaaaa” et rien d'autre.
  • tada{5,}” trouvera tous mots “tada” comportant au moins 5 'a' à la fin.
  • tra(la){3}lère” trouvera le mot “tralalalalère” et rien d'autre.

2.4. Choix multiples

Choix multiples ? N'est-ce pas déjà ce qu'on a fait avec les classes ? Oui et non.
Imaginons que nous cherchions toutes les occurrences du son “sh” que l'on peut trouver sous les graphies “ch”, “sh” et “sch”.
En première approche il semble simple de créer le motif : “s?c?h”. Pas bête, mais il y un écueil dans ce motif… Il trouvera bien les graphies recherchées (“ch”, “sh” et “sch”) mais aussi la graphie “h”… et oui… C'est ici que le caractère “|” (le pipe pour les geek, la barre verticale pour les autres) entre en jeu. Comme dans beaucoup de langages (comme le C++), ce caractère marque une alternative. On crée donc un sous-motif dans lequel les alternatives sont séparées par des “|”. De fait, pour trouver nos graphie du sont “ch”, le motif sera “(c|s|sc)h

2.5. Les ancrages

Que sont les ancrages ? Ce sont des méta-caractères permettant de situer le motif dans la chaîne contrôlée.
On peut demander à placer le motif au début (avec le circonflexe '^') ou à la fin (avec le dollar '$') de la chaîne.

Voici une chaîne : “Avec Qt, j'utilise les expressions rationnelles et j'y arrive”.

  • le motif “[aA]” y trouvera le “A” de “Avec” et les “a” de “rationnelles” et de “arrive” ;
  • le motif “^[aA]” n'y trouvera que le “A” de “Avec” ;
  • le motif “[aA]$” n'y trouvera rien !

2.6. Les assertions

Une assertion, c'est une condition que l'on spécifie. Vous l'avez remarqué, les ancrages sont des assertions, des assertions simples. On peut définir des insertions plus complexes. Il faut alors créer des motifs en signalant au moteur d'expressions rationnelles de les vérifier sans les capturer. On a alors quatre cas :

  • assertion avant positive, définie avec “(?=motif)” : la caractère est suivi du motif.
  • assertion avant négative, définie avec “(?!motif)” : la caractère n'est pas suivi du motif.
  • assertion arrière positive, définie avec “(?< =motif)” : la caractère est précédé du motif 1).
  • assertion arrière négative, définie avec “(?<!motif)” : la caractère n'est pas précédé suivi du motif.

2.7. Utiliser les sous-motifs

Vous vous souvenez que je vous ai parlé des sous-motifs ?
Mais si, ces groupes de caractères encadrés par des parenthèses ! Oui ? Et bien sachez que l'on peut utiliser, dans le motif en cours, les sous-motifs capturés.
Comment ? En les appelant par leur numéro par : “\N” où N est un numéro de 1 à 9. Ce numéro est attribué à chaque parenthèse ouvrante (en dehors des assertions). Ainsi, dans le motif “[a-z](\.([a-z]{2,3})){4}([0-9]){1,3}-([0-9]{1,2})” :

  • \1” correspond au sous-motif “\.([a-z]{2,3})
  • \2” correspond au sous-motif “[a-z]{2,3}
  • \3” correspond au sous-motif “[0-9]{1,2}” (en fait le dernier chiffre capturé)
  • \4” à “\9” ne sont pas définis.

3. Qt et les expressions régulières

3.1. Bref aperçu des classes disponibles

Qt propose plusieurs classes permettant de manipuler les expressions régulières : QRegExp, QRegularExpression, QRegularExpressionMatch, QRegularExpressionMatchIterator et QRegularExpressionValidator.

QRegExp et QRegularExpression

Les classes QRegExp et QRegularExpression fournissent un mécanisme de comparaison à un modèle basé sur les expressions régulières.

Nous l'avons évoqué précédemment, les classes QRegExp et QRegularExpression vont permettre plusieurs mécanismes :

  • La validation ;
  • La recherche ;
  • Le remplacement (précédé d'une recherche) ;
  • La séparation d'une chaîne de caractères (dans le cas de l’interprétation d'un fichier CSV par exemple)…

Le moteur d'expressions rationnelles de Qt est basé sur le moteur inclut au langage Perl. Il supporte entièrement l'Unicode. Il peut également être utilisé dans un mode “plus simple”, similaire aux fonctionnalités disponibles dans les commandes shells. En particulier, la syntaxe du motif peut être fixée à QRegExp::FixedString, ce qui signifie que le motif est interprété comme une chaîne simple, donc les caractères spéciaux (comme les backslash, ou les points) n'y sont pas échappés.

Voici la façon dont Qt encode les différents éléments de motif.

Élément Signification
\C Un caractère précédé d'un backslash est “échappé”, un caractère spécial est donc compris par le moteur pour le caractère lui même.
\a Correspond au caractère BEL, de code ASCII 0x07.
\f Correspond au caractère form feed, de code ASCII 0x0C.
\n Correspond au caractère line feed, de code ASCII 0x0A, Unix newline.
\r Correspond au caractère carriage return, de code ASCII 0x0D.
\t Correspond au caractère horizontal tab, de code ASCII 0x09.
\v Correspond au caractère vertical tab, de code ASCII 0x0B.
\xhhhh Correspond au caractère Unicode de code hexadécimal hhhh (entre 0x0000 et 0xFFFF).
\0ooo Correspond au caractère ASCII/Latin1 de code octal ooo (entre 0000 et 0377).
. Correspond à n'importe quel caractère (ceci inlcue le retour à la ligne).
\d Correspond à un nombre (QChar::isDigit()), c'est la classe [:digit:].
\D Correspond à la négation de \d.
\s Correspond à un caractère d'espacement (QChar::isSpace()), c'est la classe [:space:].
\S Correspond à la négation de \s.
\w Correspond à un caractère de mot (QChar::isLetterOrNumber(), QChar::isMark() ou '_').
\W Correspond à la négation de \w.
\N Correspond à un N-ième sous-motif.

Note: Le compilateur C++ transforme les backslashs en chaîne brute. Pour inclure un backslah dans un motif, il faut le doubler (donc transformer les \ en \\). Pour rechercher le backslash lui-même, il faut l'entrer quatre fois (donc rechercher le motif \\\\).

QRegularExpressionMatch et QRegularExpressionMatchIterator

La classe QRegularExpressionMatch permet l'accès aux résultats d'une correspondance entre un motif d'expression rationnelle et une chaîne de caractères. Un objet de type QRegularExpressionMatch peut être obtenu grâce à l'appel de la fonction QRegularExpression::match() ou via un itérateur (de type QRegularExpressionMatchIterator) résultant de l'appel à la fonction QRegularExpression::globalMatch().
Le succès ou l'échec d'une correspondance devrait être testé par l'appel à la fonction hasMatch() de la classe QRegularExpressionMatch, on peut également avoir le retour d'une correspondance partielle avec l'appel à la fonction hasPartialMatch().

De plus, QRegularExpressionMatch renvoie les chaînes capturées par les sous-motifs (vous savez, les groupes créés par les parenthèses dans le modèle). Le groupe d'indice 0 correspond à la chaîne correspondant entièrement au modèle. La méthode captured() renvoie chaque groupe capturé, soit via son index, soit via son nom :

QRegularExpression re("(\\d\\d) (?<name>\\w+)");
QRegularExpressionMatch match = re.match("23 Jordan");
if (match.hasMatch())
{
    QString number = match.captured(1); // first == "23"
    QString name = match.captured("name"); // name == "Jordan"
}

Pour chaque groupe capturé, il est possible d'obtenir l'indice du début et de fin, ainsi que la longueur de ce groupe grâce aux fonctions capturedStart(), capturedEnd() et capturedLength().

L'appel à la méthode regularExpression() renverra le motif (donc le sous-motif du motif global) ayant permis la capture du groupe. Le type et les options de correspondance sont également disponibles via les méthodes matchType() et matchOptions().

La classe QRegularExpressionMatchIterator fournit un itérateur pour parcourir les différentes captures obtnues par globalMatch(). Un nouvel itérateur est positionné avant le premier résultat. On vérifie la présence d'une prochaine capture avec la méthode hasNext(), capture que l'on obtiendra (avant de faire avancer l'itérateur) avec la méthode next(). Chaque capture est un object de type QRegularExpressionMatch contenant toutes les informations.
De plus, QRegularExpressionMatchIterator possède la méthode peekNext() qui renvoie la capture suivante sans fairte avancer l'itérateur.

QRegularExpressionValidator

La classe QRegularExpressionValidator est utilisée pour contrôler une chaîne de caractère à l'aide d'une expression rationnelle (qui peut être forunie à la construction ou plus tard). Elle va déterminer si la chaîne donnée en entrée est Aceptable, Intermediate ou Invalid.

Si une chaîne correspond partiellement au motif, la validation est considérée comme Intermediate.
Par exemple, les chaînes “” et “A” sont Intermediate pour le motif [A-Z][0-9], là ou “_” serait Invalid.
QRegularExpressionValidator va toujours chercher une correspondance parfaite.

Exemple d'utilisation :

// regexp: un - optionnel suivi de 1 à 3 chiffres
QRegularExpression rx("-?\\d{1,3}");
QValidator *validator = new QRegularExpressionValidator(rx, this);

QLineEdit *edit = new QLineEdit(this);
edit->setValidator(validator);

Voici quelques utilisations de validateurs, qui seront associés à un widget comme ci-dessus.

// entiers de 1 à 9999
QRegularExpression re("[1-9]\\d{0,3}");
// le validateur considère le motif comme le suivant "^[1-9]\\d{0,3}$"
QRegularExpressionValidator v(re, 0);
QString s;
int pos = 0;

s = "0";     v.validate(s, pos); // renvoie Invalid
s = "12345"; v.validate(s, pos); // renvoie Invalid
s = "1";     v.validate(s, pos); // renvoie Acceptable

// au moins un caractère qui ne soit pas un caractère d'espacement
re.setPattern("\\S+");
v.setRegularExpression(re);
s = "myfile.txt";  v.validate(s, pos); // renvoie Acceptable
s = "my file.txt"; v.validate(s, pos); // renvoie Invalid

// A, B ou C suivi par exactement cinq chiffres suivi par W, X, Y ou Z
re.setPattern("[A-C]\\d{5}[W-Z]");
v.setRegularExpression(re);
s = "a12345Z"; v.validate(s, pos); // renvoie Invalid
s = "A12345Z"; v.validate(s, pos); // renvoie Acceptable
s = "B12";     v.validate(s, pos); // renvoie Intermediate

// recherche la plupart des fichier "readme"
re.setPattern("read\\S?me(\.(txt|asc|1st))?");
re.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
v.setRegularExpression(re);
s = "readme";      v.validate(s, pos); // renvoie Acceptable
s = "README.1ST";  v.validate(s, pos); // renvoie Acceptable
s = "read me.txt"; v.validate(s, pos); // renvoie Invalid
s = "readm";       v.validate(s, pos); // renvoie Intermediate
1) sans l'espace entre le < et le =
les_expressions_rationnelles.txt · Dernière modification: 2015/10/16 19:12 par myrddin772