Cours n° 15, 16 février 2012

Jean-François Perrot

Expressions régulières en Perl

  1. Le point et son interprétation

  2. Autres flags (ou suffixes, ou options)

  3. Exemple d'utilisation du point et de l'option e
    1. Scénario (histoire vraie)
    2. Tâche à effectuer
    3. Principe d'une solution
    4. Réalisation en Perl

  4. Exercices 
    1. Dans le prolongement de l'examen de janvier 2009
    2. Améliorer certains textes dans Frantext


Le point et son interprétation

  1. Le méta-caractère "." désigne un caractère quelconque, mais qui n'est pas le saut de ligne "\n".
    Son emploi est commode quand on ne veut pas se casser la tête pour spécifier toutes les classes de caractères
    qui peuvent se trouver à un endroit donné.
    On en trouvera un exemple d'emploi plus loin.

  2. La restriction relative à "\n" s'explique par le fait qu'en règle générale les e.r. respectent la structure de ligne.
    Rappelons à ce propos que la ligne de texte reste un unité irremplaçable dans la communication entre l'homme et la machine.
    Par exemple, lorsqu'un logiciel détecte une erreur dans un texte, l'utilisateur est bien aise qu'on lui dise à quelle ligne se trouve cette erreur !
    D'autre part, tous les langages de programmation modernes proposent des dispositifs pour lire un fichier ligne à ligne
    (en Perl, l'opérateur "<>", en PHP la fonction file, en Java la méthode readLine()).
    Il n'est donc pas surprenant que la portée d'une e.r. soit normalement limitée à une ligne,
    et que par conséquent le caractère "\n" garde un statut exceptionnel.

  3. Cette restriction peut être levée par l'emploi du flag s : voir plus loin.

Autres flags (ou suffixes, ou options)

Rappelons qu'il s'agit d'indicateurs placés après le dernier séparateur (le second pour un filtrage, le troisième pour une substitution),
dont la présence modifie le comportement du moteur d'e.r. (cf. cours n° 8).
Nous avons déjà vu le suffixe g (comme global) qui applique le filtrage ou la substitution à toutes les sous-chaînes maximales fitrées par l'e.r.
En voici quelques autres :
  1.  Le suffixe i supprime la distinction majuscules/minuscules dans le filtrage : il le rend insensible à la casse
    en anglais case-insensitive - d'où son nom.

    Exemple : le test  $chn =~ m/0x([0-9a-f]+)/i  est vrai aussi bien pour $chn = "0xABC" que pour $chn = "0Xabc",
    alors que pour  $chn =~ m/0x([0-9a-f]+)/ (sans le suffixe i) il faut, par exemple, $chn = "0xabc".

  2. Le suffixe m modifie la signification des ancres "^" et "$" (cf. cours n° 9).
    Il indique que la chaîne traitée comporte plusieurs lignes (m comme multiligne), et il fournit le moyen de gérer cette structure : 
    "^" marque un début de ligne et "$" une fin de ligne (au lieu du début et de la fin de la chaîne toute entière) -
    et le point ne filtre toujours pas "\n".
    Autre formulation :
    ce suffixe a pour effet d'appliquer le filtrage (ou la substitution) ligne à ligne sur une chaîne de plusieurs lignes ;
    il s'emploie donc volontiers en conjonction avec le suffixe g.

  3. Le suffixe s modifie la signification du point.
    Au contraire de m, il indique que la chaîne traitée doit être vue comme une seule ligne.
    "^" marque le début de la chaîne, "$" la fin de la chaîne - comme d'ordinaire - et le point filtre "\n".

    Voici un double exemple pour illustrer le fonctionnement de ces deux suffixes antagonistes,
    adapté de la page 'multiline' or 'single string' in regex? :
    my $chn1 = "Les sanglots longs\ndes violons\nde l'automne bercent mon cœur\nd'une langueur\nmonotone";
    my $chn2 = $chn1; # deux exemplaires de la même chaîne
    my $er = '^(.*)$';

    $chn1 =~ s/
    $er/**$1--/mg;
    print $chn1;
    print "\n---------\n";
    $chn2 =~ s/$er/**$1--/s;
    print $chn2;
    envoie en sortie :
    **Les sanglots longs--
    **des violons--
    **de l'automne--
    **bercent mon cœur--
    **d'une langueur--
    **monotone--
    ---------
    **Les sanglots longs
    des violons
    de l'automne
    bercent mon cœur
    d'une langueur
    monotone--


    Note 1 : exécutez le code, et faites varier les suffixes pour vous assurer que vous avez tout compris !

    Note 2 : ces vers se trouvent au début de la chanson d'automne de Verlaine. Ils ont servi à annoncer le débarquement du 6 juin 1944 en Normandie.
     
  4. Le suffixe e ne s'applique qu'à une substitution : il indique que la chaîne substituée ne doit pas être prise verbatim,
    mais qu'elle doit être vue comme une expression Perl, et que c'est la valeur de cette expression qui doit être substituée à la sous-chaîne filtrée
    (e comme évaluer).
    Notamment, cela permet d'appliquer une fonction aux valeurs des variables spéciales $1, $2, etc.
    Voyez un exemple ci-après.
Plusieurs suffixes peuvent être employés simultanément (sauf m et s, qui sont incompatibles).
Sylvain Lhullier donne l'exemple suivant :
$s =~ s/0x([0-9a-f]+)/hex($1)/gei;
transforme tous les nombres hexadécimaux en nombres décimaux dans la [chaîne valeur de la] variable $s.

Application : comment faut-il modifier le code du §3 ci-dessus pour obtenir la sortie suivante ?
**LES SANGLOTS LONGS--
**DES VIOLONS--
**DE L'AUTOMNE BERCENT MON CœUR--
**D'UNE LANGUEUR--
**MONOTONE--
C'est très simple !

Exemple d'utilisation du point et de l'option e

  1. Scénario (histoire vraie)

    Une petite société produit et vend un logiciel dans l'interface utilisateur duquel apparaissent de nombreux messages en français.
    Pour vendre son logiciel à l'étranger, notamment en Grèce, elle dispose d'un fichier XML où figurent toutes les phrases-clefs du système
    ainsi que leurs traductions. Le codage des caractères de ce fichier est Latin-1, ce qui pose un problème pour le grec.
    En fait, les textes grecs ont été saisis en IS0-8859-7, ce qui les rend illisibles - mais l'information est préservée.
    Voici un extrait du fichier tel qu'il apparaît dans un éditeur de textes en Latin-1 : fichier ExGrec.xml

    <PhraseClef id="22" texte="Acquisition / Modification / Présentation de divers paramètres"/>
    ..........
    <PhraseClef id="758" texte="Veuillez indiquer votre mot de passe"/>
    ................
    <TraductionPhrase  id="22" texte ="ÅéóáãùãÞ/Ðáñáìåôñïðïßçóç äéáöüñùí ðáñáìÝôñùí"/>
    ..........
    <TraductionPhrase  id="758" texte ="Äùóôå ôïí êùäéêü óáò"/>


  2. Tâche à effectuer

    On demande de recoder le fichier en UTF-8, avec naturellement la restitution des caractères grecs.
    Notre extrait deviendra : fichier TradGrec.xml
    <PhraseClef id="22" texte="Acquisition / Modification / Présentation de divers paramètres"/>
    ..........
    <PhraseClef id="758" texte="Veuillez indiquer votre mot de passe"/>
    ................
    <TraductionPhrase  id="22" texte ="Εισαγωγή/Παραμετροποίηση διαφόρων παραμέτρων"/>
    ..........
    <TraductionPhrase  id="758" texte ="Δωστε τον κωδικό σας"/>

  3. Principe d'une solution

    Passer du codage ISO-8859-7 à Unicode est facile : en effet, la séquence des numéros Unicode dans le bloc Greek and Coptic
    suit exactement l'énumération des caractères grecs dans la plage B5-FE de la table ISO, à savoir :
     Ά, ·, Έ, Ή, Ί, », Ό, ½, Ύ, Ώ, ΐ, Α, Β, Γ, Δ, Ε, Ζ, Η, Θ, Ι, Κ, Λ, Μ, Ν, Ξ, Ο, Π, Ρ, -,
    Σ, Τ, Υ, Φ, Χ, Ψ, Ω, Ϊ, Ϋ, ά, έ, ή, ί, ΰ, α, β, γ, δ, ε, ζ, η, θ, ι, κ, λ, μ, ν, ξ, ο, π, ρ, ς, σ, τ, υ, φ, χ, ψ, ω, ϊ, ϋ, ό, ύ, ώ
    Les caractères '»', '½', et la lacune marquée '-' correspondent à des sauts dans les numéros Unicode.
    Évidemment, ce n'est pas un hasard ! La norme ISO date de 1987, la première version d'Unicode remonte à 1991,
    l'une et l'autre reprennent la norme nationale grecque ELOT 928 de 1986. D'où le parallélisme observé, que nous allons exploiter.

    Sachant que le numéro Unicode du caractère 'GREEK CAPITAL LETTER ALPHA WITH TONOS', premier caractère de l'énumération, est 902 (0x386),
    et que dans la table ISO son octet est B6 = 182, on en déduit la formule suivante pour calculer le numéro Unicode d' un caractère grec :
    soit k la valeur numérique de l'octet qui code ce caractère en ISO-8859-7, le rang de ce caractère dans l'énumération ci-dessus est égal à k-182,
    et son numéro Unicode est k-182+902 = k+720.

  4. Réalisation en Perl

    Elle se fait en deux temps :
    1. Écriture d'une fonction convertissant en caractères grecs une chaîne de caractères lue en Latin-1, mais provenant d'ISO-8859-7.
      Appelons-la iso2uni.

    2. Mise en œuvre de cette fonction via une expression régulière avec l'option e, dans une boucle de lecture du fichier ligne à ligne.
      Voici l'instruction essentielle de la boucle :

      $ligne =~ s/(<TraductionPhrase  id="\d+" texte =")(.*)("\/>)/$1.iso2uni($2).$3/e;

      • On emploie trois paires de parenthèses pour alléger l'écriture, mais seule la deuxième est indispensable pour fournir son argument à iso2uni.
      • Le recours au point permet d'accepter aussi bien les lettres que d'éventuels chiffres, espaces ou ponctuations : 
        tout ce qui se trouve entre les guillemets est bon à prendre !

    Réalisation de la fonction iso2uni : on tient compte de la possible présence de caractères ASCII

    sub iso2uni($){ # prend une chaîne en grec ISO lu comme du Latin-1 et la renvoie en grec (Unicode)
        my($gr) = @_;
       
        my $res="";
        my @tabcar = split(//, $gr);
        foreach my $car ( @tabcar ){
            my $k = ord($car); # entier
            if( $k < 128 ){ # ASCII
                $res .= $car;
            }else{ # caractère grec   
                $res .= chr($k+720);
            }
        }
        return $res;
    } #iso2uni



    Réalisation de la boucle : il faut bien prendre garde à l'ouverture des deux fichiers :
    En outre, il faut se méfier de la réalisation du saut de ligne : dans le fichier d'entrée, qui vient de Windows, c'est CR+LF
    et dans le fichier de sortie on le normalise en LF conformément à la norme XML.

    sub tradGrec($$){ #deux noms de fichier, source et but
        my($fichIn, $fichOut) = @_;
       
        open(ENTREE, "<:encoding(iso-8859-1)", $fichIn);
        open(SORTIE, ">:encoding(UTF-8)", $fichOut);

        my $ligne = <ENTREE>; # sauter l'en-tête XML
        print (SORTIE '<?xml version="1.0" encoding="utf-8"?>'."\n");

        while( my $ligne = <ENTREE> ){
            chomp($ligne); # LF
            chop($ligne); # CR
            $ligne =~ s/(<TraductionPhrase  id="\d+" texte =")(.*)("\/>)/$1.iso2uni($2).$3/e;
            print (SORTIE "$ligne\n");
        }
    }#tradGrec


    Le texte complet.

Exercices 

  1. Dans le prolongement de l'examen de janvier 2009

  2. Améliorer certains textes dans Frantext

    Certains textes du corpus Frantext portent la marque de l'époque où ils ont été saisis : 
    On se propose d'écrire un programme Perl qui rectifie ces errements, aujourd'hui inacceptables.
    En voici un, adaptez-le à votre convenance !