ETAPES :
1) Extraire les patrons morphosyntaxiques grâce à l'étiquetage réalisé par TreeTagger dans la BAO2
2) Extraire les relations de dépendances grâce à l'étiquetage par UDPipe dans la BAO3
EN ENTREE :
Les sortie de la BAO2 : le fichier TreeTagger pour les patrons, le fichier UDPipe pour les relations.
EN SORTIE :
Pour les patrons morphosyntaxiques : les patrons "ADJ NOM", "NOM ADJ", "NOM PRP NOM PRP", "VER DET NOM",
"NUM NOM", "VER PRP" ont été testés sur la rubrique "A la une" (3208) en Python.
Pour les relations de dépendances : la relation "obj" a été testée en Perl.
Cependant, les versions du script dans les deux langages ont été réalisés (avec quelques différences mineures).
Deux exemples de sortie avec les patron "ADJ NOM" et "VER-DET-NOM sont consultables ci-dessous.
Il est néanmoins important de noter que n'importe quelle sortie avec une autre rubrique, un autre patron ou une autre relation peut être
réalisée grâce à ces scripts et les boites à outils précédentes. De plus, ces extractions de patrons et de relation
sont également réalisables avec des feuilles de style XSL et des requêtes XQuery (voir Exercice 14 du site Document
Structuré).
Langage | Nom de fichier | Téléchargement |
Python | python-extract_patron-ADJ-NOM-3208.txt | extraction_ADJ-NOM-3208.txt |
Python | python-extract_patron-VER-DET-NOM-3208.txt | extraction_VER-DET-NOM-3208.txt |
SCRIPTS :
Vous pouvez directement télécharger les scripts commentés de la BAO3 ci-dessous.
Langage | Ligne de commande | Téléchargement |
Perl | perl BAO3-patrons.pl fichier_treetagger.xml patron | BAO3-patrons.pl |
Python | python3 BAO3-patrons.py fichier_treetagger.xml patron(s)_separateur_tiret | BAO3-patrons.py |
Perl | perl BAO3-relations.pl fichier_udpipe.xml relation | BAO3-relations.pl |
Python | python3 BAO3-relations.py fichier_udpipe.xml relation | BAO3-relations.py |
Etape 1/3 : Etape préliminaire
On s'assure que les éléments du patron passés en argument sont
dans notre liste de possibilités, on récupère la rubrique et on crée également une chaine de
caractères contenant le patron complet (ex: ADJ-NOM) pour le nommage des fichiers.
#!/usr/bin/perl #----------------------------------------------------------- # Ligne de commande : perl BAO3-patrons.pl fichier_treetagger.xml patron(s)_separateur_tiret # Attention au chemin ! Le fichier résultat de treetagger fichier_treetagger.xml se trouve dans le dossier BAO2 #--------------------------------------------------------- # On précise l'encodage de sortie en UTF-8 use utf8; binmode STDOUT,":utf8"; #--------------------------------------------------------- # On prend le premier argument en tant que fichier à traiter # Le reste des arguments (correspondant au patron) est stocké dans une liste my $fichieratraiter=shift @ARGV; my @PATRON=@ARGV; #--------------------------------------------------------- my @PATRON_TT = ("ABR", "ADJ", "ADV", "DET", "DET:ART", "DET:POS", "INT", "KON", "NAM", "NOM", "NUM", "PRO", "PRO:DEM", "PRO:IND", "PRO:PER", "PRO:POS", "PRO:REL", "PRP", "PRP:det", "PUN", "PUN:cit", "SENT", "SYM", "VER", "VER:cond", "VER:futu", "VER:impe", "VER:impf", "VER:infi", "VER:pper", "VER:ppre", "VER:pres", "VER:simp", "VER:subi", "VER:subp"); # On s'assure que les patrons font bien partis de la liste # Et on affiche la liste si besoin for $pos (@PATRON) { if ( !grep( /^$pos$/, @PATRON_TT ) ) { print "Le ou les patrons n'ont pas été reconnu(s)\n"; print "Voir la liste des POS possibles ? \nTapez 'oui', ou n'importe quelle touche pour sortir "; # <STDIN> permet de récupérer la réponse de l'utilisateur dans le terminal my $voir_liste = <STDIN>; # chomp permet d'enlever la ligne de réponse de l'utilisateur chomp $voir_liste; if ($voir_liste eq "oui") { print "@PATRON_TT\n"; exit; } exit; } } #--------------------------------------------------------- # On récupère la rubrique et le patron complet qui nous servira aux noms des fichiers de sortie # Rq : la longueur du patron '$#PATRON' n'est pas égal à longueur du patron donnée par 'scalar @PATRON' # En réalité, $#PATRON = scalar @PATRON -1, ici $#PATRON est utilisé car notre indice pour le foreach commence à 0 my $rubrique = ""; if ($fichier=~/.*?-(\d+)\.xml/){ $rubrique = $1; } $patron=""; $i=0; foreach (@PATRON) { if ($i != $#PATRON){ $patron.="$PATRON[$i]-"; } else { $patron.="$PATRON[$i]"; } $i++; } #print $patron;
Etape 2/3 : La recherche des patrons
La stratégie adoptée a été de lire le fichier ligne par ligne, en enlevant la ligne lue de la liste au fur et à
mesure. Le match des éléments du patrons et la capture des formes sont réalisés grâce aux expressions régulières
déjà intégrés dans Perl.
# On ouvre le fichier à traiter et on stocke le contenu dans une liste open my $input, "<:encoding(utf-8)", $fichieratraiter; my @CONTENU=<$input>; close($input); # On lit la liste @CONTENU en la vidant : tant qu'il y a encore du contenu, on continue la lecture # une fois que la liste sera vide, cela signifiera que tout le contenu aura été lu while (my $ligne=shift @CONTENU){ my $chaine=""; my $indice=1; # si le premier élément/POS du patron ($PATRON[0]) a été matché if ($ligne=~/<element><data type="type">$PATRON[0]<\/data><data type="lemma">[^<]+?<\/data><data type="string">([^<]+?)<\/data><\/element>/) { # On incrémente la chaine avec la forme que l'on a trouvé et on utilise une variable pour voir la longueur du patron qu'on va pouvoir atteindre $chaine.=$1; my $longueur=1; # et il faut que je continue la lecture pour voir si la suite matche également avec le reste du patron while ($CONTENU[$indice-1]=~/<element><data type="type">$PATRON[$indice]<\/data><data type="lemma">[^<]+?<\/data><data type="string">([^<]+?)<\/data><\/element>/){ $indice++; $longueur++; $chaine.=" ".$1; } # Si tout le patron complet a été matché, alors on garde la chaine if (scalar @PATRON == $longueur) { #print "$chaine\n"; $dic_Patron{$chaine}++; $nbTerme++; } } }
Etape 3/3 : Ecriture des éléments trouvés
Les séquences trouvées avec le patron soumis sont écrites dans un fichier contenant dans son nom,
la rubrique et le patron correspondants. Ces données sont triées par ordre décroissant grâce au dictionnaire
créé en amont, qui stockait les séquences du patron trouvées tout en les comptant.
# On ouvre et écrit dans notre fichier de résultat open my $resultat,">:encoding(UTF-8)","perl-extract_patron-$rubrique-$patron.txt"; # On écrit le nombre de chaines trouvés selon le patron demandé print $resultat "$nbTerme éléments trouvés\n"; # On trie notre dictionnaire selon les fréquences # Pour chaque chaine du dictionnaire, on écrire la clé suivie de son nombre d'occurrences dans le texte foreach my $patron (sort {$dic_Patron{$b} <=> $dic_Patron{$a} } keys %dic_Patron) { print $resultat "$dic_Patron{$patron}\t$patron\n"; } close($resultat); move("perl-extract_patron-$rubrique-$patron.txt", "BAO3") or die "Erreur $!"; #---------------------------------------------------------
Note : 4 méthodes différentes ont été réalisées, seule la 3ème sera commentée ici
Différence majeure avec le script Perl 1/3 : méthodes chronométrés
L'étape préliminaire du script est la même qu'en perl (patron valide, chaine contenant le nom du patron
séparé par des tirets pour le nommage du fichier...),
à la seule différence que les bibliothèques doivent être importées. La bibliothèque "time" notamment
permet d'évaluer le temps d'exécution de chaque méthode (voir plus de commentaires dans le script à télécharger,
avec notamment une zone de test qui permet de voir les différences de temps entre les méthodes)
#!/usr/bin/python3 #----------------------------------------------------------- # Ligne de commande : python3 BAO3-patrons.py fichier_treetagger.xml patron(s)_separateur_tiret # Attention au chemin ! Le fichier résultat de treetagger fichier_treetagger.xml se trouve dans le dossier BAO2 ################################################################################### # Importation des bibliothèques dont on a besoin from typing import List import re import sys from pathlib import Path import shutil, os from os.path import exists import time start_time = time.time() .... # Comme en perl, on teste si le patron est valide patron_TT = ["ABR", "ADJ", "ADV", "DET", "DET:ART", "DET:POS", "INT", "KON", "NAM", "NOM", "NUM", "PRO", "PRO:DEM", "PRO:IND", "PRO:PER", "PRO:POS", "PRO:REL", "PRP", "PRP:det", "PUN", "PUN:cit", "SENT", "SYM", "VER", "VER:cond", "VER:futu", "VER:impe", "VER:impf", "VER:infi", "VER:pper", "VER:ppre", "VER:pres", "VER:simp", "VER:subi", "VER:subp"]; def TestPatron(patron): for tag in patron: if tag not in patron_TT: return False return True #--------------------------------------------------------- # Et on récupère un patron avec des tags séparés de tirets pour le fichier de sortie def Patron_avec_tirets(patron): chaine = "" # Si le patron est valide if TestPatron(patron): # On incrémente la chaine avec les tags du patron jusqu'au dernier for i in range(len(patron)-1): chaine += patron[i] + "-" # Le dernier est ajouté sans tiret chaine += patron[-1] return chaine return "erreur"
Différence majeure avec le script Perl 2/3 : utilisation d'un buffer
On utilise une fenêtre glissante (buffer) pour lire le fichier par bloc de lignes (à la taille du patron)
permettant ainsi d'avoir une meilleure rapidité d'exécution (et donc d'être moins coûteux). On récupère dans
le buffer la forme et le tag (=POS) du bloc de lignes et on le compare au patron. S'il est identique
(ou presque, par ex si on a un patron avec VER et comme tag VER:cond) alors on garde la séquence. Puis on passe
à la prochaine ligne, et ainsi de suite jusqu'à atteindre la fin du document, on s'arrête dès que la longueur
du patron dépasse celle des lignes restantes du document (le patron ne pourra plus être trouvé car il n'y
a plus assez de lignes).
# 3ème méthode on s'appuie sur la méthode du buffer de la méthode précédente def extract3(corpus_file: str, patron: List[str]) : #chemin du fichier corpus_file # Comme la 2ème méthode, on initialise notre buffer # Mais cette fois-ci avec deux emplacements "---" pour chaque tag du patron buf = [("---", "---")] * len(patron) # On ouvre notre corpus with open(corpus_file) as corpus : # Pour chaque ligne du corpus for line in corpus : # On enlève le premier couple de notre liste buf.pop(0) # On crée notre match qui va capturer le tag et la forme présents dans la ligne match = re.match(f'<element><data type="type">([^<]+?)</data><data type="lemma">[^<]+?</data><data type="string">([^<]+?)</data></element>', line) # Si on a bien matché la ligne contenant le tag et la forme # On ajoute le couple dans le buffer et on incrémente la chaine avec la forme if match : tag = match.group(1) forme = match.group(2) buf.append((tag,forme)) # Si on a pas pu matcher la ligne # Cela signifie que l'on est soit en début soit en fin de corpus, soit que l'on change de phrase # Dans ces cas là, il faut réinitialiser le buffer à zéro (avant d'entamer la prochaine phrase notamment) else : buf = [("---", "---")] * len(patron) # break # On se base sur le même modèle que précédemment avec une légère variation chaine = "" ok = True for i, gat in enumerate(patron) : # Ici, si le premier élément du couple (tag, forme) est égal au tag correspondant du patron demandé # Attention, on veut également matcher les tags comme VER qui se trouvent sous la forme VER:cond # d'où le match et non le "=" strict if re.match(gat, buf[i][0]): chaine += buf[i][1] + " " else: ok = False if ok : global f f.write(chaine + "\n")
Différence majeure avec le script Perl 3/3 : liste de patrons en argument
Afin d'automatiser encore plus les patrons passés en argument, une fonction a été crée pour
découper les patrons (délimiteur/séparateur : le tiret). Pour cela, une liste de listes a été crée
permettant dans la fonction principale main() d'appeller la fonction d'extraction sur chaque patron
de la grande liste.
# Fonction afin d'automatiser les patrons demandés # On va créer une grande liste de patrons contenant des patrons (qui sont eux-mêmes des listes contenant les tags) def DecoupPatrons(liste_patrons: List[str]): grande_liste=[] petite_liste=[] for e in liste_patrons: # On ajoute la petite liste dès qu'on trouve un séparateur et on la vide if e == "-": grande_liste.append(petite_liste) petite_liste=[] continue else : petite_liste.append(e) # On n'oublie pas le dernier patron qui lui n'a pas de séparateur à la fin # avant de return la liste de liste (grande_liste) crée grande_liste.append(petite_liste) return grande_liste #--------------------------------------------------------- # Programme principal if __name__== "__main__": # On stocke les arguments passés au programme comme suit : # Le premier argument sera notre corpus, d'où on récupère la rubrique comme pour perl # Tout le reste des arguments sera notre patron corpus_file = sys.argv[1] p_rubrique = re.search("-(\d+).xml", corpus_file) rubrique = p_rubrique.group(1) patrons = sys.argv[2:] ################################### # Zone de test, à commenter/décommenter pour tester les différentes méthodes pour comparer le temps d'exécution ... ################################### for patron in DecoupPatrons(patrons): if Patron_avec_tirets(patron) != "erreur" : fichier = f"python-extract_patron-{rubrique}-{Patron_avec_tirets(patron)}.txt" # Si le fichier a déjà été créé, on le supprime afin de laisser place au nouveau if os.path.exists(f"./BAO3/{fichier}"): os.remove(f"./BAO3/{fichier}") with open(fichier, "w", encoding="utf-8") as f: extract3(corpus_file, patron) shutil.move(fichier, "BAO3") else : print("Attention, il y a eu au moins une erreur dans les patrons donnés. Seuls les patrons corrects ont été réalisés.")
Etape 1/3 : Etape préliminaire
L'étape préliminaire s'occupe de prendre les arguments passés au programme et de récupérer
la rubrique traitée. On initialise également notre dictionnaire de comptage des relations
et on découpe notre texte selon la balise </p>.
#!/usr/bin/perl #---------------------------------------------------------------------------------- # Ligne de commande : perl BAO3-relations.pl fichier_udpipe.xml relation # Attention au chemin ! Le fichier résultat de udpipe fichier_udpipe.xml se trouve dans le dossier BAO2 # En entrée : sortie UDPIPE formatée en XML + une relation syntaxique # En sortie la liste triée des couples (gouverneur, dépendant) en relation #---------------------------------------------------------------------------------- use strict; use utf8; binmode STDOUT, ':utf8'; #------------------------------------------------------------------------------------- my $fichier="$ARGV[0]"; my $rubrique = ""; if ($fichier=~/.*?-(\d+)\.udpipe\.xml$/){ $rubrique = $1; } my $relation="$ARGV[1]"; my %dicoRelation=(); # on découpe le texte par phrase (liste d'items annotés et potentiellement dépendants) $/="</p>";
Etape 2/3 : La recherche des éléments en relation de dépendance
Le dépendant de la relation aura dans sa ligne, la relation demandée (ex: obj) et l'identifiant de son gouverneur,
dont on doit aller chercher sa forme, soit avant, soit après cet élément. Le script ici permet donc de comparer
l'identifiant du dépendant trouvé (contenant la relation étudiée) avec celui de son gouverneur, afin d'aller
chercher sa forme selon son emplacement.
open my $IN ,"<:encoding(utf8)",$fichier; while (my $phrase=<$IN>) { # on traite chaque "paragraphe" (balises <p>) en le découpant par "items" my @LIGNES=split(/\n/,$phrase); for (my $i=0;$i<=$#LIGNES;$i++) { # si la ligne lue contient la relation, on ira chercher le dépendant puis le gouverneur if ($LIGNES[$i]=~/<item><a>([^<]+)<\/a><a>([^<]+)<\/a><a>[^<]+<\/a><a>[^<]+<\/a><a>[^<]+<\/a><a>[^<]+<\/a><a>([^<]+)<\/a><a>[^<]*$relation[^<]*<\/a><a>[^<]+<\/a><a>[^<]+<\/a><\/item>/i) { my $posDep=$1; my $posGouv=$3; my $formeDep=$2; # soit le gouverneur est avant le dépendant, soit après # on fait donc les deux cas de figure : # si le gouverneur est avant le dépendant, on parcourt les lignes de 0 à l'endroit où on a trouvé le dépendant (exclu) if ($posGouv < $posDep) { for (my $k=0;$k<$i;$k++) { # On cherche la ligne du gouverneur selon son identifiant et on capture sa forme if ($LIGNES[$k]=~/<item><a>$posGouv<\/a><a>([^<]+)<\/a><a>[^<]+<\/a><a>[^<]+<\/a><a>[^<]+<\/a><a>[^<]+<\/a><a>[^<]+<\/a><a>[^<]+<\/a><a>[^<]+<\/a><a>[^<]+<\/a><\/item>/) { my $formeGouv=$1; # On rajoute dans notre dictionnaire le couple gouverneur et dépendant trouvé $dicoRelation{"$formeGouv $formeDep"}++; } } } # si le gouverneur est après le dépendant alors on commence à la ligne suivante jusqu'à la dernière ligne de la phrase else { for (my $k=$i+1;$k<=$#LIGNES;$k++) { if ($LIGNES[$k]=~/<item><a>$posGouv<\/a><a>([^<]+)<\/a><a>[^<]+<\/a><a>[^<]+<\/a><a>[^<]+<\/a><a>[^<]+<\/a><a>[^<]+<\/a><a>[^<]+<\/a><a>[^<]+<\/a><a>[^<]+<\/a><\/item>/) { my $formeGouv=$1; # On rajoute dans notre dictionnaire le couple gouverneur et dépendant trouvé $dicoRelation{"$formeGouv $formeDep"}++; } } } } } } close ($IN);
Etape 3/3 : Ecriture des éléments trouvés
De la même manière que pour les patrons, on écrit les séquences trouvées avec la relation demandée
dans un fichier contenant dans son nom, la rubrique et la relation correspondantes.
Ces données sont également triées par ordre décroissant grâce au dictionnaire, d'après le même principe.
# On ouvre et écrit dans notre fichier de résultat avant de le déplacer open my $resultat,">:encoding(UTF-8)","perl-extract_relation-$rubrique-$relation.txt"; # On imprime la liste des couples gouverneurs et dépendants et selon leur fréquence, triée par ordre décroissant foreach my $relation (sort {$dicoRelation{$b}<=>$dicoRelation{$a}} (keys %dicoRelation)) { print $resultat "$relation\t$dicoRelation{$relation}\n"; } close($resultat); move("perl-extract_relation-$rubrique-$relation.txt", "BAO3") or die "Erreur $!"; #---------------------------------------------------------
Note : Ce script formate les données d'une façon particulière car ce dernier va servir à la BAO4 qui n'aura que quelques différences mineures avec le script ci-dessous.
Différence majeure avec le script Perl 1/3 : Fonction de nettoyage
Comme énoncé plus haut, une fonction de nettoyage a été rajoutée pour la préparation de la BAO4 car celui-ci aura besoin
de données les plus uniformes possibles.
# Fonction de nettoyage def clean(s: str) : s=re.sub("[^\w]", "", s) s=re.sub("’","'", s) s=re.sub(",","",s) s=re.sub("@","", s) s=re.sub("ᵉ","", s) s=re.sub("°","", s) return s
Différence majeure avec le script Perl 2/3 : utilisation d'un buffer, set des couples trouvés...
On utilise un buffer qui stocke tous les identifiants et formes de chaque token de la phrase,
avant de se vider pour accueillir la prochaine. Chaque couple est ajouté à un set, qui par défaut,
ne conservera que des couples uniques. Une syntaxe intéressante peut être relevée pour Python, qui n'est pas
réalisable en Perl :
fields = re.findall("<a>([^<]+)</a>", line)
idx, word, lemma, tag, _, _, head, rel, _, _ = fields
En Python, cette syntaxe qui (a l'avantage d'être compacte) est bien valide :
fields contenant exactement 8 éléments, chaque élément sera mis dans sa variable correspondante.
def Couple(f, relation): # On initialise les variables dont on va avoir besoin # Un buffer sous forme de dictionnaire pour stocker les identifiants et lemme de chaque token (réinitialisé à chaque phrase) sent_buf = {} # Un buffer sous forme de liste pour stocker des tuples contenant le lemme du dépendant et l'identifiant de son gouverneur obj_buf = [] # Trois sets pour avoir une liste sans doublons des couples, gouverneurs et dépendants couples = set() gouvernors = set() deps = set() # Pour chaque ligne for line in Path(f).read_text().split("\n"): # Lorsqu'on tombe sur un item, on cherche tous les éléments avec balises <a> # On en trouve normalement 8 qu'on met dans les différentes variables # Ceux qui ne nous sont pas utiles sont stockés dans la variable "_" if line.startswith("<item>"): fields = re.findall("<a>([^<]+)</a>", line) idx, word, lemma, tag, _, _, head, rel, _, _ = fields lemma = clean(lemma) # On stocke chaque lemme dans le dictionnaire sent_buf avec comme clé son identifiant sent_buf[idx] = lemma # Si la relation est obj, on ajoute le lemme et le gouverneur sous forme de tuple dans la liste obj_buf if rel == relation: obj_buf.append((lemma, head)) # Si on arrive à la fin d'une phrase (signalé avec la balise fermante "</p>") if line == "</p>": # Pour chaque lemme et gouverneur dans la liste obj_buf for dep_lemma, head in obj_buf: # print(f'{sent_buf[head]}, "--[{relation}]-->", {dep_lemma}') # On ajoute le lemme du gouverneur et du dépendant en tant que couple # Le lemme du gouverneur ayant été trouvé grâce au numéro "head" (identifiant du gouverneur) # qui sert de clé pour trouver le lemme associé dans le dictionnaire sent_buf couples.add((f"{sent_buf[head]}", f"{dep_lemma}")) # On ajoute le gouverneur dans le set des gouverneurs # On ajoute le dépendant dans le set des dépendants gouvernors.add(sent_buf[head]) deps.add(dep_lemma) # On vide nos buffers pour la phrase suivante obj_buf = [] sent_buf = {} return couples
Différence majeure avec le script Perl 3/3 : affichage des couples
On affiche dans le terminal les couples trouvés, sans doublons.
(Rq : On aurait pu très bien les écrire dans un fichier comme en Perl).
# Programme principal if __name__== "__main__": fichier = sys.argv[1] relation = sys.argv[2] print(Couple(fichier,relation)) #-------------------------------------------------------------------------