• Aucun résultat trouvé

4 Impl´ ementation des expressions r´ eguli` eres

Le but de cette section est de d´ecrire une technique d’impl´ementation possible des expres-sions r´eguli`eres en Java. Il s’agit d’une premi`ere approche, beaucoup moins sophistiqu´ee que celle adopt´ee notamment par la biblioth`eque Java. Toutefois, on pourra, mˆeme avec des tech-niques simples, d´ej`a aborder les probl`emes de programmation pos´es et comprendre ((comment

¸ca marche )). De fait nous allons imiter l’architecture du package java.util.regexp et ´ecrire nous aussi un package que nous appelons regextout court.

Nous en profitons donc pour ´ecrire un package. Tous les fichiers source du package regex commencent par la ligne package regex ; qui identifie leur classe comme appartenant `a ce package. En outre, il est pratique de regrouper ces fichiers dans un sous-r´epertoire nomm´e justement regex.

4.1 Arbres de syntaxe abstraite

Nous reprenons les techniques de la section IV.4 sur les arbres de syntaxe abstraite. `A savoir nous d´efinissons une classeRedes nœuds de l’arbre de syntaxe abstraite.

package regex ; class Re {

private final static int EMPTY=0, CHAR=1, WILD=2, OR=3, SEQ=4, STAR=5 ; private int tag ;

private char asChar ; private Re p1, p2 ; private Re() {}

.. . }

Nous d´efinissons cinq sortes de nœuds, la sorte d’un nœud ´etant identifi´ee par son champ tag.

Des constantes nomm´ees identifient les cinq sortes de nœuds. La correspondance entre constante et sorte de nœud est directe, on note la pr´esence de nœuds(( WILD ))qui repr´esentent les jokers.

Ensuite nous d´efinissons tous les champs n´ecessaires, un champ asCharutile quand le motif est un caract`ere (tagCHAR), et deux champsp1etp2utiles pour les nœuds internes qui ont au plus deux fils. Enfin, le constructeur par d´efaut est red´efini et d´eclar´e priv´e.

On construira les divers nœuds en appelant des m´ethodes statiques bien nomm´ees. Par exemple, pour cr´eer un motif caract`ere, on appelle :

static Re charPat(char c) { // On ne peut pas nommer cette m´ethode (( char )) Re r = new Re() ;

r.asChar = c ; return r ; }

Pour cr´eer un motif r´ep´etition, on appelle :

static Re star(Re p) { Re r = new Re() ; r.p1 = p ;

return p ; }

Les autres autres m´ethodes de construction sont ´evidentes.

Les m´ethodes statiques de construction ne se limitent ´evidemment pas `a celles qui corres-pondent aux sortes de nœuds existantes. On peut par exemple ´ecrire facilement une m´ethode plus qui construit un motifp+commepp*.

static Re plus(Re p) { return seq(p, star(p)) ; }

Du point de vue de l’architecture, on peut remarquer que tous les champs et le construc-teur sont priv´es. Rendre le construcconstruc-teur priv´e oblige les utilisaconstruc-teurs de la classe Re appeler les m´ethodes statiques de construction, de sorte qu’il est garanti que tous les champs utiles dans un nœud sont correctement initialis´es. Rendre les champs priv´es interdira leur acc`es de l’ext´erieur de la classe Re. Au final, la politique de visibilit´e des noms est tr`es stricte. Elle renforce la s´ecurit´e de la programmation, puisque si nous ne modifions pas les champs dans la classe Re, nous pourrons ˆetre sˆurs que personne ne le fait. En outre, la classe Ren’est pas publique, son acc`es est donc limit´e aux autres classes du packageregex. La classeReest donc compl`etement invisible pour les utilisateurs du package.

4.2 Fabrication des expressions r´eguli`eres

