Cours n° 12, 19 décembre 2013

Jean-François Perrot

Expressions régulières en Perl - 2 (version révisée le 26/12/2013)

  1. Extraire la sous-chaîne filtrée ?
    1. Les sous-expressions parenthésées
    2. Le filtrage renvoie la liste des sous-chaînes filtrées par les sous-motifs.
    3. Application : les nombres (v.2)
    4. Application : les commentaires (v. 2)
    5. Récupération des sous-chaînes filtrées dans des variables locales

  2. Extraire toutes les sous-chaînes filtrées ?
    1. Le suffixe (flag) g
    2. Application : les commentaires (v.3)
    3. Le suffixe g en présence de parenthèses capturantes
    4. Application : les nombres (v. 3)

Extraire la sous-chaîne filtrée ?

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...
  1. Les sous-expressions parenthésées

    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
    1. (j'ai)
    2. (([0-9]{2}) ans)
    3. ([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]+))
    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]+))

  2. Le filtrage renvoie la liste des sous-chaînes filtrées par les sous-motifs.

    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) ){...}
  3. Application : les nombres (v.2)

    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.
  4. Application : les commentaires (v. 2)

    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.

  5. 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.

Extraire toutes les sous-chaînes filtrées ?

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...
  1. Le suffixe (flag) g

    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 :
  2. Application : les commentaires (v.3)

    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.

  3. 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 = '&#1593;&#1600;&#x0646;&#1578;&#1585;&#1577; &#x0628;&#1606; &#x0634;&#x062F;&#1575;&#1583;';
        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 = '&#1593;&#1600;&#x0646;&#1578;&#1585;&#1577; &#x0628;&#1606; &#x0634;&#x062F;&#1575;&#1583;';
           $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] => &#1593;
                    [1] => &#1600;
                    [2] => &#x0646;
                    [3] => &#1578;
                    [4] => &#1585;
                    [5] => &#1577;
                    [6] => &#x0628;
                    [7] => &#1606;
                    [8] => &#x0634;
                    [9] => &#x062F;
                    [10] => &#1575;
                    [11] => &#1583;
                )

            [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] =>
                )

        )


  4. Application : les nombres (v. 3)

    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) = @_;
        open(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]+));'.