Boîte à outils 2


Le but de cette Boîte à Outils est de faire l'étiquetage des données textuelles issues des fils RSS. Les outils choisis sont TreeTagger (pour l'annotation morphosyntaxique) et UDPipe (pour l'annotation en relations de dépendance). Les scripts qui suivent reprennent donc en grande partie ceux de la BAO1 (extraction d'un fil RSS, parcours de l'arborescence, etc.) et incorporent l'étiquetage.


Script Perl commenté

#/usr/bin/perl
#-----------------------------------------------------------
use utf8;
use strict;
binmode(STDOUT, ":encoding(UTF-8)");
#-----------------------------------------------------------
# Pour lancer le programme depuis le terminal :
# perl bao2.pl chemin_dossier_corpus numero_rubrique
#il faut avoir préalablement télécharger tout ce qui est nécessaire au bon fonctionnement de UdPipe et TreeTagger

#-----------------------------------------------------------
#on s'assure qu'il y a le bon nombre d'arguments
if ($#ARGV != 1) {print "Il manque un argument à votre programme....\n";exit;} 
#le répertoire contenant le corpus est le premier argument
my $rep="$ARGV[0]";
#le numero de rubrique est le deuxième argument
my $RUBRIQUE="$ARGV[1]";
# on s'assure que le nom du répertoire ne se termine pas par un "/"
$rep=~ s/[\/]$//;
#on ouvre les fichiers de sortie
open my $output, ">:encoding(UTF-8)","corpus-titre-description-${RUBRIQUE}.txt";
open my $output2, ">:encoding(UTF-8)","pre-corpus-titre-description${RUBRIQUE}.xml";

#on ajoute la déclaration XML dans le fichier XML
print $output2 "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<corpus>\n";
#----------------------------------------
#on lance le sous-programme qui va parcourir l'arborescence (méthode récursive)
&parcoursarborescencefichiers($rep);
#----------------------------------------
print $output2 "</corpus>\n";
close $output;
close $output2;

#----------------------------------------
#1. on annote le fichier TXT avec UdPipe
&etiquetageUP;
#2. on annote le fichier XML avec TreeTagger
&etiquetageTT;
#----------------------------------------------
exit;

#----------------------------------------------
#fonction pour étiqueter le contenu textuel avec UdPipe
sub etiquetageUP {
	#udpipe --tokenize --tag --parse udpipe_model file  > result_udpipe
	system("./distrib-udpipe-1.2.0-bin/udpipe-1.2.0-bin/bin-win64/udpipe.exe --tokenize --tag --parse --tokenizer=presegmented ./distrib-udpipe-1.2.0-bin/modeles/french-sequoia-ud-2.5-191206.udpipe corpus-titre-description-${RUBRIQUE}.txt > conll-${RUBRIQUE}.udpipe");
	system("perl ./distrib-udpipe-1.2.0-bin/udpipe2xml-version-sans-titrevsdescription-v2.pl conll-${RUBRIQUE}.udpipe");
}
#----------------------------------------------
#fonction pour étiqueter le contenu textuel avec TreeTagger (via le terminal)
sub etiquetageTT {
	system("./TreeTagger/tree-tagger.exe -lemma -token -no-unknown -sgml ./TreeTagger/french-utf8.par  pre-corpus-titre-description${RUBRIQUE}.xml  >  sortie-tt-${RUBRIQUE} ");
	system("perl ./TreeTagger/treetagger2xml-utf8.pl sortie-tt-${RUBRIQUE} UTF8");
	
}
#----------------------------------------------
#même sous-programme que dans la BAO1 pour parcourir l'arborescence
sub parcoursarborescencefichiers {
    my $path = shift(@_);
    opendir(DIR, $path) or die "can't open $path: $!\n";
    my @files = readdir(DIR);
    closedir(DIR);
    foreach my $file (@files) {
		next if $file =~ /^\.\.?$/;
		$file = $path."/".$file;
		if (-d $file) {
			print "On entre dans le REPERTOIRE : $file \n";
			&parcoursarborescencefichiers($file);	#recurse!
			print "On sort du REPERTOIRE :  $file \n";
		}
		if (-f $file) {
			if ($file =~ /$RUBRIQUE.+\.xml$/) {
				print "Traitement du fichier $file \n";
				open my $input, "<:encoding(UTF-8)",$file;
				$/=undef; # par défaut cette variable contient \n
				my $ligne=<$input> ;
				close($input);
				while ($ligne=~/<item><title>(.+?)<\/title>.+?<description>(.+?)<\/description>/gs) {
					my $titre=&nettoyage($1);
					my $description=&nettoyage($2);
					print $output "$titre \n";
					print $output "$description \n";
					# segmentation titre et description avec le programme tokenize offert par treetagger
					my ($titreSEG,$descriptionSEG)=&segmentationTD($titre,$description);
					print $output2 "<item><titre>\n$titreSEG\n</titre><description>\n$descriptionSEG\n</description></item>\n";
				}
			}
		}
    }
}
#----------------------------------------------
#on crée une fonction qui va crée des fichiers "tampon" afin de récupérer les données textuelles 
#elle prend comme arguments le titre et la description
#cela est contraint par le mode de fonctionnement de TreeTagger qui prend en entrée un fichier

sub segmentationTD {
	my ($arg1,$arg2)=@_;
	#-----------------------------------------------
	#d'abord on écrit le contenu textuel dans le premier fichier tampon
	open my $tmp, ">:encoding(UTF-8)","temp1.txt";
	print $tmp $arg1;
	close $tmp;
	#puis on le tokenize le texte avec TreeTagger (via le terminal) et écrit le résultat dans le deuxième fichier tampon
	system("perl ./TreeTagger/tokenise-utf8.pl temp1.txt > temp2.txt");
	undef $/;
	open my $tmp2, "<:encoding(UTF-8)","temp2.txt";
	#on récupère le contenu segmenté dans le deuxième fichier tampon
	my $titresegmente=<$tmp2>;
	close $tmp2;
	#-----------------------------------------------
	#on applique le même traitement à la description (le deuxième argument passé au sous-programme)
	open $tmp, ">:encoding(UTF-8)","temp1.txt";
	print $tmp $arg2;
	close $tmp;
	system("perl ./TreeTagger/tokenise-utf8.pl temp1.txt > temp2.txt");
	open  $tmp2, "<:encoding(UTF-8)","temp2.txt";
	my $descriptionsegmente=<$tmp2>;
	close $tmp2;
	$/="\n";
	#-----------------------------------------------
	#on récupère les données segmentées
	return $titresegmente,$descriptionsegmente;
}

#----------------------------------------------
#on reprend la même fonction de nettoyage que dans la BAO1
sub nettoyage {
	my $texte=shift @_;
	$texte=~s/(^<!\[CDATA\[)|(\]\]>$)//g;
	$texte.=".";
	$texte=~s/\.+$/\./;
	return $texte;
}
#----------------------------------------------

  

Pour télécharger le script Perl : Script Perl

Script Python commenté

# Pour lancer le programme: 
# python3 bao1.py chemin_du_corpus numero_rubrique
#-----------------------------------------------------------
#on importe les mêmes bibliothèques que dans la BAO1
import sys
from pathlib import Path
import re
import os

#mais aussi spacy_udpipe pour faire l'annotation en relations de dépendance
import spacy_udpipe

#si on n'a pas déjà téléchargé le modèle fr-sequoia, décommenter la ligne suivante: 
#spacy_udpipe.download("fr-sequoia")
#on charge le modèle
udpipe = spacy_udpipe.load("fr-sequoia")

#on définit l'expression régulière qui va nous servir à reconnaître les titres et les descriptions    
regex_item = re.compile("<item><title>(.*?)<\/title>.*?<description>(.*?)<\/description>")
    
#on crée une fonction de nettoyage identique à celle du script Perl       
def nettoyage(texte):
    texte_net = re.sub("<!\[CDATA\[(.*?)\]\]>", "\\1", texte)
    #on ajoute un point à la fin de chaque titre ou description pour faciliter l'étiquetage
    texte_net += "."
    #s'il y a plusieurs points, on en laisse un seul
    texte_net = re.sub("\.+$",".",texte_net)
    return texte_net

#on crée une fonction pour segmenter le texte : TreeTagger a besoin d'un texte segmenté pour faire l'étiquetage
def segmentationTD(texte):
    #pour cela on utilise le même principe de fichiers tampon que dans le script Perl
	temp = open("temp1-py.txt","w")
	temp.write(texte)
	temp.close()
	os.system("perl ./TreeTagger/tokenise-utf8.pl temp1-py.txt > temp2-py.txt")
	temp2 = open("temp2-py.txt","r")
	textesegmente = temp2.read()
	temp2.close()
	return textesegmente    

#on crée une fonction pour transformer chaque token étiqueté par UDPipe en balise XML
#chaque token correspond à une balise <element> dans laquelle on trouve trois balises data: une pour le POS, une pour le lemme et une pour le token 
def token2xml(token):
    return f"<element><data type='type'>{token.pos_}</data><data type='lemma'>{token.lemma_}</data><data type='string'>{token.text}</data></element>\n"

#on crée une fonction pour réaliser l'étiquetage avec UDPipe. Contrairement au script Perl, pas besoin de passer par le terminal car on utilise spacy_udpipe
def etiquetageUP (texte):
    #on analyse le texte entier
    doc = udpipe(texte)
    #on crée une chaîne vide
    result = ""
    #pour chaque token étiqueté dans le texte
    for token in doc:
        #on appelle la fonction qui le transforme en balise XML <element>
        result += token2xml(token)
    return result

#on crée une fonction pour réaliser l'étiquetage avec TreeTagger. Comme en Perl, on écrit une commande dans le terminal grâce à la méthode os.system()
def etiquetageTT (rubrique):
	os.system(f"./TreeTagger/tree-tagger.exe -lemma -token -no-unknown -sgml ./TreeTagger/french-utf8.par  pre-corpus-titre-description-py-{rubrique}.xml  >  sortie-tt-py-{rubrique} ");
	os.system(f"perl ./TreeTagger/treetagger2xml-utf8.pl sortie-tt-py-{rubrique} UTF8");    
    


#comme dans la BAO1, on crée une fonction qui permet d'extraire le titre et la description pour un seul fichier
#ajout par rapport à la BAO1 : l'argument "output_temp" qui correspond au fichier pre-corpus-titre-description qui va servir à faire l'étiquetage avec TreeTagger
#et l'argument "output_udpipe    
def extract_un_fil(fichier_rss, output_udpipe, output_txt, output_temp, titres_uniques):
#on ouvre le fichier d'entrée
    with open(fichier_rss, "r") as input_rss:      
        #on lit et on stocke le contenu du fichier d'entrée
        lignes = input_rss.readlines()
        texte = "".join(lignes)
        #chaque fois qu'on trouve quelque chose qui correspond à notre expression régulière
        for m in re.finditer(regex_item, texte):
            #on récupère et on nettoie le titre
            titre_net = nettoyage(m.group(1))
            #idem pour la description
            description_net = nettoyage(m.group(2))
            #si le titre ne se trouve pas déjà dans les titres rencontrés
            if titre_net not in titres_uniques: 
                #on écrit le titre et sa description dans les trois fichiers de sortie le fichier xml udpipe, le fichier txt et le fichier qui va servir à l'étiquetage TreeTagger
                titres_uniques.add(titre_net)
                output_txt.write(titre_net)
                output_txt.write("\n")
                output_txt.write(description_net)
                output_txt.write("\n")
                #on étiquette le titre et la description avec UDPipe
                titre_etiquete = etiquetageUP(titre_net)
                description_etiquetee = etiquetageUP(description_net)
                #et on l'écrit dans le fichier xml
                item_xml = f"<item><titre>{titre_etiquete}</titre><description>{description_etiquetee}</description></item>\n"
                output_xml.write(item_xml)
                #on segmente le titre et la description
                titreseg = segmentationTD(titre_net)
                descriptionseg = segmentationTD(description_net)
                #et on l'écrit dans le fichier xml qui servira à TreeTagger
                output_temp.write(f"<item><titre>\n{titreseg}\n</titre><description>\n{descriptionseg}\n</description></item>\n")        


#comme dans la BAO1
#on crée une fonction pour parcourir l'arborescence
def parcours(dossier : Path, fichier_xml, fichier_txt, fichier_temp, rubrique):
    for sub in sorted(dossier.iterdir()):
        if sub.is_dir():
            print(f"On entre dans le REPERTOIRE : {sub} \n")
            parcours(sub, fichier_xml, fichier_txt, fichier_temp, rubrique)
        if sub.is_file() and sub.name.endswith(".xml") and rubrique in sub.name:
            print(f"On traite le fichier {sub}\n")
            extract_un_fil(sub, fichier_xml, fichier_txt, fichier_temp, titres_uniques)


#une fois qu'on a créé toutes les fonctions, on les applique à notre corpus

#on récupère le chemin du dossier contenant le corpus 2021
dossier = Path(sys.argv[1])
#on récupère le numéro de la rubrique à traiter
rubrique = sys.argv[2]
with open (f"bao2-{rubrique}-py.xml", "w", encoding="utf-8") as output_xml:
    with open(f"corpus-titre-description-py-{rubrique}.txt", "w", encoding="utf-8") as output_txt:
        with open(f"pre-corpus-titre-description-py-{rubrique}.xml", "w", encoding="utf-8") as output_temp:
            header = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<corpus>\n"
            #on écrit la déclaration XML dans le fichier de sortie
            output_xml.write(header)
            #on crée un set qui va servir à s'assurer qu'il n'y a pas de doublons dans les fichiers de sortie
            titres_uniques= set()
            parcours(dossier, output_xml, output_txt, output_temp, rubrique)
            output_xml.write("</corpus>\n")
    #une fois qu'on a le fichier pre-corpus-titre-description, on appelle la fonction pour étiqueter avec TreeTagger
    etiquetageTT(rubrique)
            

Pour télécharger le script Python : Script Python

Résultats


Le script Perl produit en sortie trois nouveaux fichiers par rubrique:

  1. Un fichier XML étiqueté avec TreeTagger. InternationalEconomieIdées
  2. Un fichier COnLL étiqueté avec UdPipe. InternationalEconomieIdées
  3. Le fichier COnLL transformé en fichier XML. InternationalEconomieIdées

Le script Python ne produit pas de fichier COnLL car il utilise spacy_udpipe et non UDPipe. Nous nous servirons des fichiers XML pour la BAO3.