Des automates à grep, en passant par sed :
ce que tout ingénieur devrait savoir avant de tokeniser le monde.
Le cours de Web Sémantique commence par une opération si triviale qu'on
n'y pense même plus : découper du texte. Un
.split() par-ci, un re.findall() par-là, et
on obtient des mots. Mais derrière cette apparente simplicité se cache
une théorie magnifique qui mêle logique, algèbre,
automates et langages formels.
Cette annexe n'est pas un cours d'ingénierie linguistique.
C'est un rappel — ou une découverte, selon d'où vous
venez — de ce que sont les expressions régulières, comment elles
fonctionnent vraiment (automates non déterministes →
déterministes), et pourquoi des outils centenaires (dans le monde
numérique) comme grep et sed sont encore
les meilleurs amis du développeur.
grep en une nuit (littéralement)sed fait son apparition dans Unix V7perl popularise les regex étendues (lookahead, backreferences…)Une expression régulière (ou regex) est une notation compacte pour décrire un ensemble de chaînes de caractères. Au lieu de lister « aa, ab, ba, bb », on écrit [ab]{2}. Au lieu de dire « une adresse email », on écrit \w+@\w+\.\w+.
Derrière cette notation, il y a trois opérations de base, définies par Kleene en 1956, et qui suffisent à tout construire :
| Opération | Notation | Exemple | Reconnaît |
|---|---|---|---|
| Union (ou) | | |
a|b |
a ou b |
| Concaténation | (juxtaposition) | ab |
a suivi de b |
| Étoile de Kleene | * |
a* |
ε, a, aa, aaa… |
C'est tout. Trois opérations suffisent à décrire
n'importe quel langage régulier. Tout le reste — +,
?, [a-z], \d, les groupes,
les lookaheads — n'est que du sucre syntaxique.
(Du sucre délicieux, certes, mais du sucre.)
| Notation | Équivalent | Sens |
|---|---|---|
+ | rr* | Une ou plusieurs répétitions |
? | r|ε | Optionnel (zéro ou une) |
{n,m} | rr…r(r|ε)… | Entre n et m répétitions |
[abc] | a|b|c | Une classe de caractères |
. | (tout sauf \n) | Caractère quelconque |
\d | [0-9] | Chiffre |
\w | [a-zA-Z0-9_] | Caractère de « mot » |
Cette citation célèbre est un avertissement : les regex sont puissantes mais cryptiques. Un ^(?=.*[A-Z])(?=.*\d).{8,}$ est illisible pour un humain normal. Mais bien utilisées, elles restent l'outil le plus efficace pour manipuler du texte.
Une expression régulière, c'est bien joli, mais comment un ordinateur fait-il pour décider si une chaîne correspond ? La réponse, c'est les automates finis.
L'idée est simple : on transforme la regex en une machine abstraite composée d'états et de transitions. On lui donne la chaîne caractère par caractère, et si à la fin on est dans un état « acceptant », la chaîne est reconnue.
Le plus facile à construire à partir d'une regex. Il a deux super-pouvoirs :
Exemple : la regex a(b|c)* donne le NFA suivant (en ASCII, parce que le vrai SVG coûte 50 lignes de code) :
La construction systématique d'un NFA à partir d'une regex s'appelle la construction de Thompson (Ken Thompson, 1968). Chaque opération de base correspond à un petit motif d'automate :
r₁r₂ → l'automate de r₁ suivi de celui de r₂.
r₁|r₂ → deux branches parallèles avec ε-transitions.
r* → une boucle (ε vers l'entrée de r, ε pour sortir).
Le NFA, c'est bien, mais il a un problème : pour savoir si une chaîne est reconnue, il faut explorer tous les chemins possibles en parallèle. C'est inefficace. La solution : transformer le NFA en DFA (Automate Fini Déterministe), où chaque état ne peut avoir qu'une seule transition par caractère.
La transformation s'appelle la construction par sous-ensembles (ou « powerset construction »). L'idée :
Résultat : un DFA avec potentiellement exponentiellement plus d'états que le NFA (mais en pratique, c'est très raisonnable).
re) utilisent
un hybride : une simulation de NFA avec backtracking et mémoïsation,
ce qui donne le meilleur des deux mondes — sauf quand le backtracking
explose (malédiction du (a*)*b).
Une fois le DFA construit, on peut le minimiser en fusionnant les états équivalents (algorithme de Moore ou de Brzozowski). Le résultat est un automate minimal unique pour un langage donné. C'est beau, c'est optimal, et ça s'enseigne en cours de théorie des langages (vous avez suivi, n'est-ce pas ?).
grep — Global Regular Expression Print
grep est l'outil Unix le plus légendaire. Inventé par
Ken Thompson en 1974 (en une nuit, paraît-il, pour
aider Ed à chercher du texte), il permet de filtrer les lignes
d'un fichier qui correspondent à une regex.
bash# Chercher toutes les lignes contenant "error" grep 'error' log.txt # Compter le nombre de lignes avec "error" grep -c 'error' log.txt # Afficher le numéro de ligne grep -n 'error' log.txt # Inverser : lignes qui ne contiennent PAS "error" grep -v 'error' log.txt # Ignorer la casse grep -i 'error' log.txt
Par défaut, grep utilise les regex de base (BRE), où
+, ?, |, ()
doivent être échappés. Avec -E (ERE), on écrit
naturellement :
bash# Lignes contenant "foo" ou "bar" grep -E 'foo|bar' data.txt # Adresses email (version simple) grep -E '\b\w+@\w+\.\w+\b' emails.txt # Lignes commençant par '#' (commentaires) grep -E '^#' config.ini # Numéros de téléphone français (10 chiffres, séparés ou non) grep -E '(0[1-9])([-. ]?[0-9]{2}){4}' annuaire.txt
Imaginez un fichier de log Apache avec des milliers de lignes. Vous voulez trouver les IP qui ont fait des requêtes POST en erreur 500 entre 14h et 15h :
bash# Un petit coup de grep bien senti grep -E '14:(0[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9])' access.log | \ grep -E 'POST' | \ grep -E ' 500 ' | \ grep -Eo '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | \ sort | uniq -c | sort -rn # Résultat : liste des IP défaillantes, triées par fréquence
grep vient
de la commande g/re/p de l'éditeur ed.
La commande signifiait : « Globalement, cherche la Regex, et
imprime (Print) les lignes. » Ken Thompson a tellement aimé ce
pattern qu'il en a fait un outil dédié. Un autre informaticien aurait
appelé ça grp. Mais Ken, c'est Ken.
| Commande | Usage |
|---|---|
grep -P | Perl-compatible regex (PCRE) — lookaheads, backreferences |
grep -r | Parcourt récursivement les dossiers |
grep -l | Affiche seulement les noms de fichiers qui matchent |
rg (ripgrep) | Version Rust, ultra-rapide, ignorer les .gitignore |
ag (the_silver_searcher) | Alternative rapide, très utilisée |
sed — Stream EDitor
Si grep est le couteau suisse de la recherche,
sed est celui de la transformation.
sed lit un fichier ligne par ligne, applique des
transformations, et écrit le résultat. Sans ouvrir le fichier dans
un éditeur. Sans souris. Sans GUI. Rien que du terminal.
s
La commande la plus utilisée de sed est s/ :
substitution. Le pattern classique :
s/regex/remplacement/drapeaux.
bash# Remplacer "foo" par "bar" sur chaque ligne sed 's/foo/bar/' fichier.txt # Remplacer globalement (pas juste la première occurence par ligne) sed 's/foo/bar/g' fichier.txt # Sauvegarder le résultat dans un nouveau fichier sed 's/foo/bar/g' fichier.txt > nouveau.txt # Modifier le fichier en place (-i = in-place) sed -i 's/foo/bar/g' fichier.txt
Avec \(…\) (BRE) ou (…) (ERE avec -E),
on capture des parties de la ligne et on les réutilise dans le
remplacement avec \1, \2, etc.
bash# Inverser "Nom, Prenom" → "Prenom Nom" sed -E 's/^(.+), (.+)$/\2 \1/' annuaire.txt # Standardiser des numéros de téléphone : 06.12.34.56.78 → 0612345678 sed -E 's/0([1-9])[.-]?([0-9]{2})[.-]?([0-9]{2})[.-]?([0-9]{2})[.-]?([0-9]{2})/0\1\2\3\4\5/g' contacts.txt # Ajouter une balise HTML autour d'un mot sed -E 's/\b(urgent)\b/\1<\/strong>/gi' emails.txt
sed peut aussi supprimer des lignes,
les insérer, les imprimer
conditionnellement, et même écrire des scripts entiers :
bash# Supprimer les lignes vides sed '/^$/d' fichier.txt # Supprimer les lignes 10 à 20 sed '10,20d' fichier.txt # Afficher seulement les lignes 5 à 8 sed -n '5,8p' fichier.txt # Ajouter une ligne "<hr>" après chaque ligne de titre sed '/^# /a <hr>' article.md
Vous avez un fichier CSV mal formé, avec des guillemets qui trainent, et vous voulez le nettoyer pour le cours de Web Sémantique :
bash# Entrée : "Jean DUPONT", "jean@email.com", "32", "Paris" # Problème : guillemets, espaces en trop, valeurs mixées # 1. Enlever les guillemets sed -E 's/"([^"]*)"/\1/g' data.csv | \ # 2. Nettoyer les espaces après les virgules sed -E 's/, +/,/g' | \ # 3. Remplacer les points-virgules par des virgules (fichier européen) sed 's/;/,/g' | \ # 4. Ne garder que les lignes avec une adresse email valide grep -E '^[^,]+,[^,]+@[^,]+\.\w+,' # Résultat : un CSV propre, prêt à être parsé par Python
sed est le genre d'outil que vous utilisez une fois par
mois, mais quand vous en avez besoin, rien d'autre ne fait le travail. »
— Utilisateur Unix anonyme (probablement)
sed vs awk
awk est le grand frère de sed : il sait
faire tout ce que fait sed, plus du
traitement par colonnes, des variables, des conditions, et des
boucles. La règle empirique :
sedawk '{print $1, $3}'awkMaintenant que vous êtes incollables sur les regex, voici exactement où et comment ces connaissances vous serviront dans les 5 cours :
re.split(), re.findall() pour découper
le texte en mots. Filtrage de stopwords, extraction de motifs,
cleaning de corpus. Nos amis \w+ et [^a-z].
Nettoyage des documents avant indexation : regex pour normaliser la casse, enlever la ponctuation, gérer les contractions.
Le parseur de requêtes ET/OU utilise des regex pour tokeniser la requête utilisateur. Et notre AST peut s'inspirer du parseur d'expressions régulières.
Parseur Turtle : reconnaître les URI, les littéraux avec types et langues ("..."^^xsd:int, "..."@fr). Le parseur de Prolog utilise aussi des regex pour reconnaître les termes, variables, prédicats.
Parsing d'ontologies, validation de syntaxe, extraction d'annotations. Les IRIs, les préfixes, les chaînes encodées.
Nettoyage des données CSV avant import dans Fuseki. Validation de formats (email, téléphone, prix). Filtrage des produits par regex dans le frontend.
python# Recherche.py — tokenisation (Cours #1) import re mots = re.findall(r'\w+', texte.lower()) # Nettoyage : enlever la ponctuation, normaliser propre = re.sub(r'[^a-z0-9\s]', '', texte.lower()) # Recherche booléenne : parser la requête ET/OU tokens = re.split(r'\s+(ET|OU)\s+', requete) # Mini parseur Turtle (Cours #2) — reconnaître un littéral typé motif_litteral = re.compile( r'("(?:[^"\\]|\\.)*")(?:\^\^(\w+:\w+)|@(\w+))?' )
Avant d'attaquer le Cours #1, vous devriez être capable d'écrire sans documentation les regex suivantes :
| Objectif | Regex attendue |
|---|---|
| Une URL HTTP/HTTPS | https?://\S+ |
| Un nombre décimal (européen) | -?\d+(,\d+)? |
| Un tag HTML | <[^>]+> |
| Une date JJ/MM/AAAA | \d{2}/\d{2}/\d{4} |
| Lignes vides | ^\s*$ |
.split()
serait juste une méthode qui coupe aux espaces. Avec elles, vous
pouvez découper le texte de n'importe quelle manière,
extraire n'importe quelle information, et nettoyer
n'importe quel corpus.