Vous êtes sur la page 1sur 36

1

OUTILS D’ANALYSE DE
DOCUMENTS STRUCTURÉS
MIAGE – Master 1 (INF4)
Daniel Diaz

Copyright (C) 2016-2018 Daniel Diaz

Objectifs / Programme 2

Comprendre la notion de document structuré.


Apprendre les techniques pour « lire et comprendre » (i.e.
analyser)
un document structuré.
Notions sur les langages (ER, grammaires, automate, …)
Deux niveaux d’analyse: analyse lexicale et syntaxique.
Savoir les mettre en œuvre en Java
package regexp
outil JavaCC.
Support en PDF et autre matériels du cours disponibles sur l’EPI:
https://cours.univ-paris1.fr/fixe/UFR27-M1Miage-DOCS
(si pas encore inscrit utiliser le compte visiteur et la clé)

NOTION DE
DOCUMENTS STRUCTURÉS

Cours d’analyse de documents structurés 1


Fichier binaire 4

Dans un fichier binaire, les données sont codées suivant leur type.
Exemple: fichiers d’images et vidéos (.jpeg, .png, .avi, …),
fichiers associés aux BD, fichiers .pdf, fichiers compressés
(.zip), fichiers exécutables (programmes compilés: objets,
librairies natives, exécutables).
Un fichier binaire ne peut pas être directement compris par un
humain.
Il faut donc une application qui fasse le décodage. Celle-ci doit
connaître la position (relative par rapport au début du fichier) de
chaque information (champ), son type et son encodage.

Fichier binaire : non compréhensible 5

$ less image.jpeg
<FF><D8><FF><E0>^@^PJFIF^@^A^A^A^@`^@`^@^@<FF><DB>^@C^@^H^F^
F^G^F^E^H^G^G^G^L^T^M^L^K^K^L^Y^R^S^O^T^]^Z^_^^^]^Z^\^\ $.'
",#^\^\(7),01444^_'9=82<.342<FF><DB>^@C^A^L^K^L^X^M^M^X2!^\!
22222222222222222222222222222222222222222222222222<FF><C0>^@
^Q^H^Cz^EESC^C^A"^@^B^Q^A^C^Q^A<FF><C4>^@^_^@^@^A^E^A^A^A^A^
A^A^@^@^@^@^@^@^@^@^A^B^C^D^E^F^G^H...

Le fichier n’est pas compréhensible. Il contient des caractères


