Pourquoi vos regex ne fonctionnent jamais 2


Sommaire

zenobral

Dans le monde du SEO il y a deux camps : ceux qui maîtrisent les expressions rationnelles…. et les autres. Bon évidemment il y a d’autres camps 🙂 mais c’est ce petit duel qui va nous intéresser aujourd’hui.

Bien que l’on puisse être un excellent SEO sans connaître les expressions régulières (vous avez vu, variations sémantiques, toussa), et j’en connais plein, il y a un certain nombre de chantiers auquel on ne pourra que difficilement s’attaquer sans les maîtriser un minimum. Au mieux, cela se fera avec de l’huile de coude et dans la douleur, et au pire pas du tout. Les applications sont très nombreuses, et personnellement rares sont les semaines où je n’ai pas à m’en servir une fois…. En même temps, comme tout bon ouvrier, maintenant que j’ai investi dans ma perceuse, ce n’est pas pour la laisser au placard. Parmi les applications je pense que tout le monde se sera retrouvé au moins dans un de ces cas :

  • identifier des mots clés marque/hors marque dans une grosse liste de keywords
  • catégoriser des urls
  • faire des règles de réécritures d’url pour chèvropleur (croisement entre une chèvre et un développeur) qui n’a jamais vu une conf apache, mais qui fait quand même des prestations de création de site
  • parser des lignes de log
  • nettoyer du code html
  • récupérer des adresses mails ou des numéros de téléphone en scrapant des sites
  • Parser un csv monstrueux pour ne garder que ce qui vous intéresse (des adresses ip, les visites d’un pays, les mots clés d’un client)

En général lorsque j’explique les regex à quelqu’un, j’observe trois profils distincts :

  • Le dévot, qui voit immédiatement tous les problèmes qu’il va pouvoir résoudre, qui se met à les utiliser dès que possible et qui m’envoie, des années après, des mots de remerciements pour lui avoir changé sa vie en même temps que 10% de son salaire actuel
  • Le sceptico-réfractaire, qui après avoir lu « \d+(?:\.\d+){3} » est allé direct aux toilettes rendre sa salade au tofu (en même temps normal quoi)
  • L’enthousiaste, qui sent les applications, mais pense que ça lui servira que rarement, et finalement abandonne au bout de 4-5 échecs de regex matching

A travers ce post, c’est donc ce dernier profil que j’entend récupérer… et même s’il n’y en a qu’un, ce n’est pas grave j’aurais quand même gagné des points de karma. Si vous n’avez jamais touché aux expressions rationnelles, je vous suggère de commencer par lire ce post sur ce même blog et de faire quelques essais de votre côté histoire d’assimiler quelques bases. Ce post n’est vraiment pas dédié à ceux qui n’y ont jamais touché, mais à ceux qui ont envie de s’y mettre et qui galèrent.

Au niveau outils, voici ceux dans lesquels j’utilise régulièrement des regex :

  • sed, awk et grep (outils par défaut sur mac et linux, et également disponibles sur windows via gnuwin32)
  • notepad++, un éditeur de texte très pratique
  • excel avec le plugin seotools (mais depuis les dernières versions, c’est devenu accessible uniquement dans la version payante)
  • Google spreadsheet et ses fonctions REGEXMATCH, REGEXREPLACE, REGEXEXTRACT
  • Google datastudio, notamment lors de la définition de champs personnalisés
  • botify, oncrawl, deux crawler saas dont je ne pourrais plus me passer
  • python, le langage de programmation enregistrant la plus forte progression de ces dernières années

Raison n°1 : Vous copiez/collez vos bêtement vos regex d’un programme à un autre

C’est sans doute la plus grande erreur, et potentiellement celle qui a du vous décourager. En réalité chaque moteur de regex a de subtiles différences d’un programme à un autre et vous serez obligés de regarder la doc pour vérifier que ce que vous écrivez est bien accepté syntaxiquement et correspond bien au comportement attendu.

Quelques exemples en vrac :

  • le « / » a besoin souvent d’être échappé avec un « \ » sur javascript et sed car la regex est elle même déclarée entre deux « / »
  • le \d, \w ne sont pas reconnus sur certains moteurs et doivent être explicités plutôt avec [0-9] et [a-zA-Z0-9]
  • le \w va matcher les caractères accentués sur seotools (qui tourne sur .NET) mais pas sur les autres
  • les « flags » qui permettent de faire marcher la regex en case insensitive ou en multiligne ne s’écrivent jamais de la même manière
  • le « . » qui va matcher certains caractères spéciaux ou non
  • La nécessité d’échapper le « \ » sur certains outils qui utilisent des regex natives en JS et Python (par exemple). Sur datastudio par exemple, on écrira ‘\\d’ au lieu de ‘\d’

