Cours n° 12, 19 décembre 2013
Expressions régulières en Perl - 2 (version révisée le 26/12/2013)
- Extraire
la sous-chaîne filtrée ?
- Les
sous-expressions parenthésées
- Le
filtrage renvoie la liste des sous-chaînes filtrées
par les sous-motifs.
- Application
: les nombres (v.2)
- Application
: les commentaires (v. 2)
- Récupération des sous-chaînes filtrées dans des variables locales
- Extraire
toutes les sous-chaînes filtrées ?
- Le
suffixe (flag) g
- Application
: les commentaires (v.3)
- Le suffixe g en présence de parenthèses capturantes
- Application
: les nombres (v. 3)
Comment faire pour extraire les nombres eux-mêmes
des lignes qui les contiennent ? idem pour les
commentaires...
C'est par une modification de l'e.r. elle-même que nous y
parviendrons...
-
Il arrive souvent qu'on soit intéressé par une partie seulement de la
structure décrite par une exp. reg..
Par exemple, dans la chaîne "j'ai 20 ans
",
c'est le nombre 20 (l'âge) qui nous intéresse
mais la chaîne de caractères "20" ne désigne un âge que dans le
contexte décrit par l'exp. reg. "/j'ai [0-9]{2} ans/
".
Pour traiter ce genre de situations, les exp. reg. de Perl introduisent
la notion de sous-motif ,
qui n'est pas autre chose qu'une sous-expression placée entre
parenthèses.
Dans notre exemple, on écrira "/j'ai ([0-9]{2})
ans
/".
On peut avoir autant de sous-motifs parenthésés qu'on veut.
Ils sont numérotés de 1 à n par l'ordre de gauche à droite de leurs
parenthèses ouvrantes.
Ainsi, dans "/(j'ai) (([0-9]{2})
ans)
/" les sous-motifs sont dans l'ordre
(j'ai)
(([0-9]{2})
ans)
([0-9]{2})
Dans cette perspective, les parenthèses jouent un rôle sémantique,
en désignant des sous-motifs significatifs.
Mais les parenthèses conservent leur rôle syntaxique, à cause
de la priorité des opérateurs :
il peut arriver que la syntaxe exige des parenthèses sans signification
comme sous-motifs,
par exemple, dans l'expression des nombres ([1-9][0-9]*|0([0-7]+|x[0-9A-F]+))
- les parenthèses extérieures sont significatives (elles
délimitent le motif nombre)
- les parenthèses intérieures, autour de
[0-7]+|x[0-9A-F]+
ne servent qu'à mettre 0
en facteur,
leur rôle est purement syntaxique ; nous verrons plus loin que cette
situation est effectivement gênante.
La solution proposée par Perl est de compliquer la syntaxe des e.r. en
introduisant la notation "(?:...)
"
pour marquer un parenthésage "purement syntaxique", qui ne crée pas de
sous-motifs.
Avec cette notation, notre expression devient :
([1-9][0-9]*|0(?:[0-7]+|x[0-9A-F]+))
-
Il faut pour cela être "en contexte de liste", comme on dit en Perl,
car en contexte scalaire la valeur renvoyée est booléenne, comme nous
le savons bien.
Par exemple, l'affectation :
@tab = (
"j'ai
20 ans
" =~ m/(j'ai)
(([0-9]{2})
ans)/ );
donne à la variable-tableau @tab
la
valeur-liste ('j'ai'
, '20
ans', '20')
.
L'ordre d'apparition des sous-chaînes filtrées dans la liste est, bien
entendu, celui des sous-motifs,
c'est-à-dire celui de leurs parenthèses ouvrantes dans l'e.r. de gauche
à droite.
Attention ! La longueur de ce tableau est fixée
par la structure de l'e.r. :
pour /(j'ai)
(([0-9]{2})
ans)/
, ce sera 3.
Elle est fixée avant le filtrage effectif, ce qui
est contraire à l'intuition commune,
si bien qu'il peut arriver qu'après filtrage certaines cases du tableau
ne soient pas remplies !
Si on tente de s'adresser à elles, on recevra le déplaisant message Use
of uninitialized value...
CAVEAT PROGRAMMATOR !
Le remède, en cas de doute, est de faire le test if( defined(la_variable)
){...
}
-
extraire le premier nombre de chaque ligne
qui en contient (au moins) un.
sub extrnb2($){ #arg. nom de fichier
my ($fich) = @_;
open(ENTREE, "<$fich");
my $er = '([1-9][0-9]*|0([0-7]+|x[0-9A-F]+))'; #
avec parenthèses extérieures
my @tablignes = <ENTREE>;
my @tabnb = grep(/$er/, @tablignes);
foreach my $lgn ( @tabnb ){
my @tab = ($lgn =~ m/$er/);
print ("longueur du tableau =
".scalar(@tab)." 1er = $tab[0]\n");
}
}#extrnb
fichier extrnb2.pl
,
donnée
pour essais nb.txt
.
-
extraire le premier commentaire du fichier
(qui n'en contient peut-être aucun !).
sub extrToutComm($){ #arg. nom de fichier
my ($fich) = @_;
open(ENTREE, "<$fich");
my $erc = '/[*]([^/*]|[/]|[*]+[^/*])*[*]+/'; # sans '\'
my $erfl = '//[^\n]*\n'; # idem
my $er = "($erc|$erfl)"; # avec parenthèses
my @tablignes = <ENTREE>;
my $txt = join('', @tablignes);
my
@tab = ($txt =~ m=$er=); # avec '=' comme séparateur
if(
defined($tab[0]) ){
print ("$tab[0]\n");
}else{
print ("no match !\n");
}
}#extrToutComm
fichier extrToutComm.pl
,
donnée
supplémentaire pour essais exCom3.txt
.
-
Récupération des sous-chaînes filtrées dans des variables locales
L'affectation d'un match à un tableau
n'est pas la
seule manière d'accéder à la collection des sous-chaînes
filtrées par les différents sous-motifs d'une e.r..
Cette collection est également accessible dans le second membre d'une
substitution, au moyen de variables "spéciales"
(comme il y en a tant en Perl !) nommées $1
, $2
...
, les numéros correspondant au rang du sous-motif filtrant.
Reprenons notre exemple favori : la séquence
my $chn
= "j'ai 20 ans";
my $er
= "(j'ai) (([0-9]{2})
ans)" # 3 sous-motifs
$chn =~ s/$er/$1
eu $2 au mois de mai dernier, $3
nombre magique !/;
donne à la variable $chn
la
valeur j'ai eu 20 ans au mois de mai
dernier, 20 nombre magique !
Les variables spéciales $i
sont encore
utilisables dans la suite du bloc où a lieu l'opération (substitution
ou filtrage)
- tant qu'une autre opération n'est pas venue altérer leur contenu.
Ainsi :
if( $chn =~ m/$er/ ){ #match
print "$1-$2-$3-$chn\n";
}else{
print( "No\n");
}
imprime j'ai-20 ans-20-j'ai 20 ans
Mais attention ! Le nombre de ces variables spéciales est limité à 9.
Ici, c'est par une modification non pas de l'e.r. elle-même, mais
de son contexte d'emploi, que nous y
parviendrons...
-
Les suffixes, ou flags, ou modifers, sont des indications placées après le délimiteur final de
l'exp.reg. et qui modifient sa signification.
Celui qui nous intéresse ici est g
(comme global) : on demande toutes les occurrences.
Dans le cas d'un filtrage, le suffixe vient après le second séparateur
"/", dans celui d'une substitution, après le troisième.
Utiilisation :
- en substitution :
$txt =~ s/$er/uneChaîne/g;
à lire "substituer uneChaîne
à toutes les
sous-chaînes de $txt
conformes à l'expression valeur de $er
"
Voyez le paragraphe 2 ci-après.
- en filtrage :
$txt =~ m/$er/g
dont l'interprétation est moins évidente, et dépend du contexte :
- En contexte scalaire, l'expression a pour valeur
true
autant de fois qu'il y a de sous-chaines conformes à l'exp. reg. $er
.
On peut donc l'employer dans une boucle while
plutôt que
dans une conditionnelle :
la séquence
my $k = 0;
while(
$lgn =~ m/$er/g ) {
$k++;
}
donne comme valeur à la variable $k
le
nombre de sous-chaînes de $lgn
qui sont conformes à $er
.
- En contexte de liste, l'expression renvoie le
tableau de toutes les sous-chaînes de
$lgn
qui sont
conformes à $er
.
Ainsi, après :
my @tab = ( $lgn =~ m/$er/g );
scalar(@tab)
aura la même valeur que $k
après exécution de la séquence précédente.
-
Nour arrivons enfin à notre but : supprimer tous
les commentaires !
sub supprToutCommG($){ #arg. nom de fichier
my ($fich) = @_;
open(ENTREE, "<$fich");
my $erc = '/[*]([^/*]|[/]|[*]+[^/*])*[*]+/'; # sans '\'
my
$erfl = '//[^\n]*\n'; # idem
my
$er = "$erc|$erfl"; # disjonction ou réunion...
my @tablignes = <ENTREE>;
my $txt = join('', @tablignes);
$txt =~ s=$er= =g; # avec '=' comme séparateur et le
suffixe g !
print $txt;
}#supprToutCommG
fichier supprToutCommG.pl
,
mêmes données que ci-dessus.
Le suffixe g
en présence de parenthèses capturantes
- Substitution : rien de nouveau
- Filtrage :
- en contexte scalaire, avec une boucle
while
, on peut faire appel aux valeurs des variables $1
, $2
, etc dans le corps de la boucle,
à condition de bien vérifier qu'elles ont effectivement reçu une valeur avant de les utiliser.
Voici
un exemple - à vrai dire pas très naturel, car on va rarement mélanger
des entités numériques en décimal et en hexa (ici en gras)...
my $chn = 'عـنترة بن شداد';
my $er = '&#([0-9]+);|&#x([0-9A-F]+);';
# le sous-motif 1 capture les contenus des entités décimales
# le sous-motif 2 celui des entités hexadécimales
my $dec = 0;
my $hex = 0;
while ( $chn =~ m/$er/g ){
if( defined($1) ){
print "$1 - ";
$dec++;
}else{ #alors c'est l'autre
print hex($2)." = ";
$hex++;
}
}
print "\n$dec ent. dec. - $hex ent. hex.\n";
Exécution :
1593 - 1600 - 1606 = 1578 - 1585 - 1577 - 1576 = 1606 - 1588 = 1583 = 1575 - 1583 -
8 ent. dec. - 4 ent. hex.
- en contexte de liste, la situation se complique.
Par la vertu du suffixe g
, chacun des sous-motifs donne lieu à une liste, et les différentes listes sont fondues en une seule...
Il s'agit clairement d'une faiblesse propre à Perl, qui ne connaît pas
les tableaux de tableaux
(toutes les valeurs d'un tableau Perl doivent être scalaires), contrairement
à PHP.
À moins de faire appel à des références, Perl est donc incapable de
structurer le tableau @tab
en
sous-tableaux
correspondant chacun à une occurrence, comme le fait PHP.
Variante de l'exemple précédent (même chaîne, même exp. reg.):
my @tab = $chn =~ m/$er/g ;
foreach my $nb (@tab){
if( defined($nb) ){ # pour éviter "Use of uninitialized value $nb ..."
print ("$nb - ");
}else{
print ("??? - ");
}
}
print ("\nFini\n");
Exécution :
1593 - ??? - 1600 - ??? - ??? - 0646 - 1578 - ??? -
1585 - ??? - 1577 - ??? - ??? - 0628 - 1606 - ??? - ??? - 0634 - ??? -
062F - 1575 - ??? - 1583 - ??? -
Fini
Pour réaliser exactement la même opération que ci-dessus, un peu plus de soin sera nécessaire...
my $dec = 0;
my $hex = 0;
for( my $i = 0; $i < scalar(@tab); $i++ ){
if( defined($tab[$i]) ){
if( $i%2 == 0 ){ # il n'y a pas de fonctions "odd" et "even" en Perl
print ("$tab[$i] - ") ;
$dec++;
}else{
print(hex($tab[$i]).' = ');
$hex++;
}
}
}
print "\n$dec ent. dec. - $hex ent. hex.\n";
À titre de comparaison, voici la structure du tableau que produit PHP sur les mêmes données :
code PHP (le flag g
est inconnu en PHP):
<?php
$chn =
'عـنترة
بن شداد';
$er = '/&#([0-9]+);|&#x([0-9A-F]+);/';
preg_match_all($er, $chn, $tab); // équivaut à "@tab = $chn =~m/$er/g;" en Perl
print_r($tab);
?>
Résultat : au rang 0 les occurrences de l'expression tout entière, au rang i les occurrences du i-ème sous-motif (pour i = 1, 2, etc)
Array
(
[0] => Array
(
[0] => ع
[1] => ـ
[2] => ن
[3] => ت
[4] => ر
[5] => ة
[6] => ب
[7] => ن
[8] => ش
[9] => د
[10] => ا
[11] => د
)
[1] => Array
(
[0] => 1593
[1] => 1600
[2] =>
[3] => 1578
[4] => 1585
[5] => 1577
[6] =>
[7] => 1606
[8] =>
[9] =>
[10] => 1575
[11] => 1583
)
[2] => Array
(
[0] =>
[1] =>
[2] => 0646
[3] =>
[4] =>
[5] =>
[6] => 0628
[7] =>
[8] => 0634
[9] => 062F
[10] =>
[11] =>
)
)
-
Ici aussi nous arrivons enfin à notre but, à savoir extraire
tous les nombres d'un
fichier-texte.
La combinaison des techniques vues précédemment conduit à écrire :
sub extrnb2G($){ #arg. nom de fichier
my ($fich) = @_;
o
pen(ENTREE,
"<$fich");
my $er = '([1-9][0-9]*|0([0-7]+|x[0-9A-F]+))';
my @tablignes = <ENTREE>;
my @tabnb = grep(/$er/, @tablignes); # sans le flag 'g'
foreach my $lgn ( @tabnb ){
my @tab = ($lgn =~ m/$er/g);
# avec le flag 'g'
foreach my $nb ( @tab ){
if( defined($nb) ){
print ("$nb - ");
}
}
print ("\n");
}
}#extrnb2G
fichier extrnb2G.pl
.
En l'appliquant à une donnée d'une seule ligne comme "Un
nombre : 0xFFF, un autre 0123, et un autre 987
",
on obtient : 0xFFF - xFFF - 0123 - 123 - 987 -
alors qu'on attendait seulement 3 nombres : 0xFFF
- 0123 - 987 -
C'est parce que dans le tableau @tab
sont
mélangés les résultats du filtrage de l'expression complète
(parenthèses extérieures)
et de la sous-expression "([0-7]+|x[0-9A-F]+)
",
qui ne nous intéresse pas.
Répétons que la solution proposée par Perl est de compliquer la syntaxe
des e.r. en
introduisant la notation "(?:...)
"
pour marquer un parenthésage "purement syntaxique", qui ne doit pas
donner lieu à filtrage.
En rectifiant notre e.r. dans le code Perl ci-dessus :
my $er
= '([1-9][0-9]*|0(?:[0-7]+|x[0-9A-F]+))';
on obtient le résultat désiré.
Même problème si nous voulons réécrire l'exp. reg. '&#([0-9]+);|&#x([0-9A-F]+);'
en mettant les parties communes en facteur :
elle devient '&#(([0-9]+)|x([0-9A-F]+));'
où les parenthèses extérieures jouent un rôle purement syntaxique.
Il faut donc écrire '&#(?:([0-9]+)|x([0-9A-F]+));'
.