« non imprimables » (less est intelligent et les affiche soit comme
^A (code ASCII 1), soit comme <FF> (code ASCII 255 en hexa).
On reconnaît que c’est un fichier JPEG à sa signature (en général
dans les premiers octets): FF D8.
Sites avec base des signatures: https://www.filesignatures.net
ou https://en.wikipedia.org/wiki/List_of_file_signatures

Fichier texte 6

Dans un fichier texte les données sont codées comme une simple
suite de caractères.
Exemple : fichiers .txt, fichiers sources de langages (ex: .java),
fichiers .html, .xml, .json,…
Un fichier texte est directement compréhensible par une humain
avec un simple visualiseur. Il suffit de connaître le code utilisé
pour les caractères (en général ASCII ou un sur-ensemble comme
l’UNICODE) ainsi que l’encodage (UTF-8, …).
Il peut donc le modifier simplement grâce à un éditeur de texte.
Pour des raisons de simplicité ces formats sont assez libres :
l’utilisateur peut ajouter des espaces où il veut, peut sauter des
lignes, écrire des commentaires (qui ne sont que de la
documentation),…

Cours d’analyse de documents structurés 2


Fichier texte: compréhensible et souple 7

$ less page.html
<html>
<head>
<title>Ma page perso</title>
</head>
<body>
Rock’n Roll
</body>
</html>

Le fichier est compréhensible.


Il ne contient que des caractères « imprimables ».
Son format est assez libre.
Tout cela facilite la vie de l’utilisateur…

Fichier texte: inconvénients 8

$ less page.html
<!DOCTYPE html>
<html><head
>
<!-- commentaire inutile -->
<meta charset="utf-8"
/><title>Titre de ma page</title
>
</head><body>
Rock &amp; Roll
</body></html>

Des structures optionnelles, les espaces et sauts de ligne sont


autorisés là où on ne s’y attend pas, des commentaires sont
autorisés, des caractères ont un sens particulier (méta-caractères,
comme le < > ou &) et nécessitent un moyen pour être compris
comme du texte normal,…

Comparaison formats binaire et texte 9

Format Format
Critère
binaire texte

Compacité ++ --

Facilité à créer/modifier le format pour l’utilisateur -- ++

Facilité à comprendre le format par l’utilisateur -- ++

Facilité à générer le format par un programme + ++

Facilité à lire et comprendre par un programme + --

Cours d’analyse de documents structurés 3


Comparaison formats binaire et texte 10

Le format binaire reste préférable lorsque la compacité et


l’efficacité de traitement sont primordiaux.
Le format texte tend à se généraliser de plus en plus car :
Peut-être structuré tout en restant simple pour l’utilisateur
Offre beaucoup de liberté à l’utilisateur (espacements et indentation
libre, commentaires,…)
Facile à produire et à comprendre par l’utilisateur
Mais cette facilité implique que :
Un fichier texte structuré peut être difficile à « lire et à
comprendre » par un programme informatique.

Analyse de documents structurés 11

L’opération de « lire et comprendre » s’appelle analyse (parsing).


L’analyse partitionne le document texte en ses composants et
crée une représentation interne (par exemple un arbre).
L’analyse « décortique » le langage source. Elle est commune aux
compilateurs, interpréteurs et autres outils manipulant des
sources structurées tels que : éditeurs structurels, analyseurs
statiques, pretty-printers, extracteurs de données, systèmes de
composition de texte (LaTeX),…
Ecrire un analyseur correct (ie. qui fonctionne avec tous les textes
structurellement corrects) est une tâche difficile, voire très difficile
suivant la structure des documents.
Heureusement il existe des techniques d’analyses et des outils
performants pour écrire ces analyseurs.

12

INTRODUCTION A L’ANALYSE
LEXICALE ET SYNTAXIQUE

Cours d’analyse de documents structurés 4


Nécessité de grouper en mots 13

Lors de l’analyse, le fichier source est vu comme une suite de


caractères. Ainsi avec le texte : x = prix * (z + 32)
l’analyseur verra passer la suite de caractères:
x = p r i x * ( z + 3 2 )
Remarques :
Des caractères doivent être groupés pour former des « mots ». Ainsi
on veut considérer prix comme un seul mot plutôt que comme p
suivi de r suivi de i… De même pour 32.
On ne veut pas se soucier du nombre d’espaces entre * et (.
Les commentaires sont inutiles et doivent être éliminés dès que
possible.
On dédie donc une première phase pour découper le texte source
en mots. C’est le rôle de l’analyseur lexical. Sur l’exemple ci-
dessus on aura: x = prix * ( z + 32 )

Rôle de l’analyseur lexical 14

Le rôle de l’analyseur lexical [lexer/scanner] consiste à découper


le source (suite de caractères) en une suite d’unités lexicales
[token] en filtrant les séparateurs et les commentaires. Il doit aussi
traiter les erreurs lexicales (i.e. une suite de caractères d’entrée
ne formant pas une unité lexicale valide).
On appèle lexème une suite de caractères du source qui
concorde avec la description d’une unité lexicale.

unité lexicale description informelle lexèmes


if i suivi de f if
une lettre suivie de chiffres, de lettres
id i x12 nb_elem
ou du caractère _
entier un chiffre suivi de chiffres 12 2 421

Rôle de l’analyseur syntaxique 15

L’analyseur syntaxique [parser] doit reconnaître l’aspect syntaxique du


source. Il doit vérifier que la suite d’unités lexicales (mots) fournie par
l’analyseur lexical forme des structures (phrases) correctes (sinon il doit
reporter les erreurs le plus précisément possible). L’analyseur produit en
résultat une représentation interne du source analysé, il s’agit en
général d’un arbre (dit Arbre de Syntaxe Abstraite).
Opérationnellement, l’analyseur syntaxique est le chef d’orchestre. Au
besoin il demande à l’analyseur lexical de lui retourner le prochain mot
(un couple <unité lexicale, lexème>) pour le guider dans son analyse.
Lorsqu’il n’en a plus besoin il demande à l’analyseur lexical le mot
suivant.
Ainsi ces 2 analyses se déroulent en parallèle (de manière entrelacée).
Ceci évite de devoir stocker en mémoire la liste de tous les mots
(couples) comme cela serait fait si on avait 2 phases distinctes
(séquentielles).

Cours d’analyse de documents structurés 5


Liaison analyseur lexical / syntaxique 16

x = p r i x * ( z + 3 2 )

analyseur lexical

id egal id mul parg id plus nb pard


x = prix * ( z + 32 )

analyseur syntaxique

L’analyseur syntaxique = et crée une représentation


interroge l’analyseur x * interne de la structure source
lexical pour obtenir l’unité (ex : arbre).
prix +
lexicale suivante et le
lexème associé,
z 32

Nécessité d’une description à 2 niveaux 17

La séparation entre le niveau lexical et le niveau syntaxique va


aussi se traduire au niveau de la description du langage à
analyser: on décrira séparément le niveau lexical et le niveau
syntaxique.
Nous verrons que le niveau lexical est assez simple et correspond
à la plus petite classe des langages formels : les langages
réguliers. Nous verrons qu’il existe une notation concise et
pratique: les expressions régulières. Du fait de sa simplicité, la
classe des langages réguliers peut être analysée très
efficacement grâce à des automates finis.
Pour le niveau syntaxique nous verrons qu’une classe plus riche
est nécessaire : les langages non contextuels. Nous verrons
comment cette classe peut être analysée efficacement.

Les langages formels 18

Les langages formels étudient langages d’un point de vue


théorique. On étudie la structure (la syntaxe) de ces langages.
Les langages formels s’intéressent à toutes les langages (même
beaucoup plus simples que le langage naturel).
Qu’est ce qu’un langage ? Un ensemble (possiblement infini) de
mots (NB: « mot » est à prendre au sens de « chaîne valide »).
Qu’est-ce qu’un mot ? Une chaîne de caractères formée à partir
d’un alphabet.
Qu’est-ce qu’un alphabet ? Un ensemble fini de symboles.
Des exemples de langages sur l’alphabet {a,b,c}:
L1 = {ab, ac, bc, ba, ca, cb}
L2 = {ac, abc, abbc, abbbc, ab…bc}

Cours d’analyse de documents structurés 6


Historique 19

Ces travaux remontent à 1955 sous l’influence de :


Noam Chomsky, linguiste (1928-)
grammaires génératives pour engendrer des langages.
classification : réguliers ⊂ non contextuels ⊂ contextuels ⊂ tous
Marcel P. Schützenberger, médecin/mathématicien (1920-1996) :
modèle algébrique pour formaliser les grammaires de Chomsky.
divers automates pour reconnaître les langages non contextuels.
Parallèlement John Backus invente empiriquement une notation
pour décrire Fortran (1955-1957). Sa notation s’appelle BNF
(Backus Normal Form). Par la suite, Peter Naur la modifiera pour
décrire Algol (1960) et BNF deviendra Backus-Naur Form.

Une seule opération : la concaténation 20

Un mot est formé en « concaténant » des symboles d’un alphabet.


Ainsi le mot abc n’est autre que « a » concaténé à « b »
concaténé à « c ».
Cette opération est matérialisée par l’opérateur . (point). Tout
comme en maths 2.x peut s’écrire 2x, nous pourrons omettre le
point. Ainsi a.b.c = abc
Le mot vide (de longueur 0) est symbolisé par la lettre ε (epsilon).
Propriétés: ε est élément neutre: ε.x = x.ε = x
associativité : (x.y).z = x.(y.z)
Notation puissance: an = a.a.a...a = aa…aa (répété n fois)
Ainsi: a3 = aaa a2 = aa a1 = a a0 = ε
On notera a* le langage infini de toutes les puissance de a c-a-d
{ ε, a, aa, aaa, aaaaa, aaa…aaa }

Une classification des langages 21

La classification des langages est due à


Noam Chomsky (linguiste).
Il s’agit d’inclusions ensemblistes. Ainsi un
langage régulier est aussi context-free
lequel est aussi context-sensitive,…
Chaque classe à un « pouvoir
d’expression » propre et des algorithmes
d’analyse spécifiques. Au centre on trouve
les langages les plus limités mais les plus
efficacement analysables.
Les langages context-free sont déjà plus
« expressifs » mais plus difficiles à
analyser…

Cours d’analyse de documents structurés 7


Deux classes utilisées en pratique 22

Les langages réguliers quoique simples sont suffisamment


expressifs pour décrire le niveau lexical. On peut les analyser très
efficacement grâce à des automates finis.
Les langages context-free ont un pouvoir d’expression plus élevé
et permettent de décrire le niveau syntaxique. Bien que plus
difficiles à analyser il existe des algorithmes d’analyse très
performants.

23

LANGAGES REGULIERS
POUR LE
NIVEAU LEXICAL

Expressions régulières 24

Les expressions régulières (ER) sont des notations concises pour


décrire les langages réguliers. Les ER sont définies de
récursivement comme suit (c est un caractère, r et s des ER) :

ER ce que ça décrit
ε rien (la chaîne vide)
c le caractère c lui-même
rs ce que décrit r suivi de ce que décrit s
r|s ce que décrit r ou ce que décrit s
r* ce que décrit r répété 0, 1 ou plusieurs fois

Le langage engendré par une ER est l’ensemble des chaînes


décrites par cette ER.
Librairie d’ER: http://www.regxlib.com/

Cours d’analyse de documents structurés 8


Expressions régulières étendues 25

Pour des raisons de simplicité on peut étendre les ER avec :


ER ce que ça décrit
. n’importe quel caractère (sauf \n)
"chaîne" la chaîne littéralement (ex : "abc*+")
décrit une classe de caractères, classe peut être c1c2…cn
qui décrit les caractères c1 ou c2 ou … ou cn
[classe]
ou c1 – c2 qui décrit les caractères entre c1 et c2.
On peut grouper plusieurs classes (ex : [A-Za-z0-9_])
[^classe] tous les caractères sauf ceux décrit par classe
r+ ce que décrit r répété 1 ou plusieurs fois
r? ce que décrit r ou rien (élément optionnel)
On utilise \ pour « échapper » les caractères tels que . * + ?

Exercices 26

Quels est le langage engendré par chacune des ER suivantes :


1) (a|b)(a|b)
2) (b|t)o(u|n)(b|t)o(u|n) on pourrait aussi écrire ((b|t)o(u|n))2
3) [0-9]+
4) [+–]? [0–9]+ ( "." [0–9]*)? ([eE] [+–]? [0–9]+)?
5) [a–zA–Z_][a–zA–Z0–9_]*
6) a (b | c)*
7) (a* | b*)*
8) b*ab*ab*ab*
9) (b(a|b))*

Notion d’automate 27

A tout langage régulier (et donc toute grammaire régulière ou


toute expression régulière), on peut associer un automate qui ne
reconnait que les mots du langage. On représente aisément un
automate comme un graphe orienté (graphe de transition), les
nœuds sont les états et les arcs (étiquetés avec les symboles)
représentent la fonction de transition.
Exemple : 2 automates reconnaissant : a(a | b)*bb
a a
a
a b b a b b
0 1 2 3 0 1 2 3
a
b b
Remarque : le second est plus complexe (2 transitions de plus).

Cours d’analyse de documents structurés 9


Automate fini non-déterministe / déterministe 28

L’automate fini est déterministe (AFD) [deterministic finite


automaton] si, pour chaque état, tous les arcs sortants sont
étiquetés avec un symbole différent.
Sinon c’est un automate fini non-déterministe (AFN)
[nondeterministic finite automaton]. C’est-à-dire qu’il existe au
moins un état ayant 2 arcs sortants étiquetés avec le même
caractère.
Les AFN et AFD ont le même pouvoir d’expression : les langages
réguliers. Tout AFN peut donc être transformé en un AFD.
Un AFD est plus efficace en temps qu’un AFN mais est en général
plus complexe et donc nécessite plus de place en mémoire.
Ex: l’AFD pour (a|b)*a(a|b)(a|b)…(a|b) nécessite 2n états s’il y a n-1 (a|b).
En pratique on n’utilise que des AFD.