Raison n°2 : vous pensez que la regex commence sa recherche en début de ligne

Ce n’est « généralement » pas vrai, et comme d’habitude il faut se renseigner sur l’outil utilisé pour être sûr. Mais la plupart du temps, votre regex va matcher dès que le pattern est contenu dans la ligne que vous essayez de vérifier.

Donc mettons que vous ayez une liste de mots clés marque à filtrer :

  • le bon coin
  • annonces le bon coin
  • le bon coin annonces
  • petites annonces

Inutile de mettre « .*le bon coin.* » pour filtrer… « le bon coin » suffira

Par contre vous pouvez « borner » la recherche avec « ^ » est « $ » de manière à imposer la recherche en début ou en fin de ligne ou les deux »

  • « ^le bon coin » matchera « le bon coin » et « le bon coin annonces » (la ligne doit commencer par le pattern)
  • « le bon coin$ » matchera « le bon coin » et « annonces le bon coin » (la ligne doit se terminer par le pattern)
  • « ^le bon coin$ » ne matchera  que « le bon coin » (la ligne doit commencer et finir par le pattern)

Raison n°3 : vous utilisez .* partout

S’il y a un truc que les gens ont compris dans les regex, c’est que « .* » cela matche n’importe quoi, du coup ils l’utilisent partout, dès qu’ils ne veulent pas taper un caractère spécifique, ce qui peut créer des surprises.

Supposons par exemple que l’on veuille traiter une liste d’urls pour récupérer celles qui ont au moins répertoire et récupérer le premier parent.

La manière naïve serait d’écrire ceci comme expression régulière : https://www.site.com/(.*)/.*

Observons ce qu’il se passe :

  • https://www.site.com/  –> pas de match (ok, c’est ce qu’on voulait)
  • https://www.site.com// –> match, groupe capturant 1 : «  » (merde on voulait pas le matcher celui-là)
  • https://www.site.com/femme/escarpins –> match, groupe capturant 1 : « femme » (yes!! c’est qui le boss des regex ?!)
  • https://www.site.com/homme/chaussures/ –> match, groupe capturant 1 : « homme/chaussures » (ah je voulais juste homme !!!)
  • https://www2site.com/femme/escarpins –> match, groupe capturant 1 : « femme » (mais c’est quoi ce site ?)

Je vous rassure, tout le monde est passé par là. Que se passe-t-il en réalité? Quelques clés pour comprendre :

  • le « . » est un meta-caractère qui matche, par défaut, n’importe quel caractère, sauf les sauts de ligne. Toutefois, quasi tout les moteurs de regex disposent d’une option pour que le « . » puisse également matcher les sauts de ligne si nécessaire
  • Le « * » indique le nombre de répétitions possibles qui va aller de 0 (pas de caractère) à l’infini. Si on veut imposer la présente d’au moins un caractère on utiliser le « + » qui voudra dire « au moins une fois… jusqu’à l’infini »

En conséquence .* veut dire en gros « n’importe quoi, sauf un saut de ligne, même rien »

La dernière subtilité est que le « * » et le « + » sont des répétitions dites « greedy »… En gros elles vont « consommer » le maximum de caractères pour que la regex soit valide… en commençant toujours le plus à gauche de la chaîne.

En gros si vous avez une regex simple comme /.+/ alors :

  • / –> ne matche pas ( pas de « / » final)
  • // –> ne matche pas (avec le « + », il faut au moins un caractère entre les deux « / »)
  • /cat1/ –> match et le match global est « /cat1/ »
  • /cat1/cat2 –> match et le match global est « /cat1/ »
  • /cat1/cat2/ –>match et le match global est « /cat1/cat2/ » (le « .+ » a consommé tous les caractères possibles jusqu’au « / » final)

Il est possible de définir des répétitions non greedy (ce qu’on va voir plus bas), mais dans le cas présent, on peut contourner le problème en définissant une classe de caractère personnalisée.

Qu’est ce qui définit un répertoire dans un url ? Et bien c’est tout simplement une suite de caractères, se trouvant entre deux « / » qui ne sont pas eux-même des « / ».

On définit donc une classe de caractère pour traduire « n’importe quel caractère sauf un ‘/’ : [^/]

Recomposons notre regex pour qu’elle fasse exactement ce qu’on veut :

  • on supprime le .* final qui ne sert à rien : https://www.site.com/(.*)/.*
  • on échappe les « . » dans le domaine pour éviter que le « . » match des caractères non voulus  :  https://www\.site\.com/(.*)/
  • on remplace le « * » par un « + » car on souhaite qu’il y ait au moins un caractère : https://www\.site\.com/(.+)/
  • on remplace le « . » par [^/] : https://www\.site\.com/([^/]+)/

