Des expressions régulières lisibles sans perdre leur pouvoir?

77

De nombreux programmeurs connaissent la joie de créer une expression régulière rapide, de nos jours souvent avec l'aide d'un service Web, ou plus traditionnellement à l'aide d'une invite interactive, ou peut-être même d'écrire un petit script dont l'expression régulière est en cours de développement et une collection de cas de test. . Dans les deux cas, le processus est itératif et assez rapide: continuez à pirater la chaîne d'aspect cryptique jusqu'à ce qu'elle corresponde et capture ce que vous voulez et rejette ce que vous ne voulez pas.

Pour un cas simple, le résultat pourrait ressembler à ceci, sous la forme d'une expression rationnelle Java:

Pattern re = Pattern.compile(
  "^\\s*(?:(?:([\\d]+)\\s*:\\s*)?(?:([\\d]+)\\s*:\\s*))?([\\d]+)(?:\\s*[.,]\\s*([0-9]+))?\\s*$"
);

De nombreux programmeurs savent également qu'il est pénible de devoir modifier une expression régulière ou simplement coder autour d'une expression régulière dans une base de code héritée. Avec un peu d’édition pour le séparer, regexp est toujours très facile à comprendre pour ceux qui sont familiarisés avec les expressions rationnelles, et un vétéran de l’expression rationnelle devrait immédiatement voir ce qu’il fait (réponse à la fin du message, au cas où quelqu'un voudrait l'exercice de le découvrir eux-mêmes).