Exercices 29

Trouver les automates (et si possible les expressions régulières)


pour les :
1) mots sur {a,b} de la forme anbm avec n et m ≥ 1.
2) mots avec les voyelles (sauf y), dans l'ordre (ex : madrediou) .
3) mots sur {a,b,c} commençant par b.
4) mots sur {a,b} contenant a.
5) mots sur {a,b} de longueur paire.
6) mots sur {a,b} tels que tout a est suivi d’un b.
7) mots sur {a,b} n’ayant pas 2 mêmes lettres consécutives.
8) mots sur {a,b} ayant un nombre pair de a et de b.
9) mots sur {1,2,3} tq ∑ des chiffres soit multiple de 4 (ex : 31121).

Définitions lexicales avec des ER 30

Pour pouvoir référencer les unités lexicales (depuis l’analyseur


syntaxique) on leur donne un nom. On utilise la notation :
Nom : ER
avec Nom écrit en gras. Cela permet aussi de simplifier l’écriture
des ER en nommant des sous-parties communes.
signe_opt : [+–]?
entier : [0–9]+
decim_opt : ("." entier )?
exp_opt : ( [eE] signe_opt entier )?
nombre : signe_opt entier decim_opt exp_opt
chaine : "([^"]|\\")*"

Cours d’analyse de documents structurés 10


Reconnaissance des unités lexicales 31

A partir de la spécification des unités lexicales l’analyseur lexical


doit les reconnaître dans le texte source. A la demande, il doit
fournir les informations de prochain mot :
l’unité lexicale associée (entier)
le lexème associé (chaîne)
sa position dans le fichier source (numéro de ligne, numéro de
colonne).
Règle : on reconnaît toujours la plus longue unité lexicale
possible, permet par exemple de distinguer case (mot-clé) de
caserne (id).
On peut aussi inclure d’autres traitements lors de l’analyse
lexicale (ex : gestion d’une table des symboles). L’analyseur peut
alors fournir des informations additionnelles (indice dans la table
des symboles,…).

Automates pour plusieurs unités lexicales 32

En plus des symboles, on étiquette les arcs avec l’unité reconnue


jusque là (la notion d’état final est moins significative).

0–9
Descriptions des unités :
entier
decr : – –
inf :< 0–9
infe : <= 1
entier
entier : –? [0–9]+
0–9 entier
Remarque : ils est important – –
de lire le plus long lexème 0 2 4
decr
possible (cas de < et <=).

< =
3
inf infe

Support Java pour les ER 33

Depuis le JDK 1.4 Java fournit un support pour les ER dans le


package regex. Ce n’est pas vraiment fait pour écrire des
analyseurs lexicaux mais c’est utile dans bien des cas (ex: vérifier
qu’une chaîne forme une URL valide ou une adresse email
valide). Il faut importer l’API par :
import java.util.regex.*;
Java offre deux classes:
Pattern: permet de compiler une ER en un AFD.
Matcher: permet d'analyser une chaîne donnée via un Pattern
(soumet la chaîne à l’AFD reconnaissant l’ER).
Idée: on sépare la création d’un AFD (fait 1 fois) de son utilisation
pour analyser une chaîne (fait n fois).

Cours d’analyse de documents structurés 11


Syntaxe Java des ER 34

En plus de ce qu’on a vu voici quelques éléments utiles (norme


POSIX).
Classe prédéfinies:
\d un chiffre [0-9] \D un non-chiffre [^0-9]
\s un espace [ \t\n\f\r] \S un non-espace [^\s]
\w un alphanum [a-zA-Z0-9_] \W un non-alphanum [^\w]
Opérateurs de nombre de répétition:
r{n} l’ER r doit se répéter exactement n fois
r{n,} l’ER r doit se répéter au moins n fois
r{n,m} l’ER r doit se répéter au moins n fois et au plus m fois
Marqueurs de frontière: (où commence et où finit la chaîne):
^ le début de la ligne $ la fin de la ligne
\b le début d’un mot \B le début d’un non-mot

Création d’un Pattern et d’un Matcher 35

Un Pattern est une représentation compilée d’une ER (ex: AFD).


Cette classe n’a pas de constructeur. On obtient une nouvelle
instance en appelant une méthode de classe (statique) qui prend
une ER (sous forme de chaîne) et retourne donc un Pattern .
static Pattern compile(String regex)
A partir d’un Pattern on peut appeler la méthode suivante pour
obtenir un Matcher pour une chaîne donnée.
Matcher matcher(String chaine)
Un Matcher est un moteur qui effectue des opérations d’analyse
sur une chaîne en interprétant un Pattern. Un Matcher permet de
d’exécuter l’AFD sur la chaîne d’entrée (ou des sous-portions de
cette chaîne).

Opérations de reconnaissance 36

Un Matcher permet d’effectuer 3 types d’opérations de


reconnaissances :
boolean matches(): essaye de reconnaître l’ER sur la chaîne en
entier.
boolean lookingAt(): essaye de reconnaître l’ER sur la chaîne à
partir du début mais pas forcément jusqu'à la fin.
boolean find(): essaye de reconnaître l’ER sur une portion de la
chaîne. Les appels suivants à find() continueront l’analyse à partir
du premier caractère non pris en compte jusqu’alors.
Att: find() peut s’arrêter aussitôt que l’ER est vérifiée (sans
chercher la plus longue correspondance possible).
Ces méthodes retournent true en cas de succès de
reconnaissance.

Cours d’analyse de documents structurés 12


Obtenir des d’informations sur l’analyse 37

En cas de succès on peut obtenir plus d’informations avec les


méthodes:
int start(): retourne l’indice de début de la correspondance.
int end(): retourne l’indice de fin de la correspondance + 1.
String group(): retourne la sous-chaîne mise en correspondance.

Exemples 38

Exemple: on veut tester si "aaaaab" vérifie l’ER "a*b".


Pattern p = Pattern.compile("a*b");
Matcher m = p.matcher("aaaaab");
if (m.matches())
System.out.println("Chaine valide");

Chaine valide

Exemple: les sous-chaînes de "xxaabbcxxab" vérifiant "a*b|c".


Pattern p = Pattern.compile("a*b|c");
Matcher m = p.matcher("xxaabbcxxab"); aab
while (m.find()) b
System.out.println(m.group()); c
ab

Les groupes de capture 39

Un groupe sert à capturer la sous-chaîne correspondant à une


sous-ER.
Un groupe est définit grâce aux parenthèses. Les groupes sont
numérotés à partir de 1 en comptant les parenthèses ouvrantes
de gauche à droite. Le groupe 0 correspond toujours à l’ER
entière.
Exemple: avec l’ER a((b*c)(d?)) on a les groupes suivants :

No de groupe ER associée
0 a((b*c)(d?))
1 ((b*c)(d?))
2 (b*c)
3 (d?)

Cours d’analyse de documents structurés 13


Les groupes de capture 40

Les groupes de capture peuvent être référencés à l’intérieur d’une


ER grâce à la notation \n (ou n est le numéro du groupe). NB: à
l’intérieur d’une chaîne en Java il faut doubler le \.
Ainsi l’ER "([a-z][a-z])\\1" permet de reconnaître des mots tels
que baba , bobo, titi, toto, tutu, zozo, …
On peut obtenir des informations sur la sous-chaîne associée à un
groupe. Les méthodes start(), end(), group() sont surchargées
avec un numéro de groupe. La méthode groupCount() retourne le
nombre de groupes.
int start(int n)
int end(int n)
String group(int n)
int groupCount()

Exemple d’utilisation des groupes 41

On analyse la chaîne "abbcd" avec l’ER a((b*c)(d?))et on


affiche la valeur des groupes :
Pattern p = Pattern.compile("a((b*c)(d?))");
Matcher m = p.matcher("abbcd");
if (m.matches()) {
System.out.println("nb groupes: " + m.groupCount());
for (int i = 0; i <= m.groupCount(); i++)
System.out.println("gr " + i + " = " + m.group(i));
}

groupe ER associée chaîne