Si on repasse nos urls à la moulinette :

  • https://www.site.com/  –> pas de match (yes !)
  • https://www.site.com// –> pas de matchs (yes again!)
  • https://www.site.com/femme/escarpins –> match, groupe capturant 1 : « femme » (cool, ca marche toujours)
  • https://www.site.com/homme/chaussures/ –> match, groupe capturant 1 : « homme » (yes toujours !)
  • https://www2site.com/femme/escarpins –> pas de match ( who’s the boss bitch ?!)

Raison n°4 : l’encodage des fichiers

L’encodage est un des trucs qui m’a le plus cassé la tête à mes débuts et je pense que je ne suis pas le seul. Sans parler d’expressions régulières, je pense que l’on a été nombreux à ouvrir un fichier csv directement dans excel et à remplacer à la mano les caractères spéciaux (si vous faites cela, pitié venez me demander conseil, je vais vous épargner de nombreuses heures).

En regex, certains programmes permettent de prendre les patterns depuis un fichier, ce qui est très pratique pour filtrer et catégoriser en masse, par exemple une énorme base de mots clés ou d’urls. C’est, entre autres, le cas de grep ou de sed,  programmes bien connu des utilisateurs unix, et installables sur windows.

Supposons que l’on tape ceci : grep -f pattern_file.txt kewyords.txt

L’option ‘-f’ signifie à grep que l’on veut prendre les patterns dans le fichier suivant (ici pattern_file.txt) et l’appliquer à keywords.txt. Chaque pattern va être matché contre l’ensemble des expressions contenues dans keyword.txt et seules celles qui auront matché au moins un pattern se retrouveront en sortie.

Mais, ô surprise, à la sortie vous constatez, après vérification, qu’il vous manque plein d’expressions. Et bien le premier truc à faire c’est de vérifier que l’encodage du fichier ‘pattern_file.txt’ est bien le même que ‘keywords.txt’.

En effet les caractères en utf-8 ne sont pas encodés de la même manière que en iso-8859-1 qui lui-même a quelques différences avec l’iso-8859-15.

Mon conseil ultime : convertissez toujours tous vos fichiers (avec notepad++ par exemple) en utf-8 sans BOM… Tout le temps, toute la vie, à toute heure… et vous n’aurez plus de problèmes

Raison n°5 : Les expressions Greedy

Si jamais vous vous lancez dans le nettoyage de code HTML pour récupérer uniquement le contenu textuel, alors ceci va vous intéresser.

Lorsque l’on nettoie du code HTML, il y a principalement 3 sortes de choses que l’on enlève :

  • les balises que l’on veut supprimer seules : h1, h2,h3, div, span, footer, header, etc..
  • les balises que l’on veut supprimer en même temps que leur contenu : script, style
  • les commentaires <!– blabla –>

Supprimer les tags seuls, ne pose pas de grosse difficultés, il suffit de se baser sur les conventions du w3c sur la syntaxe :

  • https://www.w3.org/TR/html52/syntax.html#syntax-start-tags
  • https://www.w3.org/TR/html52/syntax.html#tag-name

Donc par étapes :

  • un tag commence forcément par un ‘<‘ : <
  • si c’est un tag de fermeture, il sera suivi par un ‘/’ qu’on place donc en optionnel avec ‘?’ : </?
  • il est ensuite suivi du nom du tag, qui est uniquement composé de caractères ASCII alphanumériques (chiffres et lettres non accentuées) : </?[a-zA-Z0-9]+
  • Il peut ensuite posséder une suite d’attributs optionnels pour se terminer par un ‘>’. On va donc traduire cela par « n’importe quoi sauf  ‘>’ suivi d’un ‘>' » : </?[a-zA-Z0-9]+[^>]*>

Dans le cas d’un tag auto fermant comme <br />, on peut constater que le ‘ /’ est déjà bien catché par notre ‘[^>]*’

Pour enlever les commentaires ou les scripts c’est plus compliqué

La première chose dont a besoin c’est d’utiliser un flag DOTALL (ou single line, c’est la même chose) qui explicite que le ‘.’ de l’expression régulière pourra également matcher les sauts de ligne, ce qui est indispensable pour supprimer du contenu ou des balises sur plusieurs lignes.

Maintenant supposons que l’on veuille supprimer les commentaires d’un texte comme cela :

bonjour,
<!-- commentaire
très utile -->
comment allez-vous?
<!-- commentaire encore
 plus utile -->
aujourd'hui