Nous pr´esentons maintenant notre classe Pattern, un modeste remplacement de la classe ho-monyme de la biblioth`eque Java. Pour le moment nous ´evitons les automates et nous contentons de cacher un arbre Redans un objet de la classe Pattern.

package regex ;

/* Une classe Pattern simple : encapsulage d’un arbre de syntaxe abstraite */

public class Pattern { private Re pat ;

private Pattern(Re pat) { this.pat = pat ; } // Cha^ıne -> Pattern

public static Pattern compile(String patString) { Re re = Re.parse(patString) ;

return new Pattern(re) ; }

// Fabriquer le M atcher

public Matcher matcher(String text) { return new Matcher(pat, text) ; } }

Comme dans la classe de la biblioth`eque, c’est la m´ethode statique compile qui appelle le constructeur, ici priv´e. La partie la plus technique de la tˆache de la m´ethode compile est le passage de la syntaxe concr`ete contenue dans la chaˆıne patString `a la syntaxe abstraite repr´esent´e par un arbre Re, op´eration d´el´egu´ee `a la m´ethode Re.parse. Nous ne savons pas

´ecrire cette m´ethoded’analyse syntaxique (parsing). (cours INF 431). Mais ne soyons pas d´e¸cus,

nous pouvons d´ej`a par exemple construire le motif qui reconnaˆıt au moins k caract`eres c, en appelant la m´ethode atLeast suivante, `a ajouter dans la classe Pattern.

public static Pattern atLeast(int k, char c) { return new Pattern(buildAtLeast(k, c)) ; }

private static Re buildAtLeast(int k, char c) { i f (k <= 0) {

return Re.empty() ; } else i f (k == 1) {

return Re.charPat(c) ; } else {

return Re.seq

(Re.charPat(c),

Re.seq(Re.star(Re.wild()), buildAtLeast(k-1, c))) }

}

Enfin, la m´ethode matcher de de la classe Pattern se contente d’appeler le constructeur de notre modeste classe Matcher, que nous allons d´ecrire.

4.3 Filtrage

Le source de la classe Matcher (figure 6) indique que les objets contiennent deux champs pat ettext, pour le motif et le texte `a filtrer. Comme on pouvait s’y attendre, le constructeur Matcher(Re pat, String text)initialise ces deux champs. Mais les objets comportent trois champs suppl´ementaires, mStart,mEnd et regStart.

ˆ La valeur du champregStartindique l’indice danstextdu d´ebut de la recherche suivante, c’est-`a-dire o`u la m´ethodefinddoit commencer `a chercher une sous-chaˆıne filtr´ee parpat.

Ce champ permet donc aux appels successifs defind de communiquer entre eux.