nb groupes: 3 0 a((b*c)(d?)) abbcd
gr 0 = abbcd
gr 1 = bbcd 1 ((b*c)(d?)) bbcd
gr 2 = bbc 2 (b*c) bbc
gr 3 = d
3 (d?) d

Remplacements avec des ER 42

On peut effectuer des remplacements grâce aux méthodes :


public String replaceFirst(String remplacement)
public String replaceAll(String remplacement)
Elles remplacent la première (ou toutes les) occurrence(s) de l’ER
par la chaîne remplacement. On peut faire référence aux groupes
de l’ER en utilisant la notation $n (ou n est le numéro du groupe).
Un $n sera remplacé par la chaîne rendue par group(n).
Exemple: convertir date format anglais mm-jj-aaaa en jj/mm/aa:
Pattern p = Pattern.compile(
"([0-9]{1,2})-([0-9]{1,2})-[0-9]{2}([0-9]{2})");
Matcher m = p.matcher("4-28-2012");
String res = m.replaceAll("$2/$1/$3"); date: 28/4/12
System.out.println("date: " + res);

Cours d’analyse de documents structurés 14


Des raccourcis dans la classe String 43

boolean matches(String regex):


chaine.matches(regex) est équivalent à
Pattern.compile(regex).matcher(chaine).matches()
String replaceFirst(String regex, String repl):
chaine.replaceFirst(regex, repl) est équivalent à
Pattern.compile(regex).matcher(str).replaceFirst(repl)
String[] split(String regex):
chaine.split(regex) est équivalent à
Pattern.compile(regex).split(str)
qui « découpe » une chaîne en un tableau de sous-chaînes
Ex: String[] tab = "marc:paul:jean/bob".split("[:/]");
System.out.println(Arrays.toString(tab));

[marc, paul, jean, bob]

Exercices 44

Ecrire une méthode grep(String regex, String nomFichier) qui


lit le fichier nomFichier et affiche toutes les lignes contenant une
correspondance avec l’ER regex.
L’utiliser pour trouver les mots du dictionnaire français qui
contiennent au moins 5 fois la lettre i (exemple initialisation
ou indivisibilité).
Exactement 5 fois la lettre i (ne doit pas afficher indivisibilité).
Les mots contenant les sous-chaine gras, gris ou gros. Ex:
groseille.
Les mots commençant et terminant par la même chaîne de 3
caractères. Exemple: anticoagulant. ou ionisation.
Les mots commençant par m, b ou p et finissant par la même lettre.
Ex: modem ou baobab.
Fichiers disponible sur le site du cours.

Exercice 45

Ecrire une fonction substit(String regex, String repl, String


nomFichier) qui lit le fichier nomFichier et, pour chaque ligne,
remplace regex par la chaîne repl. Si un remplacement a eu lieu,
la ligne résultant est affichée.
Utiliser cette fonction sur le fichier feries.txt (regarder son
format) pour afficher les jours fériés de 2012 sous la forme:
du 01/01/2012 au 02/01/2012 Nouvel an
du 06/04/2012 au 07/04/2012 Vendredi saint
du 08/04/2012 au 09/04/2012 Pâques
du 09/04/2012 au 10/04/2012 Lundi de Pâques
du 01/05/2012 au 02/05/2012 Fête du travail
du 08/05/2012 au 09/05/2012 Fête de la Victoire

Rq: voici un testeur d’ER.
http://www.annuaire-info.com/outil-referencement/expression-reguliere/

Cours d’analyse de documents structurés 15


46

LANGAGES CONTEXT-FREE
POUR LE
NIVEAU SYNTAXIQUE

Des grammaires pour décrire les langages 47

Une grammaire décrit la structure syntaxique d’un langage.


Exemple : l’instruction conditionnelle de Java :
if ( expression ) instruction else instruction
C’est une suite composée du mot-clé if, de (, d’une expression,
de ), d’une instruction, du mot-clé else et d’une instruction.
En utilisant la variable E pour dénoter une expression et X pour
dénoter une instruction, on peut exprimer cette règle par la
production :
X → if ( E ) X else X
Les éléments comme if, (, ), ou else sont appelés terminaux.
Les variables comme X ou E sont appelées non-terminaux et
représentent des suites de terminaux. Nous utiliserons de
majuscules pour les non-terminaux

Grammaires non contextuelles 48

Une grammaire non contextuelle [context-free] est définie par :


un ensemble de symboles terminaux (minuscule ou gras).
un ensemble de symboles non-terminaux (majuscule ou italique).
un non-terminal servant de symbole de départ appelé axiome.
un ensemble de productions P→ α où P est un non-terminal et α est
une suite composée de terminaux et de non-terminaux.
Cette notation est appelée BNF (Backus-Naur Form).
Rq: il existe une norme pour la BNF (pour pouvoir être entrée sur
un clavier en ASCII). La flèche → s’écrit ::= Les non-terminaux
sont écrits entre < et > et les terminaux entre " et ". Exemple :
<chiffre> ::= "1"

Cours d’analyse de documents structurés 16


Dérivation et Langage 49

On note α ⇒ β le fait d’appliquer une production au premier non-


terminal de α pour obtenir β. Ceci revient à remplacer le non-
terminal le plus à gauche par la partie droite de la règle choisie.
On notera α ⇒* β le fait d’appliquer 0, 1 ou plusieurs productions.
On a donc α ⇒ α’ ⇒ α’’ ⇒ … ⇒ β. On dit que β est une dérivation
de α.
Exemple : avec E → E + E | E – E | 0 | 1 |… | 9 on peut générer :
E⇒E+E⇒1+E⇒1+E–E⇒1+4–E⇒1+4–9
Une dérivation possible de E est donc la chaîne 1 + 4 – 9.
Le langage engendré par une grammaire est l’ensemble des
chaînes que l’on peut dériver depuis l’axiome.
Exemple: « les expressions avec + et – sur des chiffres ».

Retour sur les langages réguliers 50

Un langage régulier peut être décrit par une ER ou un automate.


Voici une autre caractérisation : un langage est régulier s’il peut
être décrit par une grammaire régulière.
Une grammaire est régulière si toutes ses productions sont de la
forme : P→Qµ ou P→µ ou toutes de la forme P→µQ ou P→µ (où
P et Q sont 2 non-terminaux et µ est une suite de terminaux
uniquement).
Rq: toutes les productions ont un plus un non-terminal en partie droite
et il apparaît toujours au début ou toujours à la fin de la partie droite.
Exemple : une grammaire régulière pour décrire les entiers :
N → N0 N→0
N → N1 N→1
: :
N → N9 N→9

Ne pas confondre grammaire et langage 51

Un langage peut-être décrit par plusieurs grammaires différentes.


Une grammaire context-free mais pas régulière engendre un
langage context-free. Ceux-ci incluent les langages réguliers, le
langage engendré peut-être régulier (ou ne pas l’être).
Par contre une grammaire régulière engendre un langage régulier
(par définition).
Une grammaire non régulière ne veut pas dire que le langage
n’est pas régulier. Peut-être existe-t-il une grammaire régulière
pour ce langage !

Cours d’analyse de documents structurés 17


Ne pas confondre grammaire et langage 52

Grammaires Langages Grammaires Langages

G
G

Une grammaire context-free Une grammaire régulière


(mais pas régulière) engendre implique que le langage est
un langage context-free. Ce régulier.
langage peut être régulier ou
pas.
Une grammaire non régulière ne
signifie pas que le langage n’est pas
régulier.
Peut-être existe-t-il une grammaire
régulière pour ce langage !

Différence entre grammaire et langage 53

Soit le langage (L) des mots sur {a} de longueur paire et 2


grammaires :
(G1) S→ aSa S→ε n’est pas régulière: on ne peut rien dire sur L.
(G2) S→ aaS S→ε est régulière: on en déduit que L est régulier.

Grammaires Langages

G1

G2 L

Exercices 54

Quels sont les langages engendrés par :


