La première étape du projet consiste à extraire le texte d’une arborescence de fichiers. Pour cela, le programme parcoure les dossiers un à un depuis la racine pour, arrivé au niveau d’un fichier, en lire le contenu, le filtrer et écrire le résultat dans un fichier de sortie. Le programme prend en entrée un nom de répertoire qui se trouve dans le même emplacement que lui et produit en sortie des fichiers .txt et des fichiers .xml.
Le répertoire de travail est un corpus constitué de l’ensemble des fils RSS de l’année 2013 du site du journal Le Monde recueillis tous les jours à 19h. Les fichiers qu’il contient sont au format .txt et .xml. Pour pouvoir extraire le contenu qui nous intéresse, nous ne nous intéresserons qu’aux fichiers .xml car c’est sur la base des balises qu’ils contiennent que nous mettrons en place les filtres.
Ce travail peut être réalisé selon plusieurs méthodes. On peut, en effet, extraire et filtrer le contenu en utilisant des expressions régulières dans un programme en « Perl pur » qui est la méthode classique. On peut aussi utiliser des outils adaptés, du fait que nous travaillons uniquement sur des fichiers .xml, qu’offrent les bibliothèques PERL dont XML::RSS que nous avons utilisé ici. Dans les deux cas, nous nous intéressons aux balises "titre" et "description".
La première chose à faire, après avoir fait appel aux pragmas "strict" et "warnings" et déclaré les packages qui nous intéressent ainsi que nos variables (sur lesquelles nous reviendrons), c'est de penser à organiser les fichiers de sorties. En effet, lors de la rédaction d'un script nous sommes amenés à le lancer souvent, à chaque modification ou presque, alors, pour faciliter la lecture des résultats il est important de créer des répertoires et des sous-répertoires de sorties avec des noms explicites. Ils nous seront également d'une grande utilité pour différencier entre les résultats des différentes BAO au fur et à mesure que nous avancerons dans le projet. L'idée est donc de créer un répertoire global et de mettre les sorties de chaque étape de travail et/ou chaque méthode dans un sous répertoire dédié. D'un script à l'autre il suffit de copier-coller ce bout de code en changeant à chaque fois les noms des sous-répertoires. Toujours dans un souci pratique, nous écrasons le contenu ancien pour ne garder que le nouveau. En cas de besoin de garder momentanément les sorties successives d'un même script , il suffit de changer le nom du sous répertoire ou de désactiver les lignes qui opèrent l'écrasement en les mettant en commentaire.
#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-> Répertoire et Sous-répertoire pour contenir les fichiers de sortie <-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# my $repertoireglobal = "./resultatsBAO/"; #si le dossier n'existe pas, on le crée if (! -e $repertoireglobal) { mkdir($repertoireglobal) or die ("ERREUR ! Impossible de créer le répertoire !\n"); } my $sousrepertoireglobal ="./resultatsBAO/BAO1_PERL/"; if (! -e $sousrepertoireglobal) { mkdir($sousrepertoireglobal) or die ("ERREUR ! Impossible de créer le répertoire !\n"); } #si le dossier existe déjà, on écrase le contenu existant pour éviter les doublons else { print ("Ecrasement..."); opendir(DIR, $sousrepertoireglobal) or die ("ERREUR ! Impossible d'ouvrir le répertoire !\n"); my @listedecrasement = readdir(DIR); foreach my $fichierecrase(@listedecrasement) { if (-e ($fichierecrase)) { unlink $sousrepertoireglobal.$fichierecrase; } } closedir(DIR); print ("Fin de l'écrasement\n"); }
Dans les déclarations de variable, outre la variable qui recevra le nom de notre fichier passé en argument lors du lancement du script, il nous faut créer des tables de hachage qui nous serviront à éliminer les doublons plus loin dans le code, ainsi que notre dictionnaire de noms de rubriques. En effet, le contenu de nos fichiers est organisé en rubriques, celles-ci sont indiquées en chiffre dans le nom des fichiers. Pour pouvoir récupérer l'intitulé de chaque rubrique et l'afficher dans les résultats de sortie, nous devons associer à chaque "code" son nom. Il suffit pour cela de regarder dans les dossiers, d'ouvrir les fichiers et de noter à quoi correspond chaque code puis de créer un dictionnaire. Cette étape n'est pas obligatoire, elle nous permettra simplement de mieux nous y retrouver dans nos fichiers plus tard et donc de les manipuler plus facilement.
# on passe en argument le nom du dossier à traiter my $racine=$ARGV[0]; #-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-> Tables de hachage pour enlever les doublons <-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# my %dicoTitre=(); my %dicoDescription=(); #-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-> Dictionnaire des noms de rubriques <-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# my %nmrub; $nmrub{Une}="0,2-3208,1-0,0.xml"; $nmrub{International}="0,2-3210,1-0,0.xml"; $nmrub{Europe}="0,2-3214,1-0,0.xml"; $nmrub{Societe}="0,2-3224,1-0,0.xml"; $nmrub{Opinions}="0,2-3232,1-0,0.xml"; $nmrub{Economie}="0,2-3234,1-0,0.xml"; $nmrub{Medias}="0,2-3236,1-0,0.xml"; $nmrub{Rendez_vous}="0,2-3238,1-0,0.xml"; $nmrub{Sports}="0,2-3238,1-0,0.xml"; $nmrub{Environnement_Sciences}="0,2-3244,1-0,0.xml"; $nmrub{Culture}="0,2-3246,1-0,0.xml"; $nmrub{Livres}="0,2-3260,1-0,0.xml"; $nmrub{AlAune}="0,2-3404,1-0,0.xml"; $nmrub{Cinema}="0,2-3476,1-0,0.xml"; $nmrub{Voyages}="0,2-3546,1-0,0.xml"; $nmrub{Technologies}="0,2-651865,1-0.xml"; $nmrub{Politique}="0,57-0,64-82330.xml"; # "~ s" pour rechercher le "/" et le remplacer par un vide afin que le nom du répertoire entré en argument ait toujours la même forme (par exemple "2008" et non "2008/") $racine=~ s/[\/]$//;
Pour le parcours de l'arborescence, nous procédons de la même façons pour le script en Perl pur et XML::RSS. Il s'agit d'indiquer au programme de naviguer dans l'arborescence et de ne traiter que les fichiers ".xml". S'il trouve un dossier alors il boucle, et recommence à parcourir le contenu à la recherche d'un fichier au bon format. Ce qui change c'est le moment où nous lançons notre fonction. Nous avions d'abord inclus notre fonction à l'intérieur de la boucle de la création de fichiers, ce qui fait que le programme parcourt tous les fichiers, pour retrouver chaque rubrique. Nous avons ensuite amélioré le script pour ne parcourir les fichiers qu'une seule fois. Nous avons gardé les deux afin de comparer le temps de traitement et celui-ci s'améliore très nettement avec la deuxième option. On le ressent notamment lors de la phase d'étiquetage dans la BAO2.
#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-> Version lente <-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# #-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-> Création des fichiers de sortie <-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# foreach my $nomrubrique(keys(%nmrub)) { my $outTXT ="TXT_sortie_".$nomrubrique.".txt"; my $outXML ="XML_sortie_".$nomrubrique.".xml"; open (OUTTXT,">:encoding($encodagesortie)",$sousrepertoireglobal.$outTXT); open (OUTXML,">:encoding($encodagesortie)",$sousrepertoireglobal.$outXML); print OUTXML "<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n"; print OUTXML "<rubrique nom=\"".$nomrubrique."\">\n"; #on parcourt l'arborescence du fichier à traiter &parcoursfichiers($racine, $nmrub{$nomrubrique}); print OUTXML "</rubrique>\n"; close (OUTXML); } #-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-> Version rapide <-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# #-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-> Création des fichiers de sortie <-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# foreach my $nomrubrique(keys(%nmrub)) { my $outTXT="TXT_sortie_".$nomrubrique.".txt"; my $outXML ="XML_sortie_".$nomrubrique.".xml"; open (OUTTXT,">:encoding(UTF-8)", $sousrepertoireglobal.$outTXT); open (OUTXML,">:encoding(UTF-8)", $sousrepertoireglobal.$outXML); print OUTXML "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"; print OUTXML "<rubrique nom=\"".$nomrubrique."\">\n"; } #on parcourt l'arborescence du fichier à traiter &parcoursfichiers($racine); foreach my $nomrubrique(keys(%nmrub)) { my $outXML ="XML_sortie_".$nomrubrique.".xml"; if (!open (OUTXML,">>:encoding(UTF-8)", $sousrepertoireglobal.$outXML)) { die "Pb a l'ouverture du fichier \n" }; print OUTXML "</rubrique>\n"; close (OUTXML); }
C'est ici que la différence entre expressions régulières en Perl classique et XML::RSS est marquée. Visuellement déjà, on se rend compte que la version Perl classique est beaucoup plus longue que celle faisant appel à XML::RSS. On peut dire que dans la première on écrit ligne à ligne ce que le programme doit faire tandis que dans la deuxième c'est une fonction qui s'en charge. En réalité, chaque méthode à ses avantages et ses inconvénients. La version Perl classique permet de savoir ce qu'on écrit à chaque ligne, ça prend plus de temps, il faut être pointilleux, mais visuellement on comprend peut-être mieux ce qui se passe. Il faut aussi bien observer le contenu des fichiers que l'on souhaite traiter afin que l'expression régulière corresponde exactement à ce que l'on souhaite extraire. Il faut également préparer l'extraction avec un traitement préalable : enlever les retours à la ligne, les retours chariots, les espaces entre les balises… La version XML::RSS est plus pratique, il suffit de faire appel à la bonne fonction et de la laisser travailler mais le déroulement du processus peut sembler plus obscur au début sans compter qu'il faut d'abord apprendre à appeler correctement la fonction. Mais, l'appel au module XML::RSS, ne nécessite aucun prétraitement, le programme parcourt le fichier et extrait les balises qui nous intéressent pour en faire un tableau de hashage où chaque balise est associée à sa valeur.
Pour la suite des opérations, nous aurons besoin d'avoir des fichiers codés en utf-8, mais nos fichiers de départ ne sont pas tous en utf-8. Pour cette raison, on a précisé l'encodage pour les fichiers de sorties et, nous avons forcé le transcodage en utilisant la bibliothèse "Unicode::String qw(utf8)".
#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-> Version RegExp <-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# #-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-> Procédure pour extraire le contenu des fichiers .xml <-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# sub extraiscontenu { # le premier argument de la procédure ce sont nos fichiers .xml my $file=shift(@_); # le deuxième argument de la procédure est le nom de chacun de ces fichiers .xml my $idrubrique = shift(@_); print $file,"\n"; open(IN,$file); # on lit la première ligne du fichier my $ligne=<IN>; my $encodage=""; #si la première ligne correspond à cette expression régulière on récupère son contenu if($ligne=~/encoding ?= ?[\'\"]([^\'\"]+)[\'\"]/i) { $encodage=$1; #extraire l'encodage } #s'il n'y a pas de déclaration d'encodage, on l'ajoute nous-mêmes if ($encodage ne "") { open(IN,"<:encoding($encodage)",$file); } my $texte=""; # lire le fichier ligne à ligne while (my $ligne=<IN>) { #enlever les retours à la ligne $ligne =~s/\n//g; #enlever les retours chariots $ligne=~s/\r//g; #concaténer et stocker le contenu des lignes nettoyées dans la variable $texte $texte .= $ligne; # enlever les espaces entre les balises $texte =~ s/> *?</></g; } close(IN); # on s'interesse au contenu qui correspond à cette expression régulière while ($texte=~/<item><title>([^<]+)<\/title>.+<description>([^<]+)<\/description>/g) { # on stocke le premier élément qui match dans la variable $titre my $titre=$1; # on nettoie le titre $titre=&nettoietexte($titre); # on stocke le deuxième élément qui match dans la variable $ description my $description=$2; # on nettoie la description $description=&nettoietexte($description); # on précise l'encodage pour le titre et la description pour les variables qui ne sont pas vides (résultant de balises vides) if (!(($titre eq "")or($description eq ""))) { if (uc($encodage) ne "UTF-8") { utf8($titre); utf8($description); } # on supprime les doublons et on stocke le contenu dans les fichiers de sortie créés précédemment if ((!(exists $dicoTitre{$titre})) && (!(exists $dicoDescription{$description}))) { $dicoTitre{$titre}="1"; $dicoDescription{$description}="1"; my $outTXT="TXT_sortie_".$idrubrique.".txt"; if (!open (OUTTXT,">>:encoding(UTF-8)", $sousrepertoireglobal.$outTXT)) { die "Pb a l'ouverture du fichier \n" }; print OUTTXT "Titre : ".$titre."\n"."Description : ".$description."\n"; close OUTTXT; my $outXML="XML_sortie_".$idrubrique.".xml"; if (!open (OUTXML,">>:encoding(UTF-8)", $sousrepertoireglobal.$outXML)) { die "Pb a l'ouverture du fichier \n" }; print OUTXML "<item><titre>".$titre."</titre><description>".$description."</description></item>\n"; close OUTXML; } } } } #-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-> Version XML::RSS <-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# #-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-> Procédure pour extraire le contenu des fichiers .xml <-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# sub extraiscontenu { my $file = shift@_; my $rss=new XML::RSS; eval {$rss->parsefile($file); }; if($@) { $@=~ s/at\/.*?$//s; } else { my $date=$rss->{'channel'}->{'pubDate'}; print OUTXML "<rss>\n"; print OUTXML "<date>$date</date>\n"; foreach my $item (@{$rss->{'items'}}) { my $titre=$item->{'title'}; my $resume=$item->{'description'}; if ((!(exists $dicoTitre{$titre})) and (!(exists $dicoDescription{$resume}))) { $dicoTitre{$titre}=1; $dicoDescription{$resume}=1; $titre=&nettoietexte($titre); $resume=&nettoietexte($resume); if (uc($encodage) ne "utf-8") {utf8($titre);utf8($resume);} print OUTTXT"Titre : $titre\n"; print OUTTXT"Resume : $resume\n"; print OUTXML "<item><title>$titre</title><abstract>$resume</abstract></item>\n"; } } } print OUTXML "</rss>\n"; }
Pour finir, il est impératif de nettoyer nos titres et nos descriptions des suites de caractères codés que l'on retrouve dans les documents XML. Pour cela il faut faire une liste des caractères indésirables à nettoyer. Il y a les plus connus comme les accents ou les signes de ponctuation et il y en a d'autre beaucoup moins évident. Il faut alors examiner le contenu des fichiers de sorties pour pouvoir construire un filtreur qui prennent un maximum de cas en compte.
#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-> Procédure pour nettoyer les caractères spéciaux <-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# sub nettoietexte { my $texte=shift; $texte=~s/</</g; $texte=~s/>/>/g; $texte=~s/<a href[^>]+>//g; $texte=~s/<img[^>]+>//g; $texte=~s/<\/a>//g; $texte=~s/&#34;/"/g; $texte=~s/<[^>]+>//g; $texte=~s/'/'/g; $texte=~s/"/"/g; $texte=~s/ //g; $texte=~s/–/–/g; $texte=~s/ & / & /g; $texte=~s/ / oe /g; $texte=~s/&/&/g; $texte=~s/&/&/g; $texte=~s/"/"/g; $texte=~s/'/'/g; $texte=~s/</</g; $texte=~s/</</g; $texte=~s/>/>/g; $texte=~s/>/>/g; $texte=~s/ //g; $texte=~s/£/£/g; $texte=~s/£/£/g; $texte=~s/©/©/g; $texte=~s/«/«/g; $texte=~s/«/«/g; $texte=~s/»/»/g; $texte=~s/»//g; $texte=~s/É/É/g; $texte=~s/É/É/g; $texte=~s/í/î/g; $texte=~s/î/î/g; $texte=~s/ï/ï/g; $texte=~s/ï/ï/g; $texte=~s/à/à/g; $texte=~s/à/à/g; $texte=~s/â/â/g; $texte=~s/â/â/g; $texte=~s/ç/ç/g; $texte=~s/ç/ç/g; $texte=~s/è/è/g; $texte=~s/è/è/g; $texte=~s/é/é/g; $texte=~s/é/é/g; $texte=~s/ê/ê/g; $texte=~s/ê/ê/g; $texte=~s/ô/ô/g; $texte=~s/ô/ô/g; $texte=~s/û/û/g; $texte=~s/û/û/g; $texte=~s/ü/ü/g; $texte=~s/ü/ü/g; $texte=~s/ü/ü/g; $texte=~s/\x9c/œ/g; $texte=~s/<br\/\>//g; $texte=~s/<img.*?\/>//g; $texte=~s/<a.*?>.*?<\/a>//g; $texte=~s/<![CDATA[(.*?)]]>/$1/g; $texte=~ s/<[^>]>//g; $texte=~s/\.$//; $texte=~s/&/et/g; return $texte; };