Les articles de ESSI# - Reconnaissance de schéma (Pattern matching)

RACINE

Reconnaissance de motifs (Pattern matching)


Introduction

Reconnaitre une chaine dans une autre est souvent une tâche PENIBLE et peu reluisante. On perd du temps à chercher des index dans notre flux, copier, coller, se tromper de +ou- 1 position, etc ... et encore, je ne parle pas de pattern matching "évolué" nécessitant des expressions régulières !!
En tant que programmeur on a nettement mieux à faire que passer notre temps à cela (et surtout la tâche est souvent génératrice de bugs vicelards car subtiles, et nous pas assez). Par conséquence, les différents langages nous ont proposés différents moyens de faire plus simple, et la recherche dans la théorie des langages nous a doté d'outils ultra puissants (je pense à lex et yacc, ainsi qu'à leurs descendants).

Je vais vous présenter ici comment faire facilement du pattern matching "à la main", puis comment utiliser les expressions régulières dans .NET.


Pour utiliser .NET framework, vous devez avoir installé sur votre poste AU MOINS :
  1. Windows XP Pro ou 2000 avec SP2 ou NT4 avec SP6a
  2. Internet Explorer 5.01
  3. IIS (mais pas dans tout les cas)
  4. .NET Framework SDK (evidemment !!) + ses patches (recommandé)
  5. un éditeur de votre choix (emacs ou VS.NET suivant les courants de pensée et les moyens)

Je prends pour pré-requis que mon lecteur a un niveau même faible en C# (voir de Java au vues de ressemblances syntaxiques), mais sait ce qu'est le pattern matching dans des Strings (pas ceux que l'on voit à la plage ;)) et si possible des notions de lex.



Voici la liste des différents styles visuels utilisés dans ce cours (et les suivants) :
Si vous voyez ceci ... ...ça veut dire cela.
IDisposable Interface C'est un lien vers une page d'aide du .NET Framework SDK. Ce lien ne fonctionnera que si le .NET framework SDK ANGLAIS est installé sur votre poste courant.
Si vous avez la version française, modifiez les liens comme suit :
ms-help://MS.NETFrameworkSDK/... en
ms-help://MS.NETFrameworkSDK.Fr/...