1) S → ab S → aSb
2) S→a S → Sb
3) S→ε S → SS S → (S) S → [S] S → {S}
4) E → E+E E→ E–E E → E*E E → E/E E → (E) E → a E → b
5) S → SS S → 0 S → 1 S → 2 … S → 9
6) S→ε S → aSa S → bSb
7) S → DS S → D D→0 D→1 D→2 … D→9
8) S→ε S → aSb S → bSa S → SS
Remarque : on ne peut décrire les mots anbncn (n≥0) avec une grammaire
non contextuelle. Il faut une grammaire contextuelle :
S→ε S → aSBc cB → Bc aB → ab bB → bb

Cours d’analyse de documents structurés 18


Exercices (2) 55

Trouver les grammaires qui engendrent les :


1) nombres binaires « sans zéro inutile »
2) mots de la forme a2nbcn (n ≥ 0)
3) mots sur {a,+,=} de la forme an + am=am+n (ex : a+aa=aaa)
4) mots de la forme anbmcmdn (n ≥ 0, m > 0)
5) mots sur {a,b} contenant un nombre pair de a et de b
6) mots de la forme apbqcr avec p=q ou q=r (p, q, r ≥ 0)
7) mots sur {a,b} tels que le nombre de b (>0) est multiple de 3
8) mots sur {0,1,…,9} contenant autant de chiffres pairs qu’impairs

Ambiguïté 56

Une grammaire est ambiguë s’il existe une chaîne ayant plus
d’une dérivation. La grammaire suivante :
E→E+E|E–E|0|1|2|3|4|5|6|7|8|9
est ambiguë car la chaîne 9 – 5 + 8 a 2 dérivations différentes :
1. E⇒E+E⇒E–E+E⇒9–E+E⇒9–5+E⇒9–5+8
2. E⇒E-E⇒9–E⇒9–E+E⇒9–5+E⇒9–5+8
La chaîne a plusieurs interprétations ce qui n’est pas acceptable
dans un compilateur. On n’utilisera donc pas de grammaires
ambiguës ou alors avec des règles additionnelles pour résoudre
les ambiguïtés.
Exemple de grammaire équivalente non ambiguë (déjà vue) :
E→E+C E→C
E→E–C C→0|1|2|3|4|5|6|7|8|9

Encore des restrictions sur les langages 57

On peut construire un analyseur syntaxique pour tout langage


non contextuel. Il existe donc des analyseurs universels pour ces
langages. Malheureusement leur complexité est de O(n3) pour
analyser une chaîne de longueur n. C’est trop coûteux !
On préfèrera utiliser des langages un peu plus restreints (mais
suffisant pour décrire nos langages d’entrée) et pour lesquels il
existe des analyseurs efficaces (linéaires, i.e. O(n)).
Ces analyseurs doivent être déterministes (ne doivent pas
essayer une possibilité et « rebrousser chemin » en cas
d’échec). Ils se basent sur la chaîne source pour guider
l’analyse.
Ces analyseurs peuvent être classés en 2 catégories analyseurs
descendants et analyseurs ascendants. Nous n’étudierons que
les premiers.

Cours d’analyse de documents structurés 19


58

GRAMMAIRES
POUR
ANALYSE DESCENDANTE

Analyse descendante 59

Analyse descendante [top-down] : on part de l’axiome et on


applique des productions jusqu’à aboutir au texte source. La
construction débute donc à la racine (axiome) et descend vers les
feuilles (terminaux).
Cela ressemble beaucoup à ce que l’on fait « à la main » lorsque,
partant de l’axiome, on cherche une dérivation conduisant à la
chaîne source à analyser.
Ce sont des méthodes simples, intuitives, permettant de
construire des analyseurs efficaces à la main. Il existe des
générateurs d’analyseurs syntaxiques basés sur ces méthodes
descendants :
JavaCC (Java), ANTLR (Java/C++).

Analyse descendante ⇒ prédictive 60

Supposons que l’on veuille analyser la chaîne bouton avec :


S → XX X → bY Y → oZ Z→n
X → tY Z→u
Il faut donc trouver une dérivation gauche. On part de l’axiome S
et on le remplace par une production (il n’y en a qu’une) :
S ⇒ XX
Ici on peut appliquer X → bY ou X → tY. Pour choisir, on va
s’aider du prochain symbole de la chaîne, c’est b, on utilise donc
X → bY :
S ⇒ XX ⇒ bYX …et ainsi de suite…
Principe : pour dériver un non-terminal, on utilise un symbole de
pré-vision [lookahead] pour choisir LA production à appliquer.
L’analyse descendante est donc une analyse prédictive.

Cours d’analyse de documents structurés 20


Table d’analyse : exemple 61

En reprenant la la grammaire :
S → XX X → bY Y → oZ Z→n
X → tY Z→u
on obtient la table d’analyse suivante :

table M b o u t n
S S → XX S → XX
X X → bY X → tY
Y Y → oZ
Z Z→u Z→n

M[X,ā] contient la production à appliquer pour dériver le non-


terminal X à la vue du symbole de pré-vision ā.
Les entrées vides correspondent à des cas d’erreurs.

Algorithme d’analyse descendante 62

On suppose que la chaîne est terminée par $ (marqueur de fin=EOF).


empiler S$ avec S au sommet ($ indique aussi la fin de la pile)
positionner le pointeur p sur le premier symbole de la chaîne
répéter
dépiler Ŧ
soit ā le symbole désigné par p
si Ŧ est un non-terminal X alors
si M[X,ā] ≠ X→ Ŧ1… Ŧn alors échec finsi // entrée vide
empiler Ŧ1… Ŧn avec Ŧ1 au sommet // émettre X→ Ŧ1… Ŧn
sinon (Ŧ est un terminal ou $)
si Ŧ ≠ ā alors échec finsi
avancer p
finsi
jusqu’à Ŧ=$
succès

Application de l’algorithme d’analyse 63

Analyse de la chaîne bouton avec la grammaire :


S → XX X → bY Y → oZ Z→n
X → tY Z→u

pile reste action pile reste action


S$ ⓑouton$ utiliser S → XX X$ ⓣon$ utiliser X → tY
XX$ ⓑouton$ utiliser X → bY tY$ ⓣon$ absorber t
bYX$ ⓑouton$ absorber b Y$ ⓞn$ utiliser Y → oZ
YX$ ⓞuton$ utiliser Y → oZ oZ$ ⓞn$ absorber o
oZX$ ⓞuton$ absorber o Z$ ⓝ$ utiliser Z → n
ZX$ ⓤton$ utiliser Z → u n$ ⓝ$ absorber n
uX$ ⓤton$ absorber u $ $ absorber $

Cours d’analyse de documents structurés 21


Analyse prédictive ⇒ grammaire adéquate 64

Attention : il faut que la grammaire permette la prédiction. Avec :


S → XX X → bY Y → on
X → tY Y → ou
lorsqu’il faut dériver Y, en voyant o on ne peut pas choisir entre
Y → on et Y → ou. Problème de prédiction multiple :
pile reste action
S$ⓑouton$ utiliser S → XX
XX$ ⓑouton$ utiliser X → bY
bYX$ ⓑouton$ absorber b
YX$ ⓞuton$ que choisir : Y → on ou Y → ou ?

Ici il faudrait 2 symboles de pré-vision pour choisir la production à


appliquer (o n ≠ o u).

Grammaires LL(k) 65

Une grammaire est LL(k) si, pour toute chaîne source que l’on lit
de la gauche vers la droite, on peut construire de manière
déterministe une dérivation gauche à la vue de k symboles de
pré-vision. Autrement dit : en regardant les k prochains
symboles, on peut toujours décider qu’elle production utiliser
pour dériver le non-terminal le plus à gauche. En pratique on
utilise k=1.
Remarque : la première grammaire était LL(1), la seconde LL(2).

Left to right scanning LL(k) Nécessite k symboles de


lecture de la chaîne pré-vision.
de gauche à droite Leftmost derivation
dérivation gauche

Grammaires LL(1) ⇒ restrictions 66

Une grammaire LL(1) vérifie les conditions nécessaires