ˆ Les champsmStartetmEnd identifient la position de la derni`ere sous-chaˆıne detext dont un appel `a find a d´etermin´e que le motif pat la filtrait. La convention adopt´ee est celle de la m´ethode substring des objets String (voir la section B.6.1.3). Les deux champs servent `a la communication entre un appel `a find et un appel subs´equent `a group (voir la fin de la section 3.3).

La m´ethode find est la plus int´eressante, elle cherche `a identifier une sous-chaˆıne filtr´ee par pat, `a partir de la position regStart et de la gauche vers la droite. La technique adopt´ee est franchement na¨ıve, on essaie tout simplement de filtrer successivement toutes les sous-chaˆınes commen¸cant `a une positon donn´ee (start) des plus longues `a la chaˆıne vide. On renvoie true (apr`es mise `a jour de l’´etat duMatcher), d`es qu’une sous-chaˆıne filtr´ee est trouv´ee. Pour savoir si une sous-chaˆınetext[start. . .end[est filtr´ee, on fait appel `a la m´ethode statiqueRe.matches.

Notons que c’est notre parti-pris de rendre priv´es tous les champs de l’arbre de syntaxe des expressions r´eguli`ere qui oblige `a ´ecrire toute m´ethode qui a besoin d’examiner cette structure comme une m´ethode de la classe Re.

Exercice 5 Ecrire la m´ethode´ matches de la classe Matcher. On suivra la sp´ecification de la classeMatcherde la biblioth`eque. `A savoir, l’appelmatches()teste le filtrage de toute l’entr´ee par le motif et on peut utiliser group() pour retrouver la chaˆıne filtr´ee.

Solution.C’est simple : un appel `aRe.matcheset on affecte les champsmStart etmEnd selon le r´esultat.

Fig. 6 – Notre classe Matcher package regex ;

public class Matcher { private Re pat ; private String text ;

// Les recherches commencent `a cette position dans text private int regStart ;

// La derni`ere sous-cha^ıne filtr´ee est text[mStart...mEnd[

private int mStart, mEnd ; Matcher(Re pat, String text) {

this.pat = pat ; this.text = text ;

regStart = 0 ; // Commencer `a filtrer `a partir du d´ebut mStart = mEnd = -1 ; // Aucun motif encore reconnu

}

// Renvoie la derni`ere sous-cha^ıne filtr´ee, si il y a lieu public String group() {

i f (mStart == -1) throw new Error("Pas de sous-cha^ıne filtr´ee") ; return text.substring(mStart, mEnd) ;

}

// M´ethode de recherche des sous-cha^ınes filtr´ees a peu pr`es // conforme `a celle des Matcher de java.util.regex

public boolean find() {

for (int start = regStart ; start <= text.length() ; start++) for (int end = text.length() ; end >= start ; end--) {

i f (Re.matches(text, pat, start, end)) { mStart = start ; mEnd = end ;

regStart = mEnd ; // Le prochain find commencera apr`es celui-ci return true ;

} }

mStart = mEnd = -1 ; // Pas de sous-cha^ıne reconnue regStart = 0 ; // Recommencer au d´ebut, bizarre return false ;

} }

public boolean matches() {

i f (Re.matches(text, pat, 0, text.length())) { mStart = 0 ;

mEnd = text.length() ; return true ;

} else {

mStart = mEnd = -1 ; return false ;

} }

Pour ´ecrire la m´ethode matches de la classe Re, nous allons distinguer les divers motifs possibles et suivre la d´efinition de pm de la figure 3.

// Test de pat text[i. . .j[

static boolean matches(String text, Re pat, int i, int j) { switch (pat.tag) {

.. . }

throw new Error ("Arbre Re incorrect") ; }

Notons bien que text[i. . .j[ est la chaˆıne dont nous cherchons `a savoir si elle est filtr´ee par pat. La longueur de cette chaˆıne estj-i. Nous ´ecrivons maintenant le source du traitement des cinq sortes de motifs possibles, c’est `a dire la liste des cas du switchci-dessus. Le cas des motifs vide, des caract`eres et du joker est rapidement r´egl´e.

case EMPTY:

return i == j ; case CHAR:

return i+1 == j && text.charAt(i) == pat.asChar ; case WILD:

return i+1 == j ;

En effet, le motif vide filtre la chaˆıne vide et elle seule (j−i = 0), le motif caract`ere ne filtre que la chaˆıne compos´ee de lui mˆeme une fois, et le joker filtre toutes les chaˆınes de longueur un.

Le cas de l’alternative est ´egalement assez simple, il suffit d’essayer les deux termes de l’alternative (regles OrLeft etOrRight).

case OR:

return matches(text, pat.p1, i, j) || matches(text, pat.p2, i, j) ; La s´equence (ruleSeq) demande plus de travail. En effet il faut essayertoutes les d´ecompositions en pr´efixe et suffixe de la chaˆıne test´ee, faute de quoi nous ne pourrions pas renvoyer false avec certitude.

case SEQ:

for (int k = i ; k <= j ; k++) {

i f (matches(text, pat.p1, i, k) && matches(text, pat.p2, k, j)) return true ;

}

return false ;z

Et enfin, le cas de la r´ep´etitionq*est un peu plus subtil, il est d’abord clair (r`egleStarEmpty) qu’un motif q* filtre toujours la chaˆıne vide. Si la chaˆıne text[i. . .j[ est non-vide alors on cherche `a la d´ecomposer en pr´efixe et suffixe et `a appliquer la r`egleStarSeq.

case STAR:

i f (i == j) { return true ; } else {

for (int k = i+1 ; k <= j ; k++) {

i f (matches(text, pat.p1, i, k) && matches(text, pat, k, j)) return true ;

}

return false ; }

On note un point un peu subtil, dans le cas d’une chaˆıne non-vide, on ´evite le cas k = j qui correspond `a une division de la chaˆıne test´ee en pr´efixe vide et suffixe complet. Si tel n’´etait pas le cas, la m´ethode matches pourrait ne pas terminer. En effet, le second appel r´ecursif matches(text, pat, k, j) aurait alors les mˆemes arguments que lors de l’appel. Un autre point de vue est de consid´erer que l’application de la r`egleStarSeq`a ce cas est inutile, dans le sens qu’on ne risque pas de ne pas pouvoir prouverq*mparce que l’on abstient de l’employer.

q ǫ q*m q*m

L’inutilit´e de cette r`egle est particuli`erement flagrante, puisqu’une des pr´emisses et la conclusion sont identiques.

4.4 Emploi de notre package regex

Nos classes Pattern etMatchersont suffisamment proches de celles de la biblioth`eque pour que l’on puisse, dans le sourceGrep.java(figure 5), changer la ligneimport java.util.regex.*

en import regex.*, ce qui nous donne le nouveau source ReGrep.java. D`es lors, `a condition que le source des classes du package regex se trouve dans un sous-r´epertoireregex, nous pou-vons compiler par javac ReGrep.java et nous obtenons un nouveau programme ReGrep qui utilise notre impl´ementation des expressions r´eguli`eres `a la place de celle de la biblioth`eque.

Nous nous livrons ensuite `a des exp´eriences en comparant les temps d’ex´ecution (par les commandestime java Grep . . . ettime java ReGrep . . . ).

(1) Dans le dictionnaire fran¸cais, nous recherchons les mots qui contiennent au moins n fois la mˆeme voyelle non accentu´ee. Par exemple, pourn= 3 nous ex´ecutons la commande :

% java Grep ’(a.*a.*a|e.*e.*e|i.*i.*i|o.*o.*o|u.*u.*u)’ /usr/share/dict/french

1 2 3 4 5 6

Grep 2.7 2.5 1.9 1.7 1.6 1.5

ReGrep 3.1 9.7 16.4 17.9 18.6 18.0

On voit que notre technique, sans ˆetre ridicule, est nettement moins efficace.

(2) Toujours dans le dictionnaire fran¸cais, nous recherchons les mots qui contiennent n fois la lettre e, apr`es effacement des accents. Par exemple, pour n = 3 nous ex´ecutons la commande :

% java Grep ’(e|´e|`e|^e).*(e|´e|`e|^e).*(e|´e|`e|^e)’ /usr/share/dict/french

1 2 3 4 5 6 7

Grep 2.9 2.2 1.9 1.7 1.6 1.5 1.5

ReGrep 3.1 9.0 12.8 15.2 15.8 16.0 15.9

Cette exp´erience donne des r´esultats similaires `a la pr´ec´edente. Plus pr´ecis´ement d’une part la biblioth`eque est plus rapide en valeur absolue ; et d’autre part, nos temps d’ex´ecution sont croissants, tandis que ceux de la biblioth`eque sont d´ecroissants. Mais et c’est impor-tant, il semble bien que les temps se stabilisent dans les deux cas.

(3) Nous recherchons une sous-chaˆıne filtr´ee par X(.+)+X dans la chaˆıne XX=· · ·=, o`u =· · ·= est le caract`ere = r´ep´et´e n fois. Cette reconnaissance doit ´echouer, mais nous savons [3, Chapitre 5] qu’elle risque de mettre en difficult´e l’impl´ementation de la biblioth`eque.

16 18 20 22 24

Grep 0.3 0.5 1.3 4.8 18.6

ReGrep 0.2 0.2 0.2 0.2 0.2

Et effectivement l’emploi de la biblioth`eque conduit `a un temps d’ex´ecution manifestement exponentiel. Il est fascinant de constater que notre impl´ementation ne conduit pas `a cette explosion du temps de calcul.