La manière naïve serait d’écrire notre regex comme cela : <!–.*–>

Comme on l’a vu précédemment, le problème est que le ‘*’ va fonctionner en mode greedy et consommer tous les caractères possibles jusqu’au dernier ‘–>’ rencontré dans le texte.

Regardez ici ce qu’il se passe : https://regex101.com/r/NM8w0H/1

On obtient un seul objet « match1 » qui contient effectivement les commentaires, mais également ce qui se trouve entre les commentaires… Shame !

La parade est donc d’explicitement préciser au caractère ‘*’ de fonctionner en mode non greedy via l’adjonction du caractère ‘?’ : <!–.*?–>

Dans ce cas là, au lieu d’avoir notre ‘.*’ qui consommera tous les caractères jusqu’au dernier ‘–>’, notre ‘.*?’ consommera tous les caractères jusqu’au premier ‘–>’ rencontré. Il suffira simplement de répéter la suppression.

Si vous observez le changement ici : https://regex101.com/r/6tQgiG/1/

Cette fois-ci on constate que l’on a capturé seulement les commentaires.

Pour les balises script et style c’est exactement la même problématique :

On va préciser explicitement notre regex en non greedy avec <script>.*?</script> afin de supprimer que le contenu inclus entre une balise <script> ouvrante et la première balise </script> fermante rencontrée.

Exemple ici : https://regex101.com/r/zz0SzW/1

Raison n°6 : Vous ne maîtrisez pas le rappel de groupes capturants

Ce n’est pas la fonctionnalité la plus connue des expressions régulières, mais elle vous sortira de plusieurs mauvais pas.

Si on reprend l’exemple de la suppression des tags script et style précédent. Mettions que vous vouliez écrire une seule expression régulière pour supprimer ces deux balises en même temps.

La manière naïve de l’écrire serait un truc comme cela :

<(script|style)>.*?</(script|style)>

Notez que la parenthèse du groupe capturant sert à limiter la portée de l’opérateur ‘|’ :

  • a|bc signifie « a ou bc »
  • (a|b)c signifie « a ou b suivi de c »

Le problème c’est que vous pouvez tomber sur un truc comme ceci :

<script> document.write(‘<style>p {color:red}</style>’) </script>

Et concrètement voilà ce qui va matcher : https://regex101.com/r/bzooGr/1

Pourquoi? Et bien concrètement votre regex fait exactement ce que vous lui avez demandé : elle commencer à la première balise qui est un <style> ou un <script>, et consomme tous les caractères jusqu’à la première balise </style> ou </span> fermante. Le problème ici est que vous avez rencontré un <script> en premier mais que la première balise fermante rencontrée est un </style>.

Ce que vous voulez vraiment faire en réalité, est que lorsque vous rencontrez une balise <script>, votre regex se termine au </script> correspondant… et même chose pour la balise style.

Et encore une fois les regex vont vous sortir de ce mauvais pas à l’aide du rappel des groupes capturants. A nouveau, il faut vérifier que votre programme accepte cette fonctionnalité et vérifier sa syntaxe. Sur la plupart des programmes, la syntaxe est un ‘\’ suivi du n° du groupe capturant : \3 pour le troisième groupe capturant par exemple.

Dans le cas où vous auriez plusieurs plusieurs parenthèses imbriquées les unes dans les autres, sachez que l’on compte simplement dans l’ordre des parenthèses ouvrantes.

Ex : la regex a(b(c(d))) sur la chaîne de caractère abcd

  • groupe 0 (le match entier) –> abcd
  • groupe 1 –> bcd
  • groupe 2 –> cd
  • groupe 3 –> d

Notre regexp transformée passe donc de cela : https://regex101.com/r/s1Z8em/2

Et là c’est bien le comportement attendu :

  • si le premier tag est un <script>, la regex machera jusqu’au premier tag fermant </script>
  • si le premier tag est un <style>, la regex machera jusqu’au premier tag fermant </style>

Ressources complémentaires

Conseil Final

Typiquement, commencez par travailler les expressions régulières sur un seul programme afin de vous sentir à l’aise avec son moteur. Notepad++ est une bonne alternative pour les débutants pour tester pas mal de choses et devrait vous permettre de faire vos premières armes. Si jamais vous bloquez dans un autre environnement, revenez simplement sur votre programme favori et comparez… Si cela marche, c’est que la différence ne vient pas de votre méconnaissance de la théorie, mais de la documentation.

C’est honnêtement un peu de temps à investir pour devenir bon… Mais si vous évoluez longtemps dans le milieu du marketing digital, vous le retrouverez 100 fois.


Laissez un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

2 commentaires sur “Pourquoi vos regex ne fonctionnent jamais