suivantes :
elle n’est pas ambiguë. Sinon il pourrait y avoir 2 dérivations gauches
différentes pour une même chaîne. Donc à un moment on ne pourrait
prédire quelle production appliquer.
elle n’est pas récursive à gauche : en essayant de développer un non-
terminal X on ne doit pas retomber sur X sans avoir absorbé de
symboles d’entrée (comme ce serait le cas par exemple avec X →
Xa). Sinon on ne peut savoir comment dériver X : en utilisant la
production récursive ou une autre qui termine la récursion. De plus
certains analyseurs peuvent boucler (notamment ceux écrit à la main).
elle est factorisée à gauche : un même non-terminal ne peut avoir 2
règles avec un préfixe commun. Sinon la prédiction est impossible.
C’était le cas avec Y → on et Y → ou. La mise en facteur du o
redonne la 1ère grammaire Y → oZ Z → n Z → u.

Cours d’analyse de documents structurés 22


Ambiguïté : que faire ? 67

Il n’y a pas de règle pour supprimer l’ambiguïté d’une grammaire.


D’ailleurs il existe des langages intrinsèquement ambigus (i.e. il
n’existe pas de grammaire non-ambiguës les engendrant).
Une grammaire ambiguë traduit en général une erreur de
conception, il faut la revoir. Elle est souvent trop « permissive »,
pas assez dirigiste.
Exemple de grammaire ambiguë :
E→E+E
E→E*E
E→(E)
E → entier
Rappel: on va enlever l’ambiguïté en marquant les priorités.

Notations d’opérateurs 68

Un opérateur est un symbole représentant une fonction appliquée à


1 argument X (opérateur unaire) ou à 2 arguments X et Y
(opérateur binaire). On définit des opérateurs pour les fonctions
usuelles car ils facilitent la lisibilité : 2 + 3 * 6 est plus simple que
plus(2,mul(3,6)). Il existe 3 types de notations : préfixe, postfixe et
infixe.

préfixe postfixe infixe


forme exemple forme exemple forme exemple
unaire op X – 12 X op cpt ++ pas applicable
binaire op X Y + 2 3 X Y op 2 3 + X op Y 2 + 3
2 + (3 * 6) + 2 * 3 6 2 3 6 * + 2 + 3 * 6
(2 + 3) * 6 * + 2 3 6 2 3 + 6 * (2 + 3)* 6

Priorité des opérateurs 69

Avec la notation préfixe (polonaise) et postfixe (polonaise


inversée) il y a une seule interprétation possible à une
expression.
En notation infixe il faut associer des priorités aux opérateurs
pour lever les ambiguïtés. Ainsi, * étant plus prioritaire que +,
l’expression 2 + 3 * 6 s’interprète comme 2 + (3 * 6).
Pour forcer (2 + 3) * 6 on utilisera des parenthèses explicites.
Une grammaire peut ou non traduire les priorités.
Celle-ci non (ambigüe): Mais celle-ci oui :
E→E+E E→E+T
E→E*E E→T
E→C T→T *C
T→C

Cours d’analyse de documents structurés 23


Associativité des opérateurs 70

La priorité ne suffit pas à lever toutes les ambiguïtés de la


notation infixe. C’est le cas en présence plusieurs opérateurs de
même priorité. Exemple: 9 – 5 – 8 se calcule-t-il (9 – 5) – 8 ou
bien 9 – (5 – 8) ?
On définit l’ordre de calcul grâce à l’associativité des opérateur.
Un opérateur op est associatif à gauche si :
X op Y op Z signifie (X op Y) op Z.
Un opérateur op est associatif à droite si :
X op Y op Z signifie X op (Y op Z).
Une grammaire peut ou non traduire les associativités.
Celle-ci non : Mais celle-ci oui :
E→E+E E→E+C
E→C E→C

Grammaire pour les opérateurs 71

En résumé: il faut recenser tous les opérateurs, leur priorité (1


est moins prioritaire que 2,…) et leur associativité (à gauche, à
droite, pas associatif).
Pour chaque priorité P:
Créer un non-terminal EP qui fait apparaître chaque opérateur o
de priorité P. Exemple avec un opérateur o binaire:
• si o associatif à gauche: EP → EP o EP+1
• si o associatif à droite: EP → EP+1 o EP
• si o non-associatif: EP → EP+1 o EP+1
Ajouter une règle sans opérateur EP → EP+1

Grammaire pour les opérateurs 72

Ex: pour les expressions à base de + (priorité 1), * (priorité 2) et


d’entiers. Les 2 opérateurs sont associatifs à gauche. On obtient:
Grammaire: Renommage des non-terminaux:
E1 → E1 + E2 E→E+T E se lit « expression »
E1 → E2 E→T
E2 → E2 * E3 T→T*F T se lit « terme »
E2 → E3 T→F
E3 → ( E1 ) F→(E) F se lit « facteur »
E3 → entier F → entier

NB: au niveau le plus bas (E3) on trouve les éléments atomiques


(ici les entiers) et les expressions parenthésées (qui sont par
définition les plus prioritaires).

Cours d’analyse de documents structurés 24


Élimination de la récursivité à gauche 73

Une grammaire est récursive à gauche si elle contient un non-


terminal A tel qu’il existe une dérivation A ⇒… ⇒ Aα.
On ne traitera ici que le cas de la récursivité à gauche immédiate
caractérisée par l’existence d’une production de la forme : A →
Aα. Il doit alors exister au moins une production A → β (où β ne
commence pas par A) pour arrêter la récursivité. On a donc :
A → Aα | β
Toute dérivation de A donnera : A ⇒… ⇒ Aα…α ⇒… ⇒ βα …α
(soit un β suivi d’un nombre quelconque de α).
On obtient la même chose avec :
A → βA’ le A’ se lit « reste de A »
A’ → αA’ | ε remarque : A’ est récursif à droite désormais

Élimination de la récursivité à gauche (2) 74

E→E+T E → T E’
E→T E’ → + T E’
E’ → ε
T→T*F devient T → F T’
T→F T’ → * F T’
T’ → ε
F→(E) F→(E)
F → entier F → entier

NB: on peut généraliser le processus d’élimination de récursivité

Factorisation à gauche 75

Une grammaire non factorisée à gauche ne permet pas de


choisir a bonne production à la vue d’un seul symbole. Ex:
S → if ( E ) S else S
S → if ( E ) S
à la vue de if on ne peut décider comment dériver S. On
factorise :
S → if ( E ) S S’
S’ → else S
S’ → ε
De manière générale, si A a n alternatives débutant par α, i.e. :
A → αβ1 A → αA’
… on factorise en A’ → β1
A → αβn …
A’ → βn

Cours d’analyse de documents structurés 25


Nécessaire n’est pas suffisant 76

Les conditions vues précédemment sont nécessaires mais pas


suffisantes pour être LL(1). Exemple : S → aSa S → ε, n’est
pas ambiguë, n’est pas récursive à gauche, est factorisée à
gauche, mais n’est pas LL(1). Lorsqu’on dérive S, à la vue d’un
a, on ne sait pas quelle production appliquer. Essayons avec aa :

pile reste action


S$ ⓐa$ admettons qu’on utilise S → aSa
aSa$ ⓐa$ absorber a
Sa$ ⓐ$ cette fois il faudrait utiliser S → ε !!!

Les générateurs calculent cette table et indiquent les erreurs


LL(1) en reportant toutes les entrées contenant plus d’une
production.

77

UTILISATION
D’UN
GENERATEUR D’ANALYSEURS

Générateurs d’analyseurs 78

Il existe des générateurs d’analyseurs lexicaux et syntaxiques. Ils


peuvent être séparés comme le couple Lex et Yacc (1975) ou
intégrés dans un même outil comme dans JavaCC ou ANTLR.
La partie lexicale est décrite avec des expressions régulières. La
partie syntaxique avec des règles BNF ou mieux EBNF. Le
résultat est un fichier source fournissant une fonction d’analyse
d’un flux d’entrée. On peut inclure des actions et ainsi faire
retourner à l’analyseur le résultat désiré.
Nous utiliserons JavaCC qui génère des analyseurs syntaxiques
par descente récursive pour des grammaires LL(k). Il prend en
charge l’aspect lexical et syntaxique et produit du Java.

Cours d’analyse de documents structurés 26


Fonctionnement de JavaCC 79

JavaCC prend en entrée un fichier suffixé .jj (ex : Comp.jj)


décrivant les unités lexicales + la grammaire syntaxique et génère
un ensemble de fichiers sources Java. On peut aussi inclure des
actions sous la forme d’instructions Java exécutées lors de
l’analyse.

