Expressions rapides et régulières syntaxe

1. Introduction

En termes simples, les expressions régulières (regexes ou regexps en abrégé) sont un moyen de spécifier des modèles de chaîne. Vous connaissez sans aucun doute la fonction de recherche et remplacement dans votre éditeur de texte préféré ou dans votre IDE. Vous pouvez rechercher des mots et des expressions exactes. Vous pouvez également activer des options, telles que l'insensibilité à la casse, de sorte qu'une recherche du mot "couleur" trouve également "Couleur", "COULEUR" et "CoLoR". Mais que se passerait-il si vous vouliez rechercher les variantes orthographiques du mot "couleur" (orthographe américaine: couleur, orthographe britannique: couleur) sans avoir à effectuer deux recherches distinctes?

Si cet exemple semble trop simple, que diriez-vous de chercher toutes les variantes orthographiques du nom anglais "Katherine" (Catherine, Katharine, Kathreen, Kathryn, etc., pour en nommer quelques-unes). Plus généralement, vous pouvez rechercher dans un document toutes les chaînes qui ressemblent à des nombres hexadécimaux, des dates, des numéros de téléphone, des adresses électroniques, des numéros de carte de crédit, etc..

Les expressions régulières sont un moyen puissant de résoudre (partiellement ou totalement) ces problèmes pratiques (et bien d’autres) impliquant le texte..

Contour

La structure de ce tutoriel est la suivante. Je présenterai les concepts de base que vous devez comprendre en adaptant une approche utilisée dans les manuels théoriques (après avoir éliminé toute rigueur ou pédantisme inutile). Je préfère cette approche car elle vous permet de mettre en perspective environ 70% des fonctionnalités dont vous aurez besoin, dans le contexte de quelques principes de base. Les 30% restants sont des fonctionnalités plus avancées que vous pourrez apprendre plus tard ou ignorer, à moins que vous ne souhaitiez devenir un maestro regex.

La syntaxe associée aux expressions régulières est très volumineuse, mais la plupart d'entre elles sont simplement là pour vous permettre d'appliquer les idées de base le plus succinctement possible. Je vais les présenter progressivement, plutôt que de laisser tomber une grande table ou une liste à mémoriser.

Au lieu de nous lancer directement dans une implémentation rapide, nous explorerons les bases à travers un excellent outil en ligne qui vous aidera à concevoir et à évaluer des expressions régulières avec un minimum de frictions et de bagages inutiles. Une fois que vous maîtrisez les idées principales, écrire du code Swift est essentiellement un problème de correspondance entre votre compréhension et l'API Swift..

Nous essaierons tout au long de garder un état d’esprit pragmatique. Les expressions régulières ne sont pas le meilleur outil pour chaque situation de traitement de chaîne. En pratique, nous devons identifier les situations dans lesquelles les expressions rationnelles fonctionnent très bien et les situations dans lesquelles elles ne fonctionnent pas. Il existe également un moyen terme dans lequel les expressions rationnelles peuvent être utilisées pour effectuer une partie du travail (généralement du prétraitement et du filtrage) et le reste du travail laissé à la logique algorithmique..

Concepts de base

Les expressions régulières ont leur fondement théorique dans la "théorie du calcul", l'un des sujets étudiés par l'informatique, où elles jouent le rôle d'input appliqué à une classe spécifique de machines à calculer abstraites appelées automates finis..

Détendez-vous, cependant, vous n'êtes pas obligé d'étudier le contexte théorique pour utiliser les expressions régulières de manière pratique. Je ne les mentionne que parce que l'approche que je vais utiliser pour motiver les expressions rationnelles à partir de la base reflète l'approche utilisée dans les manuels d'informatique pour définir des expressions régulières "théoriques"..

En supposant que vous maîtrisiez bien la récursivité, j'aimerais que vous gardiez à l'esprit la définition des fonctions récursives. Une fonction est définie en termes de versions plus simples d'elle-même et, si vous suivez une définition récursive, vous devez vous retrouver dans un cas de base explicitement défini. Je soulève cette question parce que notre définition ci-dessous va être récursive aussi.