Ok, c'est pas forcément une bonne idée de Microsoft, mais c'est ainsi ... :(
--> téléchargez le source
C'est un lien vers un fichier téléchargeable hosté sur le même site.

Reconnaissance simple

A la main (dans le cambouis)

Je ne vais pas détailler ici comment faire car je ne conseille PAS DU TOUT cette technique la plus primitive possible. Toutefois, vous y trouverez quelques informations que j'espère utiles.

C'est la manière la plus complexe et probablement la moins puissante. Je ne vous conseil pas de l'utiliser à moins d'avoir de TRES simples opérations à effectuer.

Typiquement, vous cherchez "où commence cette partie de mot ?", "où finis ce terme ?", "combien de x dans ma chaine ?", "c'est quoi l'extension de ce fichier ?", etc ...
Tout le travail à base de comptage de caractères, de placement d'index et de copie de caractères (ou de blocs) se fait principalement grâce aux méthodes de la classe String.

ATTENTION : la classe String est de type immuable. C'est à dire qu'une fois sa valeur fixée, vous ne pouvez la changer ! Ainsi, si vous voulez modifier une chaine vous en obtiendrez une autre.

Pour bidouiller, vous utiliserez principalement :
Pourquoi Quoi Exemple
Quel est le caractère en position x ? Attributs Chars ou opérateur [] string maChaine = "hello";
char c = maChaine[0];
char k = maChaine[3];

c vaut 'h' et k vaut 'l' (le deuxième)
Enlever les espaces inutiles en début et/ou fin Methode Trim/ TrimEnd/ TrimStart string maChaine = "   hello ";
string autreChaine = maChaine.Trim();

autreChaine
vaut "hello"
Remplir une chaine avec un caractère Méthode PadLeft/ PadRight string maChaine = "7";
string james = maChaine.PadLeft (3, "0");

james
vaut "007"
Commence/finit par xxx ? Methodes StartsWith/ EndsWith string s = "hello";
bool ok = s.StartsWith("cel");

ok
vaut false
Séparer une chaine en morceaux Methode Split string path= "c:\C#work\courstp\article05.html";
string[] parts = path.Split (new Char() {'\', '.'});

parts
vaut {"c:", "C#work", "courstp", "article05", "html"}
Position de tel caractère Méthodes IndexOf/ IndexOfAny/ LastIndexOf/ LastIndexOfAny string s = "hello";
int p = s.IndexOf ('e');

p
vaut 1

Attention aux manipulations de chaines de caractères, on se trompe souvent. Sachez que la classe String ne contient pas la panacée à tout vos besoins, notamment sur les noms de fichiers.
Sachez que si vous désirez connaître l'extension d'un fichier, il est recommandé d'utiliser Path.GetExtension("toto.exe"). Ou encore Path.GetFileNameWithoutExtension ("c:\xyz\toto.exe") (qui vaut "toto") pour récupérer des noms de fichiers. Mon conseil, pensez à la classe Path.

Traitements basiques

Pour reconnaitre des motifs simples, vous allez utiliser les méthodes IndexOf ou LastIndexOf (par exemple). Vous pourrez ainsi savoir si une sous chaine est présente dans le flux de caractère. Par contre, pour savoir si un chiffre est présent, vous devrez tenter de savoir si le "0" est présent, ou le "1", ou le "2", etc ... il existe plus élégant non ?

Toutefois des traitements basiques sont parfois utiles et largement suffisant pour certaines tâches. Ainsi, vous proposez à l'utilisateur un formatage simple de chaine de texte. Imaginons un application qui dump (copie) des pistes audio de votre CD vers votre disque dur (en wav ou mp3). Cette application proposera certainement un moyen de définir le nom des fichiers.

En effet, il est plus sympa d'avoir des "01 - Docteur Renaud, Mister Renard.mp3", "02 - Petit pédé.mp3" que des "TRACK01.MP3", "TRACK02.MP3", etc... Comment faire ?
Le logiciel propose certainement dans ses entrailles quelque chose genre "Nom des fichiers à produire" qui propose un certain formalisme. Par exemple : "%n - numero de la piste, %t - nom de la piste".

En C#, on effectuera alors simplement ceci :
string leFormat = "%n - %d.mp3";
...
foreach(Track t in tracks){
   /* Il est sans risque d'utiliser leFormat, car les Strings sont inchangeables */
   string leFichier = leFormat.Replace ("%n", t.Number);
   leFichier = leFichier.Replace ("%d", t.Title);
   ...
}
Libre à vous d'utiliser les capacités du langage (ou plutôt de la plateforme) mise à votre disposition pour donner plus de liberté à vos utilisateurs, ou pour vous simplifier votre programmation.
Par contre, vous risquez d'être bien vite piègé si vous avez des besoins plus complexe. C'est là où interviennent les expressions régulières ...

Reconnaissance avancée

Expressions régulières

Pas de cours sur les expressions régulières ici ! Une vague explication et des pointeurs, rien de plus !

Comment reconnaître dans un flux (fichier, port reseau, String, etc ...) un certain motif ? Que vous attendez un certain formalisme ? Que vous ne vous interressez qu'à une certaine partie du flux ? Et surtout, sans devenir fou ! Imaginez vous parser de l'XML "à la main" ? Et encore, pas du bel XML connu. Du malformé donc vous ne connaissez pas à l'avance toute la structure. Impensable et pas très malin. Vous foncez dans le mur.

Les expressions régulières permettent de décrire un flux dans sa forme (et dans une certaine mesure sa structure). Elles permettent à un automate de reconnaître des expressions, en se basant sur des transitions et des états. (théorie des langages)
Par exemple, voici un mini fichier html :

<html><body><H1>Salut !!!</H1></body></html>

Voici un expression régulière qui permet de décrire si un flux est un fichier HTML (simplissime) :

"<html><body><H1>[^>]*</H1></body></html>"

La partie centrale ([^>]*) signifie n'importe quoi sauf > ([^>]), et ceci 0 ou plusieurs fois (*). Interressant non ? Vous venez ici de décrire à quoi ressemble un fichier HTML.

Je tiens à insister sur le fait que les expressions régulières devraient se contenter de l'analyse lexicale (reconnaitre des mots d'un langage) et peu déborder sur l'analyse syntaxique (l'agencement de ces mots). Cette dernière partie étant hors de portée, et hors du champ de cet article. En d'autres termes, ce n'est pas avec uniquement des expressions régulières que vous pourrez analyser des structures complexes...

Je vous conseil de regarder vos cours de LFA (pour les ESSIens) par M. Bruno MARTIN pour la théorie ou le cours de M. Paul FRANCHI pour la pratique.

Pratique [System.Text.RegularExpressions]

Je vais tenter de présenter simplement comment utiliser des expressions régulières avec .NET. J'ai eu besoin de les utiliser pour mon logiciel Web Pics Downloader qui récupère des pages web et télécharge les images présentes sur cette dernière. La tâche consiste à reconnaître le chevron correspondant à une image et à lire l'attribut contenant son chemin d'accès.
C'est le prétexte que je vais utiliser ici.

Un chevron HTML décrivant une image est de ce type :

<img src="chemin/vers/image.jpg" ... />

Les "..." désignent le fait que d'autres attributs peuvent être présents mais ils ne nous interressent pas. De plus, comme le HTML peut être produit par des humains, "/" final peut être manquant.

Voici quelques connaissances sur les expressions régulières dans .NET que nous allons utiliser :

Le chevron image

Voici l'instance de Regex qui nous permet de reconnaître nos chevrons :
/// <summary>
/// pour reconnaitre un chevron "<img ... >"
/// </summary>
private static Regex rImg = new Regex (@"<(i|I)(m|M)(g|G)[^>]*>");
Ecrire (i|I)(m|M)(g|G) nous permet de reconnaître "img" et "IMG" mais aussi "Img" ou "iMg".

Supposons maintenant que nous analysions un objet nommé line de type string : en effet, on lit notre fichier HTML ligne par ligne et on stocke dans une chaine de caractères la ligne courante.

Deux questions :
  1. Existe-t-il dans cette ligne une sous-chaine qui "match" ce "pattern" ?
  2. Si une telle sous-chaine existe, comment la récupérer ?
Deux réponses :
  1. rImg.IsMatch (line) retourne vrai si il existe au moins une sous-chaine dans line qui correspond au pattern
  2. Match m = rImg.Match (line) pour récupérer LA PREMIERE sous-chaine correspondante,
    MatchCollection[] m = rImg.Matches (line) pour récupérer TOUTES LES sous-chaines correspondantes
Notez que vous récuperez des Match ou MatchCollection et pas de simples string. Pour récupérer la valeur (chaîne) correspondante à la capture, utilisez l'attribut Value de la classe Match.

Le chemin vers l'image

Le chemin vers l'image est dans l'attribut "src".
/// <summary>
/// pour reconnaitre la membre 'src="..."' d'un chevron image
/// </summary>
private static Regex rSrcAttr = new Regex ("(s|S)(r|R)(c|C)=\"(?<imgUrl>[^\"]+)\"");
" La syntaxe en rouge (?<imgUrl>[^\"]+)"et un groupe nommé et se comprend comme suit : Ce groupe est en fait "ce qui est entre les guillemets" de l'attribut "src". Par contre, il ne faut pas chercher cet attribut n'importe où ! On va le chercher dans les résultats positifs de la recherche précédente, c'est à dire dans les chevrons <img ...>.

Voici le code C# de comment analyser une ligne de texte susceptible de contenir notre chaine recherchée :
//pour chaque ligne qui contient des chevrons image
if(rImg.IsMatch (line))
{
	//pour chaque occurence de <img ...>
	foreach (Match m in rImg.Matches(line))
	{
		//m contient un attibut 'src=""' ?
		if (rSrcAttr.IsMatch (m.Value))
		{
		        //on récupère le chemin dans la variable imgUrl
			String imgURL = rSrcAttr.Match (m.Value).Result ("${imgUrl}");
			...
		}
	}
}

Voilà. Vous savez maintenant utiliser des expressions régulières en C#. Elles possèdent d'autres atouts non présentés ici que je vous laisse le soin de découvrir :).

Autre pointeur interressant dans l'aide .NET : Regular Expression Classes.

Le mot de la fin

Conclusion

J'espère avoir apporté une réponse à tout ceux qui se demandaient comment manipuler des chaines de caractères, ainsi qu'à ceux qui voulaient utiliser des expressions régulières.
Si vous avez des remarques concernant cet article ((im)précisions, remarques, ...) faites moi signe ;)!!

Remerciements

Monsieur Laurent ELLERBACH, responsable relation études supérieures chez Microsoft France, pour ses idées et son soutien logistique.
Merci à M. Paul FRANCHI, professeur à l'ESSI Nice-Sophia Antipolis pour ses cours d'analyse lexicale qui sont bien pratiques dès qu'on doit écrire des expressions régulières. (cf "Bibliographie" & "Sur le Web")

Sources

Sur le Web : Bibliographie :

Rien, juste les très complets cours de M. Paul FRANCHI de 2ème année de l'ESSI. Disponibles sur le web sur :
http://www.essi.fr/~pfz/ section "Compilation".





par Alain Vizzini (vizzini@essi.fr )
pour l'ESSI & Microsoft, création 27-04-2003, dernière màj 27-04-2003


+ 17.000 pages vues depuis le 14 oct. 2002