description Comp.jj
du langage

javacc Comp.java
CompConstants.java
CompTokenManager.java
fichiers Java ParseException.java
getNextToken()
générés SimpleCharStream.java
Token.java
TokenMgrError.java

Installation et utilisation de JavaCC 80

Récupérer la distrib de JavaCC 5.0 (fichier javacc-5.0.zip


dispo depuis le site du cours) et la décompresser sur votre disque.
Le décompresser sous la racine C:\ (ou autre).
Ceci crée une arborescence dans C:\javacc-5.0 :
• C:\javacc-5.0\bin contient les exécutables
• C:\javacc-5.0\doc contient la documentation
• C:\javacc-5.0\examples contient des exemples

Pour compiler utiliser la commande javacc Comp.jj (après


avoir ajouté C:\javacc-5.0\bin dans le PATH).
Sous Mac: décompresser dans votre home (ex /Users/foo) et
ajouter /Users/foo/javacc-5.0/bin au PATH en modifiant le
fichier /Users/foo/.bash_profile pour ajouter la ligne :
export PATH=$PATH:/Users/foo/javacc-5.0/bin

Intégration de JavaCC et de Netbeans 81

Module pour NetBeans offrant la coloration syntaxique + appel du


javacc. Ce plugin est dispo sur le site du cours (fichier
1275348047830_org-javaccnb.nbm). Le site original est:
http://plugins.netbeans.org/PluginPortal/faces/PluginDetailPage.jsp?pluginid=20277

Install Netbeans: menu « Outils » puis « Plugins » onglet


« Téléchargés ». Cliquer sur « ajouter des plugins » et
sélectionner le fichier .nbm téléchargé.
Aller dans « Outils » puis « Options » onglet « Divers » puis
« Javacc » et saisir le chemin jusqu’à javacc:
sous PC: C:\javacc-5.0\bin\javacc.bat
sous Mac : /Users/foo/javacc-5.0/bin/javacc
Pour compiler un fichier .jj, dans la partie gauche « explorateur
Projets » faire clic droit sur le .jj puis « Javacc Compile… ».

Cours d’analyse de documents structurés 27


Syntaxe des fichiers JavaCC 82

Une description de la syntaxe JavaCC se trouve dans :


C\javacc-5.0\doc (ou /Users/foo/javacc-5.0/doc)
En voici une version simplifiée.
options
PARSER_BEGIN(Comp)

programme JAVA : doit au moins définir la classe Comp (peut


contenir le main()).
PARSER_END(Comp)

définition des règles syntaxiques

définition des unités lexicales (unités à ignorer / à garder)

Définitions lexicales avec JavaCC 83

On définit une unité lexicale par un nom et une expression régulière.


TOKEN : { < nom : expression_regulière > }
On peut grouper plusieurs définitions dans un TOKEN en les séparant
par |. Si on écrit SKIP à la place de TOKEN, l’unité est ignorée.

ER ce que ça décrit
"chaîne" la chaîne elle-même
les caractères entre c1 et c2 . On peut en écrire plusieurs
["c1"-"c2"]
séparés par , et ~ au début complémente.
er1 | er2 ce que décrit er1 ou ce que décrit er2
( er )? ce que décrit er ou rien (élément optionnel)
( er )* ce que décrit er répété 0, 1 ou plusieurs fois
( er )+ ce que décrit er répété 1 ou plusieurs fois

Notation BNF étendue 84

L’EBNF est une notation pratique et concise grâce aux opérateurs :


α | β indique une alternative (correspond à 2 règles)
{ α } indique que α se répète de 0 à n fois
( α )+ indique que α se répète de 1 à n fois
[ α ] indique que α se répète 0 ou 1 fois (élément optionnel)
Application à notre grammaire des expressions :
E → T E’
E’ → + T E’ E→T{+T}
E’ → ε
T → F T’
BNF: EBNF: T → F { * F }
T’ → * F T’
T’ → ε
F→(E)
F → ( E ) | entier
F → entier

Cours d’analyse de documents structurés 28


Définitions syntaxiques avec JavaCC 85

Voici comment définir X → partie_droite :


void X():
{}
{ dans le corps on traduit la partie_droite comme suit:
un terminal constant TERM se référence par "TERM"
un terminal nommé TERM se référence par <TERM>
le terminal associé à la fin de fichier se référence par <EOF>
un non-terminal Y est traduit par Y()
le | (OU) se traduit par le même opérateur |
le {…} (0,1 ou plusieurs fois) se traduit par (…)*
le (…)+ (1 ou plusieurs fois) se traduit par (…)+
le […] (0 ou 1 fois) se traduit par […] ou par(…)?
}

Analyseur d’expressions : partie syntaxique 86

On ajoute une règle en début pour vérifier la présence du marqueur


de fin (ici fin de ligne) après une expression.
void verifie() : // règle additionnelle: verifie → expression \n
{}
{
expression() "\n"
}

void expression() : // expression → terme { + terme | – terme }


{}
{
terme()
( "+" terme()
| "-" terme()
)*
}

Analyseur d’expressions : partie syntaxique 87

void terme() : // terme → facteur { * facteur | / facteur }


{}
{
facteur()
( "*" facteur()
| "/" facteur()
)*
}

void facteur() : // facteur → ( expression ) | entier


{}
{
"(" expression() ")"
| <ENTIER>
}

Cours d’analyse de documents structurés 29


Analyseur d’expressions : partie lexicale 88

Comme JavaCC permet d’écrire directement un terminal constant


sous forme d’une chaîne (ex: "+") dans une définition EBNF, il ne
reste plus qu’à décrire les séparateurs et les unités lexicales non
constantes :

SKIP :
{ " " | "\t" | "\r" }

TOKEN :
{
< ENTIER: ( ["0"-"9"] )+ >
}

Rappel: les programmes sont disponibles sur le site du cours

API de JavaCC pour l’analyse lexicale 89

La documentation de l’API (Application Programming Interface) se


trouve à : https://javacc.java.net/doc/apiroutines.html. Résumé :
Comp(InputStream in) : crée un nouvel analyseur qui lit ses
unités lexicales depuis le flux in.
Token getNextToken() : récupère l’unité lexicale suivante (utile
que si vous ne voulez qu’un analyseur lexical).
Token : objet associé à une unité lexicale. Contient les champs :
int kind : l’unité lexicale (cf. fichier CompConstants.java).
int beginLine, beginColumn, endLine, endColumn :
informations de position dans le source.
String image : la chaîne contenant le lexème.
type nonTerminal() : demande l’analyse syntaxique associée
au non-terminal (normalement l’axiome).

Analyseur d’expressions : le main 90

PARSER_BEGIN(ExprVerif)

public class ExprVerif {


public static void main(String [] args)
throws ParseException {
ExprVerif syn = new ExprVerif(System.in);
System.out.print("entrer une expression: ");
syn.verifie();
System.out.println("syntaxe correcte");
}
}

PARSER_END(ExprVerif)

Cours d’analyse de documents structurés 30


Analyseur d’expressions : exécution 91

entrer une expression: 2 + 3 * 6


syntaxe correcte

entrer une expression: 2 + * 43


Exception in thread "main" ParseException: Encountered "*"
at line 1, column 5. Was expecting one of:
<ENTIER> ...
"(" ...

entrer une expression: 7 + 3 12


Exception in thread "main" ParseException: Encountered "12"
at line 1, column 7. Was expecting one of:
"\n" ...
"+" ...
"-" ...
"*" ...
"/" ...

Ajout d’actions lors de l’analyse syntaxique 92

Notre analyseur ne fait que vérifier si la syntaxe est correcte. On


va l’enrichir pour qu’il évalue le résultat de l’expression analysée.
Pour chaque non-terminal JavaCC produit une méthode qui
effectue son analyse. JavaCC offre la possibilité de personnaliser
le comportement de l’analyseur en ajoutant :
des arguments aux méthodes associées aux non-terminaux.
des valeurs de retour à ces méthodes.
des actions : instructions à exécuter à des moments précis de
l’analyse (i.e. dans le corps des méthodes).
On va personnaliser chaque méthode d’un non-terminal X pour
qu’elle retourne l’entier correspondant à la valeur de la (sous-
)expression analysée par X.