Cependant, les choses n'ont pas besoin d'être beaucoup plus complexes pour qu'une expression rationnelle devienne vraiment une chose en écriture seule, et même avec une documentation diligente (ce que tout le monde fait bien sûr pour tous les expressions rationnelles complexes qu'ils écrivent ...), la modification des expressions rationnelles devient une tâche ardue. tâche ardue. Cela peut aussi être une tâche très dangereuse si l'expression rationnelle n'est pas testée avec soin (mais tout le monde a bien sûr des tests unitaires complets pour toutes leurs expressions rationnelles complexes, qu'elles soient positives ou négatives ...).

Donc, bref, existe-t-il une solution / alternative d'écriture-lecture pour les expressions régulières sans perdre leur pouvoir? À quoi ressemblerait l'expression rationnelle ci-dessus avec une approche alternative? N'importe quelle langue convient, bien qu'une solution multilingue soit préférable, dans la mesure où les expressions rationnelles sont multilingues.


Et ensuite, ce que fait l’expression rationnelle précédente est la suivante: analyser une chaîne de nombres au format 1:2:3.4, en capturant chaque nombre, où les espaces sont autorisés et 3obligatoires.

Hyde
la source
2
chose liée sur SO: stackoverflow.com/a/143636/674039
wim
24
Lire / éditer des expressions rationnelles est en fait trivial si vous savez ce qu’elles sont censées capturer. Vous avez peut-être entendu parler de cette fonctionnalité rarement utilisée dans la plupart des langues, appelée "commentaires". Si vous n'en mettez pas au-dessus d'une expression rationnelle complexe, vous en paierez le prix plus tard. En outre, la révision du code.
TC1
2
Deux options pour nettoyer cela sans le casser en plus petits morceaux. Leur présence ou leur absence varie d'une langue à l'autre. (1) regex de lignes étendues, où les espaces blancs dans les regex sont ignorés (sauf s'ils sont échappés) et un formulaire de commentaire d'une seule ligne est ajouté, afin que vous puissiez le scinder en blocs logiques avec indentation, interligne et commentaires. (2) des groupes de capture nommés, dans lesquels vous pouvez attribuer un nom à chaque parenthèse, ce qui ajoute une auto-documentation et remplit automatiquement un hachage de correspondances - bien meilleur qu’un tableau de correspondances indexé numériquement ou des variables $ N.
Ben Lee
3
Une partie du problème tient au langage des expressions rationnelles lui-même et aux mauvais choix historiques dans sa conception, qui traînent comme des bagages. Dans un langage sain, les parenthèses de regroupement sont purement un moyen syntaxique de façonner l’arbre d’analyse. Mais dans les implémentations de regex remontant à Unix, ils ont une sémantique: reliant les registres aux correspondances de sous-expression. Vous avez donc besoin de crochets plus compliqués et plus laids pour obtenir un regroupement pur!
Kaz
2
Pas vraiment une réponse pratique, mais il peut être utile de mentionner que le pouvoir de l'expression régulière est exactement comme celui d'un automate fini. Autrement dit, les expressions rationnelles peuvent valider / analyser la même classe de chaînes validée et analysée par des automates finis. Par conséquent, une représentation lisible par un humain d'une expression rationnelle devrait probablement être capable de construire rapidement un graphique, et je pense que la plupart des langages basés sur du texte sont vraiment mauvais; c'est pourquoi nous utilisons des outils visuels pour de telles choses. Jetez un coup d’œil à hackingoff.com/compilers/regular-expression-to-nfa-dfa pour trouver de l’inspiration.
damix911

Réponses:

80

Un certain nombre de personnes ont mentionné composer à partir de pièces plus petites, mais personne n'a encore donné d'exemple, alors voici le mien:

string number = "(\\d+)";
string unit = "(?:" + number + "\\s*:\\s*)";
string optionalDecimal = "(?:\\s*[.,]\\s*" + number + ")?";

Pattern re = Pattern.compile(
  "^\\s*(?:" + unit + "?" + unit + ")?" + number + optionalDecimal + "\\s*$"
);

Pas le plus lisible, mais je sens que c'est plus clair que l'original.

En outre, C # a l' @opérateur qui peut être préfixé à une chaîne afin d'indiquer qu'il doit être pris à la lettre (pas de caractères d'échappement), donc numberserait@"([\d]+)";

Bobson
la source
Tout à l'heure, remarquons à quel point [\\d]+et [0-9]+devrait être juste \\d+(enfin, certains peuvent trouver [0-9]+plus lisible). Je ne vais pas modifier la question, mais vous voudrez peut-être corriger cette réponse.
Hyde
@hyde - Bonne prise. Techniquement, ce n'est pas tout à fait la même chose - \dcorrespond à tout ce qui est considéré comme un numéro, même dans les autres systèmes de numérotation (chinois, arabe, etc.), alors qu'il [0-9]ne fait que correspondre aux chiffres standard. J'ai normalisé \\d, cependant, et en a pris en compte dans le optionalDecimalmodèle.
Bobson
42

La clé pour documenter l'expression régulière est de le documenter. Bien trop souvent, les gens jettent dans ce qui semble être du bruit de ligne et s'en tiennent là.

Au sein de perl, l' /xopérateur situé à la fin de l'expression régulière supprime les espaces permettant de documenter l'expression régulière.

L'expression régulière ci-dessus deviendrait alors:

$re = qr/
  ^\s*
  (?:
    (?:       
      ([\d]+)\s*:\s*
    )?
    (?:
      ([\d]+)\s*:\s*
    )
  )?
  ([\d]+)
  (?:
    \s*[.,]\s*([\d]+)
  )?
  \s*$
/x;

Oui, cela consomme un peu d'espace blanc vertical, bien que l'on puisse le raccourcir sans sacrifier trop de lisibilité.

Et ensuite, ce que fait l’expression rationnelle précédente est la suivante: analyser une chaîne de nombres au format 1: 2: 3.4, en capturant chaque nombre, les espaces étant autorisés et 3 obligatoires seulement.

En regardant cette expression régulière, on peut voir comment cela fonctionne (et ne fonctionne pas). Dans ce cas, cette expression rationnelle correspondra à la chaîne 1.

Des approches similaires peuvent être prises dans une autre langue. L' option python re.VERBOSE y travaille.

Perl6 (l'exemple ci-dessus s'appliquait à perl5) va plus loin avec le concept de règles qui conduit à des structures encore plus puissantes que le PCRE (il donne accès à d'autres grammaires (sans contexte et sensibles au contexte) que celles qui sont régulières et étendues).

En Java (d'où cet exemple est tiré), on peut utiliser la concaténation de chaînes pour former l'expression régulière.

Pattern re = Pattern.compile(
  "^\\s*"+
  "(?:"+
    "(?:"+
      "([\\d]+)\\s*:\\s*"+  // Capture group #1
    ")?"+
    "(?:"+
      "([\\d]+)\\s*:\\s*"+  // Capture group #2
    ")"+
  ")?"+ // First groups match 0 or 1 times
  "([\\d]+)"+ // Capture group #3
  "(?:\\s*[.,]\\s*([0-9]+))?"+ // Capture group #4 (0 or 1 times)
  "\\s*$"
);

Certes, cela en crée beaucoup plus "dans la chaîne, ce qui peut être source de confusion, peut être plus facilement lu (en particulier avec la coloration syntaxique sur la plupart des IDE) et documenté.

La clé est de reconnaître le pouvoir et la nature "écrire une fois" dans laquelle les expressions régulières tombent souvent. L'écriture du code pour éviter ceci de manière défensive afin que l'expression régulière reste claire et compréhensible est la clé. Nous mettons en forme le code Java pour plus de clarté - les expressions régulières ne sont pas différentes lorsque le langage vous en donne la possibilité.


la source
13
Il y a une grande différence entre "documenter" et "ajouter des sauts de ligne".
4
@JonofAllTrades Rendre le code lisible est la première étape de tout. L'ajout de sauts de ligne permet également d'ajouter des commentaires pour ce sous-ensemble de l'ER sur la même ligne (chose plus difficile à effectuer sur une seule longue ligne de texte d'expression régulière).
2
@JonofAllTrades, je ne suis pas du tout d'accord. "Documenter" et "ajouter des sauts de ligne" ne sont pas si différents en ce sens qu'ils servent tous les deux le même objectif - rendre le code plus facile à comprendre. Et pour le code mal formaté, "l'ajout de sauts de ligne" répond mieux à cet objectif que l'ajout de documentation.
Ben Lee
2
L'ajout de sauts de ligne est un début, mais cela représente environ 10% du travail. D'autres réponses donnent plus de détails, ce qui est utile.
26

Le mode "verbeux" proposé par certaines langues et bibliothèques est l'une des réponses à ces préoccupations. Dans ce mode, les espaces dans l'expression rationnelle sont supprimés (vous devez donc les utiliser \s) et les commentaires sont possibles. Voici un petit exemple en Python qui supporte cela par défaut:

email_regex = re.compile(r"""
    ([\w\.\+]+) # username (captured)
    @
    \w+         # minimal viable domain part
    (?:\.w+)    # rest of the domain, after first dot
""", re.VERBOSE)

Dans toutes les langues, implémenter un traducteur du mode commenté au mode "normal" devrait être une tâche simple. Si vous êtes préoccupé par la lisibilité de vos expressions rationnelles, vous justifieriez probablement cet investissement en temps assez facilement.

Xion
la source
15

Chaque langue qui utilise des expressions rationnelles vous permet de les composer à partir de blocs plus simples pour faciliter la lecture. Si vous utilisez quelque chose de plus compliqué que votre exemple, vous devez absolument tirer parti de cette option. Le problème particulier de Java et de nombreux autres langages est qu’ils ne traitent pas les expressions régulières comme des citoyens "de première classe", mais les obligent à se faufiler dans le langage via des chaînes de caractères. Cela signifie que de nombreux guillemets et barres obliques inverses qui ne font pas vraiment partie de la syntaxe des expressions rationnelles et rendent la lecture difficile, mais cela signifie également que vous ne pouvez pas être beaucoup plus lisible que cela sans définir efficacement votre propre mini-langage et interprète.

Bien entendu, Perl était le meilleur moyen d’intégrer des expressions rationnelles, avec son option d’espace et ses opérateurs citant les regex. Perl 6 étend le concept de construction de regex à partir de parties en grammaires récursives réelles, ce qui est tellement préférable d'utiliser ce n'est vraiment aucune comparaison. La langue a peut-être manqué le bateau de la rapidité, mais son support regex était The Good Stuff (tm).

Kilian Foth
la source
1
Par "blocs plus simples" mentionnés au début de la réponse, voulez-vous dire simplement la concaténation de chaînes ou quelque chose de plus avancé?
Hyde
7
Je voulais dire définir les sous-expressions comme des littéraux de chaîne plus courts, les affecter à des variables locales portant des noms significatifs, puis les concaténer. Je trouve que les noms sont plus importants pour la lisibilité que la simple amélioration de la mise en page.
Kilian Foth
11

J'aime utiliser Expresso: http://www.ultrapico.com/Expresso.htm

Cette application gratuite présente les fonctionnalités suivantes que je trouve utiles au fil du temps:

  • Vous pouvez simplement copier et coller votre regex et l'application l'analysera pour vous
  • Une fois votre regex écrite, vous pouvez le tester directement depuis l'application (l'application vous donnera la liste des captures, remplacements, etc.)
  • Une fois que vous l'avez testé, il générera le code C # pour l'implémenter (notez que le code contiendra les explications à propos de votre expression régulière).

Par exemple, avec l'expression régulière que vous venez de soumettre, cela ressemblerait à ceci: Exemple d'écran avec la regex donnée initialement

Bien sûr, faire un essai vaut mille mots pour le décrire. S'il vous plaît noter également que je suis note liée de quelque façon que ce soit avec l'éditeur de cette application.

E. Jaep
la source
4
cela vous dérangerait-il d'expliquer ceci plus en détail - comment et pourquoi répond-il à la question posée? « Réponses Link-only » ne sont pas tout à fait les bienvenus à Stack Echange
moucheron
5
@gnat Désolé pour ça. Vous avez absolument raison. J'espère que ma réponse corrigée fournit plus d'informations.
E. Jaep
Je peux aussi vraiment recommander: regex101.com
Epskampie Il y a
9

Pour certaines choses, il pourrait être utile d’utiliser une grammaire telle que BNF. Celles-ci peuvent être beaucoup plus faciles à lire que les expressions régulières. Un outil tel que GoldParser Builder peut ensuite convertir la grammaire en un analyseur syntaxique qui effectue le gros du travail pour vous.

Les grammaires BNF, EBNF, etc. peuvent être beaucoup plus faciles à lire et à créer qu’une expression régulière compliquée. GOLD est un outil pour de telles choses.

Le lien wiki c2 ci-dessous contient une liste d'alternatives possibles pouvant être recherchées sur Google, avec quelques discussions à ce sujet. Il s’agit essentiellement d’un lien "voir aussi" pour compléter les recommandations de mon moteur de grammaire:

Alternatives aux expressions régulières

Prenant "alternative" signifie "installation sémantiquement équivalente avec une syntaxe différente", il existe au moins ces alternatives à / avec RegularExpressions:

  • Expressions régulières de base
  • Expressions régulières "étendues"
  • Expressions régulières compatibles Perl
  • ... et beaucoup d'autres variantes ...
  • Syntaxe RE de style SNOBOL (SnobolLanguage, IconLanguage)
  • Syntaxe SRE (RE comme EssExpressions)
  • différentes syntaces FSM
  • Grammaires d'intersection d'états finis (assez expressives)
  • ParsingExpressionGrammars, comme dans OMetaLanguage et LuaLanguage ( http://www.inf.puc-rio.br/~roberto/lpeg/lpeg.html )
  • Le mode d'analyse de RebolLanguage
  • ProbabilityBasedParsing ...
Nick P
la source
Pourriez-vous expliquer davantage en quoi ce lien est utile et à quoi sert-il? Les réponses "en lien uniquement" ne sont pas les bienvenues à Stack Exchange
gnat le
1
Bienvenue aux programmeurs, Nick P. S'il vous plaît ignorer le vote négatif / r, mais lisez la page sur les méta que @gnat a lié.
Christoffer Lette
@ Christoffer Lette Appréciez votre réponse. Je vais essayer de garder cela à l'esprit dans les prochains messages. Le commentaire de @ gnat Paulo Scardine reflète l'intention de mes messages. Les grammaires BNF, EBNF, etc. peuvent être beaucoup plus faciles à lire et à créer qu’une expression régulière compliquée. GOLD est un outil pour de telles choses. Le lien c2 contient une liste d'alternatives possibles pouvant être recherchées sur Google, avec des discussions à ce sujet. C'était essentiellement un lien "voir aussi" pour compléter ma recommandation de moteur de grammaire.
Nick P
6

C'est une vieille question et je n'ai vu aucune mention d' expressions verbales, alors j'ai pensé ajouter cette information ici aussi aux futurs demandeurs. Les expressions verbales ont été spécialement conçues pour rendre les regex humains compréhensibles, sans qu'il soit nécessaire d'apprendre la signification des symboles de regex. Voir l'exemple suivant. Je pense que cela fait mieux que ce que vous demandez.

// Create an example of how to test for correctly formed URLs
var tester = VerEx()
    .startOfLine()
    .then('http')
    .maybe('s')
    .then('://')
    .maybe('www.')
    .anythingBut(' ')
    .endOfLine();

// Create an example URL
var testMe = 'https://www.google.com';

// Use RegExp object's native test() function
if (tester.test(testMe)) {
    alert('We have a correct URL '); // This output will fire}
} else {
    alert('The URL is incorrect');
}

console.log(tester); // Outputs the actual expression used: /^(http)(s)?(\:\/\/)(www\.)?([^\ ]*)$/

Cet exemple concerne le javascript, vous pouvez maintenant trouver cette bibliothèque pour de nombreux langages de programmation.

Parivar Saraff
la source
2
C'est génial!
Jeremy Thompson
3

Le moyen le plus simple serait de continuer à utiliser regex mais de construire votre expression en composant des expressions plus simples avec des noms descriptifs, par exemple http://www.martinfowler.com/bliki/ComposedRegex.html (et oui, il s'agit de string concat)

Cependant, vous pouvez également utiliser une bibliothèque de combinateur d'analyseurs, par exemple http://jparsec.codehaus.org/, qui vous donnera un analyseur décent complet et récursif. Là encore, le véritable pouvoir provient de la composition (cette fois-ci de la composition fonctionnelle).

jk.
la source
3

Je pensais qu'il valait la peine de mentionner les expressions de grst de logstash . Grok s'appuie sur l'idée de composer des expressions d'analyse syntaxique longues à partir d'expressions plus courtes. Il permet de tester facilement ces blocs de construction et est livré pré-emballé avec plus de 100 modèles couramment utilisés . Autres que ces modèles, il permet d'utiliser la syntaxe de toutes les expressions régulières.

Le modèle ci-dessus exprimé dans grok est (j'ai testé dans l' application de débogage, mais aurait pu gaffe):

"(( *%{NUMBER:a} *:)? *%{NUMBER:b} *:)? *%{NUMBER:c} *(. *%{NUMBER:d} *)?"

Les parties et les espaces optionnels le rendent un peu plus laid que d'habitude, mais ici et dans d'autres cas, l'utilisation de grok peut rendre la vie beaucoup plus agréable.

YoniLavi
la source
2

En F #, vous avez le module FsVerbalExpressions . Il vous permet de composer des expressions rationnelles à partir d'expressions verbales. Il contient également des expressions rationnelles prédéfinies (comme l'URL).

L'un des exemples de cette syntaxe est le suivant:

let groupName =  "GroupNumber"

VerbEx()
|> add "COD"
|> beginCaptureNamed groupName
|> any "0-9"
|> repeatPrevious 3
|> endCapture
|> then' "END"
|> capture "COD123END" groupName
|> printfn "%s"

// 123

Si vous n'êtes pas familier avec la syntaxe F #, groupName est la chaîne "GroupNumber".

Ils créent ensuite une expression verbale (VerbEx) qu’ils construisent en tant que "COD (? <GroupNumber> [0-9] {3}) END". Ils testent ensuite la chaîne "COD123END", où ils obtiennent le groupe de capture nommé "GroupNumber". Cela donne 123.

Honnêtement, je trouve la regex normale beaucoup plus facile à comprendre.

CodeMonkey
la source
-2

Premièrement, comprenez que le code qui fonctionne simplement est un mauvais code. Un bon code doit également signaler avec précision les erreurs rencontrées.

Par exemple, si vous écrivez une fonction pour transférer de l'argent d'un compte d'utilisateur à un autre utilisateur; vous ne renverriez pas simplement un booléen "travaillé ou échoué" car cela ne donnerait à l'appelant aucune idée de ce qui n'allait pas et ne lui permettait pas d'informer l'utilisateur correctement. Au lieu de cela, vous pourriez avoir un ensemble de codes d'erreur (ou un ensemble d'exceptions): compte de destination introuvable, fonds insuffisant dans le compte source, autorisation refusée, impossible de se connecter à la base de données, charge excessive (nouvelle tentative ultérieure), etc. .

Maintenant, pensez à votre exemple "analyser une chaîne de nombres au format 1: 2: 3.4". La regex ne fait que signaler un "succès / échec" qui ne permet pas de présenter un retour adéquat à l'utilisateur (que ce retour soit un message d'erreur dans un journal ou une interface graphique interactive où les erreurs sont affichées en rouge sous la forme types d'utilisateurs, ou quoi que ce soit d'autre). Quels types d'erreurs ne parvient-il pas à décrire correctement? Mauvais caractère dans le premier chiffre, premier chiffre trop grand, deux-points manquants après le premier chiffre, etc.

Pour convertir "un code défectueux qui fonctionne simplement" en "un bon code générant des erreurs descriptives adéquates", vous devez diviser l'expression rationnelle en plusieurs expressions rationnelles plus petites (en général, des expressions rationnelles si petites qu'il est plus facile de le faire sans regex ).

Rendre le code lisible / maintenable n’est qu’une conséquence accidentelle de la qualité du code.

Brendan
la source
6
Probablement pas une bonne hypothèse. Le mien est parce que A) Cela ne répond pas à la question ( comment le rendre lisible?), B) La correspondance d'expression régulière est un test réussi / échec, et si vous le décomposez au point de pouvoir dire exactement pourquoi il a échoué, perdre beaucoup de puissance et de vitesse, et augmenter la complexité, C) Rien ne dit de la question que le match puisse même échouer - il s'agit simplement de rendre lisible Regex. Lorsque vous avez le contrôle des données entrantes et / ou les validez au préalable, vous pouvez supposer que celles-ci sont valides.
Bobson
A) Le casser en morceaux plus petits le rend plus lisible (en conséquence de le rendre bon). C) Lorsque des chaînes inconnues / non validées entrent dans un logiciel, un développeur sain analysera (avec un rapport d'erreur) à ce moment-là et convertira les données dans un formulaire ne nécessitant pas de reparsing - regex ne sera plus nécessaire par la suite. B) est un non-sens qui ne s'applique qu'aux mauvais codes (voir points A et C).
Brendan
Allant de C: Et si c'est sa logique de validation? Le code de l'OP pourrait correspondre exactement à ce que vous suggérez - valider l'entrée, signaler s'il n'est pas valide et le convertir en un format utilisable (via les captures). Tout ce que nous avons, c'est l'expression elle-même. Comment suggéreriez-vous de l'analyser autrement qu'avec une regex? Si vous ajoutez un exemple de code permettant d'obtenir le même résultat, je supprimerai mon vote négatif.
Bobson
S'il s'agit de "C: Validation (avec rapport d'erreur)", il s'agit d'un code incorrect, car le rapport d'erreur est incorrect. Si cela échoue; Était-ce parce que la chaîne était NULL, ou parce que le premier nombre avait trop de chiffres, ou parce que le premier séparateur ne l'était pas :? Imaginez un compilateur qui n'a qu'un seul message d'erreur ("ERROR") qui est trop stupide pour dire à l'utilisateur quel est le problème. Maintenant, imaginez des milliers de sites Web qui sont tout aussi stupides et affichent (par exemple) "Adresse e-mail incorrecte" et rien de plus.
Brendan
En outre, imaginez un opérateur de centre d’assistance semi-formé recevant un rapport de bogue émanant d’un utilisateur complètement non formé qui dit: Le logiciel a cessé de fonctionner - la dernière ligne du journal du logiciel est "ERREUR: impossible d’extraire le numéro de version mineur de la chaîne de version" 1: 2-3.4 '(deux points après le deuxième chiffre) "
Brendan le