Notez que lorsque nous parlons de chaînes en général, nous avons implicitement un jeu de caractères, tel que ASCII, Unicode, etc. Supposons pour le moment que nous vivons dans un univers dans lequel les chaînes sont composées des 26 lettres de minuscules. alphabet (a, b,… z) et rien d'autre.

Règles

Nous commençons par affirmer que chaque caractère de cet ensemble peut être considéré comme une expression régulière qui correspond à une chaîne. Alors une comme une expression régulière correspond à "a" (considéré comme une chaîne), b est une regex correspondant à la chaîne "b", etc. Disons aussi qu'il y a une expression régulière "vide" Ɛ qui correspond à la chaîne vide "". Ces cas correspondent aux "cas de base" triviaux de la récursivité.

Maintenant, considérons les règles suivantes qui nous aident à créer de nouvelles expressions régulières à partir d’existantes existantes:

  1. le enchaînement (c'est-à-dire "chaîner ensemble") de deux expressions régulières quelconques est une nouvelle expression régulière qui correspond à la concaténation de deux chaînes quelconques correspondant aux expressions régulières d'origine.
  2. le alternance de deux expressions régulières est une nouvelle expression régulière qui correspond à l'une des deux expressions régulières d'origine.
  3. le Kleene star d'une expression régulière correspond à zéro ou plusieurs instances adjacentes de ce qui correspond à l'expression régulière d'origine.

Faisons ce concret avec plusieurs exemples simples avec nos chaînes alphabétiques.

Exemple 1

De la règle 1, une et b étant des expressions régulières correspondant à "a" et "b", signifie un B est une expression régulière qui correspond à la chaîne "ab". Puisque un B et c sont des expressions régulières, abc est une expression régulière correspondant à la chaîne "abc", etc. En continuant ainsi, nous pouvons créer de longues expressions régulières arbitraires qui correspondent à une chaîne avec des caractères identiques. Rien d'intéressant n'est encore arrivé.

Exemple 2

À partir de la règle 2, o et une être des expressions régulières, o | a correspond à "o" ou "a". La barre verticale représente l'alternance. c et t sont des expressions régulières et, combiné avec la règle 1, nous pouvons affirmer que c (o | a) t est une expression régulière. Les parenthèses sont utilisées pour regrouper.

À quoi ça correspond? c et t ne correspondent qu'à eux-mêmes, ce qui signifie que la regex c (o | a) t correspond à "c" suivi d'un "a" ou d'un "o" suivi de "t", par exemple, la chaîne "cat" ou "cot". Notez que ça fait ne pas Match "manteau" comme o | a correspond uniquement à "a" ou "o", mais pas les deux à la fois. Maintenant, les choses commencent à devenir intéressantes.

Exemple 3

De la règle 3, une* correspond à zéro ou plusieurs instances de "a". Il correspond à la chaîne vide ou aux chaînes "a", "aa", "aaa", etc. Exercons cette règle en conjonction avec les deux autres règles.

Qu'est-ce que chaud rencontre? Il correspond à "ht" (avec zéro instances de "o"), "hot", "hoot", "hooot", etc. Qu'en est-il de b (o | a) *? Il peut correspondre à "b" suivi par un nombre quelconque d'instances de "o" et "a" (y compris aucune). "b", "boa", "baa", "bao", "baooaoaoaoo" ne sont que quelques exemples du nombre infini de chaînes de caractères auxquelles cette expression régulière correspond. Notez à nouveau que les parenthèses sont utilisées pour regrouper la partie de l’expression régulière à laquelle le * est appliqué.

Exemple 4

Essayons de découvrir des expressions régulières qui correspondent aux chaînes que nous avons déjà en tête. Comment pourrions-nous faire une expression régulière reconnaissant le bluff des moutons, ce que je considérerai comme autant de répétitions du son de base "baa" ("baa", "baabaa", "baabaabaa", etc.)

Si vous avez dit, (bêlement)*, alors vous avez presque raison. Mais notez que cette expression régulière correspondrait également à la chaîne vide, ce que nous ne souhaitons pas. En d'autres termes, nous voulons ignorer les moutons qui ne baissent pas. baa (baa) * est l'expression régulière que nous recherchons. De même, une vache mugissant pourrait être moo (moo) *. Comment pouvons-nous reconnaître le son de l'un ou l'autre animal? Simple. Utilisez l'alternance. baa (baa) * | moo (moo) *

Si vous avez compris les idées ci-dessus, félicitations, vous êtes sur la bonne voie..

2. Questions de syntaxe

Rappelons que nous avons placé une restriction stupide sur nos chaînes. Ils ne peuvent être composés que de lettres minuscules de l'alphabet. Nous allons maintenant supprimer cette restriction et considérer toutes les chaînes composées de caractères ASCII.

Nous devons comprendre que, pour que les expressions régulières soient un outil pratique, elles doivent elles-mêmes être représentées sous forme de chaînes. Ainsi, contrairement à ce qui était le cas auparavant, nous ne pouvons plus utiliser de caractères tels que *, |, (, ), etc. sans indiquer en quelque sorte si nous les utilisons comme des caractères "spéciaux" représentant l'alternance, le groupement, etc. ou si nous les traitons comme des caractères ordinaires qui doivent être appariés littéralement.

La solution consiste à traiter ceux-ci et d’autres "métacaractères" pouvant avoir une signification particulière. Pour basculer d’un usage à l’autre, nous devons pouvoir leur échapper. Ceci est similaire à l'idée d'utiliser "\ n" (échappant le n) pour indiquer une nouvelle ligne dans une chaîne. Il est légèrement plus compliqué en ce sens que, selon le caractère de contexte qui est habituellement "méta", peut représenter son moi littéral sans échappement. Nous verrons des exemples plus tard..

Une autre chose que nous valorisons est la concision. Beaucoup d'expressions régulières qui peuvent être exprimées en n'utilisant que la notation de la section précédente seraient fastidieusement verbeuses. Par exemple, supposons que vous souhaitiez simplement rechercher les deux chaînes de caractères composées d'une lettre minuscule suivie d'un chiffre (par exemple, des chaînes telles que "a0", "b9", "z3", etc.). En utilisant la notation que nous avons discutée plus tôt, cela donnerait l'expression régulière suivante:

(a | b | c | d | e | f | g | h | i | j | k | l | m | n | o | p | q | r | s | t | u | v | w | x | y | z) (0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9)

Il suffit de taper ce monstre m'a anéanti.

Ne pas [abcdefghijklmnopqrstuvwxyz] [0123456789] ressembler à une meilleure représentation? Notez les métacaractères [ et ] qui signifient un ensemble de caractères, dont l’un donne une correspondance positive. En fait, si nous considérons que les lettres a à z, et les chiffres 0 à 9 apparaissent en séquence dans l’ensemble ASCII, nous pouvons atténuer la regex jusqu’à ce qu’elle soit froide. [a-z] [0-9].

Dans les limites d'un jeu de caractères, le tiret, -, est un autre métacaractère indiquant une plage. Notez que vous pouvez insérer plusieurs plages dans la même paire de crochets. Par exemple, [0-9a-zA-Z] peut correspondre à n'importe quel caractère alphanumérique. le 9 et une (et  z et UNE)serrer les uns contre les autres peut sembler drôle, mais souvenez-vous que les expressions régulières parlent de brièveté et que leur signification est claire.

Parlant de brièveté, il existe des moyens encore plus concis de représenter certaines classes de personnages apparentés, comme nous le verrons dans une minute. Notez que la barre d'alternance, |, est toujours une syntaxe valide et utile comme nous le verrons dans un instant.

Plus de syntaxe

Avant de commencer à pratiquer, examinons un peu plus la syntaxe.

Période

La période, ., correspond à n'importe quel caractère, à l'exception des sauts de ligne. Cela signifie que c.t peut correspondre à "chat", "crt", "c9t", "c% t", "c.t", "c t", etc. Si nous voulions faire correspondre la période à un caractère ordinaire, par exemple, pour faire correspondre la chaîne "c.t", nous pourrions soit y échapper (c \ .t) ou le placer dans une classe de caractères qui lui est propre (c [.] t).

En général, ces idées s’appliquent à d’autres métacaractères, tels que [, ], (, )*, et d'autres que nous n'avons pas encore rencontrés.

Parenthèses

Parenthèses (( et )) sont utilisés pour grouper comme nous l’avons vu auparavant. Nous allons utiliser le mot jeton pour signifier un seul caractère ou une expression entre parenthèses. La raison en est que de nombreux opérateurs de regex peuvent être appliqués à.

Les parenthèses sont également utilisées pour définir groupes de capture, vous permettant de savoir quelle partie de votre match était capturé par un groupe de capture particulier dans la regex. Je parlerai plus tard de cette fonctionnalité très utile.

Plus

UNE + Après un jeton, une ou plusieurs instances de ce jeton. Dans notre exemple de bêlement de moutons, baa (baa) * pourrait être représenté plus succinctement (bêlement)+. Rappeler que * signifie zéro ou plusieurs occurrences. Notez que (bêlement)+ est différent de bêlement+, parce que dans l'ancien le + est appliqué à la bêlement gage alors que dans ce dernier cas, il s’applique uniquement aux une avant cela. Dans ce dernier cas, il correspond à des chaînes telles que "baa", "baaa" et "baaaa".

Point d'interrogation

UNE ? le fait de suivre un jeton signifie zéro ou une instance de ce jeton.

Entraine toi

RegExr est un excellent outil en ligne pour expérimenter des expressions régulières. Lorsque vous êtes à l'aise pour lire et écrire des expressions régulières, il sera beaucoup plus facile d'utiliser l'API d'expression régulière du framework Foundation. Même dans ce cas, il sera plus facile de tester d'abord votre expression régulière en temps réel sur le site Web..

Visitez le site Web et concentrez-vous sur la partie principale de la page. Voici ce que vous verrez:

Vous entrez une expression régulière dans la zone située en haut et entrez le texte dans lequel vous recherchez des correspondances..

Le "/ g" à la fin de la zone d’expression ne fait pas partie de l’expression régulière en soi. Il s'agit d'un indicateur qui affecte le comportement global du moteur de regex en matière de correspondance. En ajoutant "/ g" à l'expression régulière, le moteur de recherche recherche toutes les correspondances possibles de l'expression régulière dans le texte, ce qui correspond au comportement souhaité. La surbrillance bleue indique une correspondance. Survoler l'expression régulière avec votre souris est un moyen pratique de vous rappeler le sens de ses éléments constitutifs.

Sachez que les expressions régulières se présentent sous différentes formes, en fonction de la langue ou de la bibliothèque que vous utilisez. Cela signifie non seulement que la syntaxe peut être légèrement différente entre les différentes variantes, mais également sur les capacités et les fonctionnalités. Swift, par exemple, utilise la syntaxe de modèle spécifiée par ICU. Je ne sais pas quelle version est utilisée dans RegExr (qui fonctionne avec JavaScript), mais dans le cadre de ce tutoriel, ils sont assez similaires, voire identiques..

Je vous encourage également à explorer le volet de gauche, qui contient de nombreuses informations présentées de manière concise..

Notre premier exemple pratique

Pour éviter toute confusion potentielle, je devrais mentionner que, lorsque vous parlez de correspondance d'expression régulière, nous pourrions vouloir dire l'une des deux choses suivantes:

  1. rechercher toutes les sous-chaînes d'une chaîne qui correspondent à une expression régulière
  2. vérifier si la chaîne complète correspond ou non à l'expression régulière

La signification par défaut avec laquelle les moteurs de regex fonctionnent est (1). Ce dont nous avons parlé jusqu'à présent est (2). Heureusement, il est facile de mettre en œuvre le sens (2) au moyen de métacaractères qui seront introduits ultérieurement. Ne t'en fais pas pour l'instant.

Commençons simplement en testant notre exemple de bêlement des moutons. Type (bêlement)+ dans la zone d'expression et quelques exemples pour tester les correspondances, comme indiqué ci-dessous.

J'espère que vous comprenez pourquoi les matchs qui ont réussi ont réellement abouti et pourquoi les autres ont échoué. Même dans cet exemple simple, il y a quelques points intéressants à souligner.

Allumettes gourmandes

La chaîne "baabaa" contient-elle deux correspondances ou une seule? En d'autres termes, chaque "baa" est-il un match ou l'ensemble du "baabaa" est-il un match? Cela revient à déterminer si un "match gourmand" est recherché ou non. Un match gourmand tente de faire correspondre autant de chaîne que possible.

À l'heure actuelle, le moteur des expressions rationnelles correspond goulûment, ce qui signifie que "baabaa" est un match simple. Il y a moyen de faire des correspondances paresseuses, mais c'est un sujet plus avancé et, puisque nos plaques sont déjà pleines, nous ne couvrirons pas cela dans ce tutoriel..

L'outil RegExr laisse une petite mais perceptible espace dans la mise en surbrillance si deux parties adjacentes d'une chaîne chacune individuellement (mais pas collectivement) correspondent à l'expression régulière. Nous verrons un exemple de ce comportement dans un peu.

Majuscule et minuscule

"Baabaa" échoue à cause de la majuscule "B". Supposons que vous vouliez que seul le premier "B" soit en majuscule, quelle serait l'expression régulière correspondante? Essayez de comprendre par vous-même d'abord.

Une réponse est (B | b) aa (baa) *. Cela aide si vous le lisez à haute voix. "B" majuscule ou minuscule, suivi de "aa", suivi de zéro ou plusieurs instances de "baa". C'est réalisable, mais notez que cela pourrait rapidement devenir gênant, surtout si nous voulions ignorer complètement la capitalisation. Par exemple, nous aurions à spécifier des remplaçants pour chaque cas, ce qui donnerait quelque chose de difficile à manier comme ([Bb] [Aa] [Aa])+.

Heureusement, les moteurs d'expression régulière ont généralement une option pour ignorer la casse. Dans le cas de RegExr, cliquez sur le bouton "drapeaux" et cochez la case "ignorer la casse". Notez que la lettre "i" est ajoutée à la liste d'options à la fin de l'expression régulière. Essayez quelques exemples avec des lettres en majuscules, telles que "bAABaa".

Un autre exemple

Essayons de concevoir une expression régulière capable de capturer des variantes du nom "Katherine". Comment aborderiez-vous ce problème? J'écrirais autant de variations, regarderais les parties communes, puis essayer d'exprimer en mots les variations (en mettant l'accent sur les lettres de remplacement et optionnelles) sous forme de séquence. Ensuite, je tenterais de formuler l’expression régulière qui assimile toutes ces variations.

Essayons-le avec cette liste de variantes: Katherine, Katharine, Catherine, Kathreen, Kathleen, Katryn et Catrin. Je vous laisse le soin d'en écrire plusieurs autres si vous le souhaitez. En regardant ces variations, je peux approximativement dire que:

  • le nom commence par "k" ou "c"
  • suivi de "at"
  • suivi éventuellement d'un "h"
  • éventuellement suivi d'un "a" ou d'un "e"
  • suivi d'un "r" ou d'un "l"
  • suivi de "i", "ee" ou "y"
  • et définitivement suivi d'un "n"
  • éventuellement un "e" à la fin

Avec cette idée en tête, je peux arriver à l'expression régulière suivante:

[kc] ath? [ae]? (r | l) (i | ee | y) ne?

Notez que la première ligne "KatherineKatharine" a deux correspondances sans aucune séparation entre elles. Si vous le regardez de près dans l'éditeur de texte de RegExr, vous pouvez observer une petite rupture dans la mise en surbrillance entre les deux correspondances, ce dont je parlais plus tôt..

Notez que l'expression régulière ci-dessus correspond également à des noms que nous n'avons pas pris en compte et qui pourraient même ne pas exister, par exemple, "Cathalin". Dans le contexte actuel, cela ne nous affecte pas du tout négativement. Mais dans certaines applications, telles que la validation de courrier électronique, vous souhaitez être plus précis quant aux chaînes que vous faites correspondre et à celles que vous refusez. Cela ajoute généralement à la complexité de l'expression régulière.

Plus de syntaxe et d'exemples

Avant de passer à Swift, j'aimerais aborder quelques aspects supplémentaires de la syntaxe des expressions régulières..

Représentations concises

Plusieurs classes de personnages liés ont une représentation concise:

  • \ w caractère alphanumérique, soulignement compris, équivalent à [a-zA-Z0-9_]
  • \ré représente un chiffre, équivalent à [0-9]
  • \ s représente un espace, c'est-à-dire un espace, une tabulation ou un saut de ligne

Ces classes ont également des classes négatives correspondantes:

  • \ W représente un caractère non alphanumérique, non souligné
  • \RÉ un non-chiffre
  • \ S un caractère non-espace

Rappelez-vous les classes non capitalisées, puis rappelez-vous que la classe en majuscule correspondante correspond à ce que la classe non capitalisée ne correspond pas. Notez que ceux-ci peuvent être combinés en incluant des crochets internes si nécessaire. Par exemple, [\ s \ s] représente n'importe quel caractère, y compris les sauts de ligne. Rappelons que la période . correspond à n'importe quel caractère sauf les sauts de ligne.

Les ancres

^ et $ sont des ancres qui représentent respectivement le début et la fin d'une chaîne. Rappelez-vous que j'ai écrit que vous pourriez vouloir faire correspondre une chaîne entière, plutôt que de chercher des correspondances de sous-chaîne? Voici comment tu fais ça. ^ c [oau] t $ correspond à "chat", "lit" ou "coupe", mais pas, par exemple, "attraper" ou "recouper".

Limites des mots

\ b représente une limite entre les mots, par exemple en raison d'espace ou de ponctuation, ainsi que le début ou la fin de la chaîne. Notez qu'il est un peu différent en ce sens qu'il correspond à une position plutôt qu'à un caractère explicite. Il peut être utile de penser à une limite de mot en tant que séparateur invisible qui sépare un mot du mot précédent / du mot suivant. Comme vous vous en doutez, \ B représente "pas une limite de mot". \ bcat \ b trouve des correspondances dans "chat", "un chat", "bonjour, chat", mais pas dans "acat" ou "catch".

Négation

L’idée de négation peut être plus précise en utilisant le ^ métacaractère à l'intérieur d'un jeu de caractères. Ceci est une utilisation complètement différente de ^ à partir de "début de l'ancre de chaîne". Cela signifie que, pour la négation, ^ doit être utilisé dans un jeu de caractères dès le début. [^ a] correspond à tout caractère autre que la lettre "a" et [^ a-z] correspond à n'importe quel caractère sauf une lettre minuscule.

Pouvez-vous représenter \ W en utilisant la négation et les plages de caractères? La réponse est [^ A-Za-z0-9_]. Qu'est-ce que tu penses [a ^] allumettes? La réponse est soit un "a", soit un "^", car il ne s'est pas produit au début du jeu de caractères. Ici "^" se correspond littéralement.

Alternativement, nous pourrions y échapper explicitement comme ceci: [\ ^ a]. Espérons que vous commencez à développer une certaine intuition sur la façon dont fonctionne l'évasion.

Quantificateurs

Nous avons vu comment * (et +) peut être utilisé pour faire correspondre un zéro ou plus (et une ou plusieurs) fois. Cette idée de faire correspondre un jeton plusieurs fois peut être plus précise en utilisant des quantificateurs entre accolades. Par exemple, 2, 4  signifie deux à quatre correspondances du jeton précédent. 2, signifie deux matches ou plus et 2 signifie exactement deux matches.

Nous examinerons des exemples détaillés qui utilisent la plupart de ces éléments dans le prochain tutoriel. Mais pour des raisons pratiques, je vous encourage à créer vos propres exemples et à tester la syntaxe que nous venons de voir avec l'outil RegExr..

Conclusion

Dans ce tutoriel, nous nous sommes principalement concentrés sur la théorie et la syntaxe des expressions régulières. Dans le prochain tutoriel, nous ajoutons Swift au mélange. Avant de continuer, assurez-vous de bien comprendre ce que nous avons couvert dans ce tutoriel en jouant avec RegExr.