Ajout d’actions lors de l’analyse syntaxique 93

Complément de syntaxe JavaCC pour la déclaration des règles :


type X(arguments) :
{déclarations }
{
traduction de la partie droite {instructions JAVA }
}
type est le type de valeur que retourne la méthode.
arguments sont des arguments que l’on peut passer à la méthode.
déclarations permet de déclarer un ensemble de variables locales
(qui pourront être utilisées dans les actions).
{instructions JAVA } sont des instructions qui seront exécutées
lorsque l’analyseur les rencontre.
On peut récupérer la valeur de retour d’un non-terminal et
l’affecter à une variable.
On peut récupérer la valeur d’un terminal et l’affecter à une
variable de type Token.

Cours d’analyse de documents structurés 31


Evaluateur d’expressions : actions 94

// règle additionnelle: evalue → expression \n


// pour s’assurer que termine bien par fin ligne
int evalue() :
{ int x; }
{
x = expression() "\n" { return x;}
}

// expression → terme { + terme | – terme }


int expression() :
{ int x, y; }
{
x = terme()
( "+" y = terme() { x = x + y; }
| "-" y = terme() { x = x - y; }
)*
{ return x;}
}

Evaluateur d’expressions : actions 95

// terme → facteur { * facteur | / facteur }


int terme() :
{ int x, y; }
{
x = facteur()
( "*" y = facteur() { x = x * y; }
| "/" y = facteur() { x = x / y; }
)*
{ return x;}
}

// facteur → ( expression ) | entier


int facteur() :
{ int x; Token t; }
{
( "(" x = expression() ")"
| t = <ENTIER> { x=Integer.parseInt(t.image); }
)
{ return x;}
}

Evaluateur d’expressions : le main 96

PARSER_BEGIN(ExprEval)

public class ExprEval {

public static void main(String [] args)


throws ParseException {
ExprEval syn = new ExprEval(System.in);
System.out.print("entrer une expression: ");
int res = syn.evalue();
System.out.println("resultat = " + res);
}

PARSER_END(ExprEval)

Cours d’analyse de documents structurés 32


Evaluateur d’expressions : exécution 97

entrer une expression: 9 - 5 + 8


resultat = 12

entrer une expression: 9 - (5 + 8)


resultat = -4

entrer une expression: 2 + 3 * 6


resultat = 20

entrer une expression: (2 + 3) * 6


resultat = 30

entrer une expression: - 2 * 3


Exception in thread "main" ParseException: Encountered "-"
at line 1, column 1. Was expecting one of:
<ENTIER> ...
"(" ...

Création d’un Arbre de Syntaxe Abstraite 98

On va ici produire en sortie de l’analyseur un ASA pour une


expression. Cet arbre pourra ensuite être manipulé pour évaluer
l’expression (comme on le faisait précédemment) ou pour tout
autre traitement. On obtient donc un analyseur réutilisable
puisqu’il fournit une représentation interne de l’expression qui
peut être utilisé pour n’importe quel autre traitement sur les
expressions.
L’ASA à des nœuds étiquetés par l’opération et pour fils les
opérandes. Les feuilles de l’ASA sont des entiers. Ainsi
l’expression 2 + (3 * 6) donnera lieu à l’ASA:
+
2 *
3 6

ASA en Java : modélisation 99

On définit une classe abstraite ASAExpr qui modélise un ASA (une


classe abstraite permet de fournir des implémentations par défaut
mais on pourrait aussi utiliser une interface). Cette classe est
héritée (et spécialisée) par les classes modélisant les opérateurs
(ASAExprPlus, ASAExprMoins, ASAExprMoins, ASAExprMoins) et par la
classe ASAExprEntier modélisant un entier.

ASAExpr
<<abstract>>

ASAExprPlus ASAExprMoins … ASAExprEntier

Cours d’analyse de documents structurés 33


ASA en Java : classe ASAExpr 100

La classe Java ASAExpr. Contient des méthodes pour accéder aux


fils (0, 1 ou 2 fils).

package asaexpr;
public abstract class ASAExpr {

public ASAExpr getExpr1() { // retourne le 1er fils


return null;
}

public ASAExpr getExpr2() { // retourne le 2ème fils


return null;
}

ASA en Java : classe ASAExpr 101

public abstract String getLabelExplorateur();


public String notationExplorateur() {
return notationExplorateur("");
}
// calcule la notation explorateur
private String notationExplorateur(String prefixe) {
String res = prefixe + getLabelExplorateur() + "\n";
String prefixeSuiv = prefixe + " ";
ASAExpr e1 = getExpr1();
ASAExpr e2 = getExpr2();
if (e1 != null)
res += e1.notationExplorateur(prefixeSuiv);
if (e2 != null)
res += e2.notationExplorateur(prefixeSuiv);
return res;
}
} // fin de la classe ASAExpr

ASA en Java: classe ASAExprPlus 102

package asaexpr;
public class ASAExprPlus extends ASAExpr {
private ASAExpr e1, e2;
public ASAExprPlus(ASAExpr e1, ASAExpr e2) { // constructeur
this.e1 = e1;
this.e2 = e2;
}
public ASAExpr getExpr1() {
return e1;
}
public ASAExpr getExpr2() {
return e2;
}
public String getLabelExplorateur() {
return "PLUS"; "MOINS" pour ASAExprMoins,…
}
} // fin de la classe ASAExprPlus

Cours d’analyse de documents structurés 34


ASA en JAVA : classe ASAExprEntier 103

package asaexpr;
public class ASAExprEntier extends ASAExpr {

private int valeur;

public ASAExprEntier(int valeur) { // constructeur


this.valeur = valeur;
}

public String getLabelExplorateur() {


return "ENTIER = " + valeur;
}
} // fin de la classe ASAExprEntier

Création de l’ASA : actions 104

// règle additionnelle: parse → expression \n


// pour s’assurer que termine bien par fin ligne
ASAExpr parse() :
{ ASAExpr x; }
{
x = expression() "\n" { return x;}
}

// expression → terme { + terme | – terme }


ASAExpr expression() :
{ ASAExpr x, y; }
{
x = terme()
( "+" y = terme() { x = new ASAExprPlus(x, y); }
| "-" y = terme() { x = new ASAExprMoins(x, y); }
)*
{ return x;}
}

Création de l’ASA : actions 105

// terme → facteur { * facteur | / facteur }


ASAExpr terme() :
{ ASAExpr x, y; }
{
x = facteur()
( "*" y = facteur() { x = new ASAExprMult(x, y); }
| "/" y = facteur() { x = new ASAExprDivis(x, y); }
)*
{ return x;}
}

// facteur → ( expression ) | entier


ASAExpr facteur() :
{ ASAExpr x; Token t; }
{
( "(" x = expression() ")"
| t = <ENTIER> { x = new ASAExprEntier(
Integer.parseInt(t.image)); }
)
{ return x;}
}

Cours d’analyse de documents structurés 35


Création de l’ASA : le main 106

options { OUTPUT_DIRECTORY="../jjgener"; }
PARSER_BEGIN(ExprArbre)
package jjgener;
import asaexpr.*;
public class ExprArbre {
public static void main(String [] args)
throws ParseException {
ExprArbre syn = new ExprArbre(System.in);
System.out.print("entrer une expression: ");
ASAExpr a = syn.parse();
System.out.println(a.notationExplorateur());
}
}
PARSER_END(ExprArbre)

Création de l’ASA : exécution 107

entrer une expression: 2+3*6


PLUS
ENTIER = 2
MULT
ENTIER = 3
ENTIER = 6

entrer une expression: (4+1)*(9-3)+6


PLUS
MULT
PLUS
ENTIER = 4
ENTIER = 1
MOINS
ENTIER = 9
ENTIER = 3
ENTIER = 6

Exercices 108

Ajouter une méthode evalue qui retourne l’entier associé à


l’évaluation.
Ajouter opérateur % (modulo)
Ajouter l’opérateur – unaire (plus prioritaire que * / %)
Ajouter des IDENT (on pourra écrire 2 + taille). NB: pour
l’instant la méthode evalue retournera 0 pour un IDENT.

Cours d’analyse de documents structurés 36