Boîte à outils 3 : extraction de patrons et de relations de dépendance


Le but de cette Boîte à Outils est de rechercher et d'extraire des patrons et relations de dépendance à partir des données textuelles étiquetées dans la BAO2. Plus précisément, il s'agit de:

  1. extraire des patrons avec Perl, Python, XSLT et XQuery (ces deux dernières méthodes sont traitées dans le cours 'Documents Structurés'). On travaille à partir des fichiers XML étiquetés avec TreeTagger.
  2. extraire des relations de dépendance avec Perl, XSLT, XQuery. On travaille à partir de la sortie UDpipe reformatée en XML.

PARTIE 1: EXTRATION DE PATRONS


Pour chaque rubrique, quatre patrons obligatoires sont à extraire:

NOM PREP NOM PREP
VERBE DET NOM
NOM ADJ
ADJ NOM

Il faut aussi extraire deux patrons facultatifs de longueur 3, sélectionnés en fonction de la rubrique.
Pour international, je suis partie du principe qu'il y était souvent questions de chefs d'Etat, de personnalités politiques et diplomatiques, donc de noms propres. Par conséquent, j'ai choisi les patrons NOM PREP NAM (par exemple "sortie des Britanniques") et NOM NAM NAM (par exemple "président Joe Biden").

Pour l'économie, j'ai considéré qu'il y avait beaucoup de chiffres (pourcentages, sommes, dates, etc.). Par conséquent, j'ai choisi le patron VERBE NUM NOM. Il y a également de nombreux noms d'organisations, par exemple 'Banque de France', qui suivent le patron NOM PREP NAM. Je l'ai donc choisi comme deuxième patron pour cette rubrique.

Enfin, dans la rubrique Idées, de nombreuses personnalités donnent leur point de vue sur un sujet. J'ai donc pensé que le patron NOM NAM NAM serait pertinent (par exemple "économiste Thomas Piketty"). J'ai choisi comme deuxième patron NAM PREP NOM.

Script Perl commenté

#!/usr/bin/perl
#--------------------------------------------------------------------
#pour lancer le script depuis le terminal: 
#perl programme.pl fichierTT patron
#exemple de patron: DET NC ADJ
#le fichierTT est un texte étiqueté et lemmatisé par treetagger 


#pour les patrons, voir le site
#--------------------------------------------------------------------

use utf8;
binmode STDOUT,":utf8";
#--------------------------------------------------------------------
#le nom de la rubrique à traiter est le premier argument; on l'enlève de la liste des arguments
my $fileatraiter=shift @ARGV;
#le patron correspond aux arguments restants
my @PATRON=@ARGV;


open my $input, "<:encoding(utf-8)",$fileatraiter;
#on lit l'ensemble du fichier et on ajoute le contenu dans une liste
my @LISTE=<$input>;
close($input);

#tant qu'on n'a pas fini de lire le texte, on récupère la ligne et on l'enlève de la liste (la liste sera vide quand on aura lu tout le texte)
while (my $ligne=shift @LISTE) {
	# si la ligne contenue dans $ligne correspond au premier du patron $PATRON[0]
	my $terme="";
	#on cherche le patron dans la ligne grâce à une expression régulière
	if ($ligne=~/<element><data type="type">$PATRON[0](:[^<]+?)?<\/data><data type="lemma">[^<]+?<\/data><data type="string">([^<]+?)<\/data><\/element>/) {
		#on récupère le premier groupe de capture (la forme)
		$terme=$terme.$2;
		#on crée une variable pour la longueur du patron
		my $longueur=1;
		#et l'indice du patron où on se trouve
		my $indice=1;
		# on lit autant de ligne qu'il y a d'élément dans le patron et on teste chaque patron
		while (($LISTE[$indice-1]=~/<element><data type="type">$PATRON[$indice](:[^<]+?)?<\/data><data type="lemma">[^<]+?<\/data><data type="string">([^<]+?)<\/data><\/element>/) and ($indice <= $#PATRON)) {
			#on incrémente l'indice et la longueur
			$indice++;
			#et on récupère la forme
			$terme.=" ".$2;
			$longueur++;
		}
		#si on a parcouru tout le patron
		if ($longueur == $#PATRON + 1) {
			#on ajoute le terme (donc le patron complet) dans un dictionnaire ; cela nous sert à compter les fréquences
			$dicoPatron{$terme}++;
			$nbTerme++;
		}
	}	
}
open my $fileResu,">:encoding(UTF-8)","NOMPREPNOMPREP-3210.txt";
print $fileResu "$nbTerme éléments trouvés\n";
#on parcourt le dictionnaire contenant toutes les expressions trouvées avec le patron et on l'écrit dans le fichier de sortie
foreach my $patron (sort {$dicoPatron{$b} <=> $dicoPatron{$a} } keys %dicoPatron) {
	print $fileResu "$dicoPatron{$patron}\t$patron\n";
}
close($fileResu);
  

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

Script Python commenté

#pour lancer le script depuis le terminal:
#python3 bao3.py fichier_tt patron
#exemple de patron : ADJ NOM
from typing import List
import re
import sys

#on définit une fonction pour extraire le patron du fichier d'entrée
def extract(corpus_file: str, output_file, patron: List[str]):
    #on crée un buffer: c'est une liste contenant des tuples de couples forme, POS. 
    #à mesure que l'on avance dans la lecture du fichier d'entrée, le buffer se vide d'une ligne et en ajoute une nouvelle. Cela permet de ne pas lire tout le fichier d'un coup, 
    #mais aussi de s'assurer de ne pas passer à côté de patrons
    #pour ne pas avoir d'erreur au début du programme (lorsqu'on videra le buffer), on remplit la liste avec autant de tuples qu'il y a d'élément dans le patron
    buf = [("---", "---")] * len(patron)
    #on ouvre le fichier d'entrée
    with open(corpus_file) as corpus:
        #on crée un dictionnaire pour compter les fréquences
        frequences = {}
        #à chaque nouvelle ligne qu'on lit
        for line in corpus:
            #on enlève le premier tuple du buffer
            buf.pop(0)
            #on cherche grâce à une expression régulière la forme et le POS 
            match = re.match('<element><data type="type">([^<]+?)</data><data type="lemma">[^<]+?</data><data type="string">([^<]+?)</data></element>', line) 
            if match:
                #s'il y a match, on récupère le POS et la forme et on les ajoute sous forme de tuple au buffer
                tag = match.group(1)
                forme = match.group(2)
                buf.append((tag,forme))
                #sinon, cela signifie qu'on change de ligne: dans ce cas on réinitialise le buffer
            else:
                buf = [("---", "---")] * len(patron)
            
            ok = True
            terme = ""
            #on parcourt le patron 
            for i, gat in enumerate(patron):
                #si le patron correspond au premier élément du tuple dans le buffer
                if gat == buf[i][0]:
                    #alors on stocke le deuxième élément du tuple (la forme du mot)
                    terme = terme + buf[i][1] + f"/{gat} "
                #sinon, on sort de la boucle
                else:
                    ok = False
            #si le premier élément du buffer correspond bien au patron
            if ok:
                #on vérifie si l'expression est déjà dans notre dictionnaire
                #si non, on l'ajoute au dictionnaire et on lui associe la fréquence 1
                if terme not in frequences: 
                    frequences[terme] = 1
                #si oui, on incrémente sa fréquence
                else: 
                    frequences[terme] += 1
        #une fois qu'on a tout parcouru, on tri le dictionnaire par ordre croissant de fréquence
        for exp in sorted(frequences, key=frequences.get, reverse=True):
            #et on écrit dans le fichier de sortie la fréquence et l'expression
            output_file.write(str(frequences[exp]) +"   "+ str(exp) + "\n")

if __name__ == "__main__":
    #le nom du fichier d'entrée est le premier argument
    corpus_file = sys.argv[1]
    #le patron correspond aux arguments suivants (donc à partir du deuxième)
    patron = sys.argv[2:]
    #pour pouvoir mettre le nom du patron dans le nom du fichier de sortie
    #on récupère dans une string les éléments du patron (sans espace)
    name = ""
    for pos in patron:
        name += pos
    #on ouvre le fichier de sortie et on appelle la fonction
    output_file = open(f"{name}-3210.txt", "w", encoding="utf-8")
    extract(corpus_file, output_file, patron)
    output_file.close()

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

Pour télécharger le fichier XSL (extrait les quatre patrons obligatoires) : Fichier XSLT

Pour télécharger le fichier XQuery (extrait les quatre patrons obligatoires) : Fichier XQuery

On obtient en sortie six fichiers TXT par rubrique (un pour chaque patron). Ils contiennent toutes les occurrences du patron et leur fréquence:

On peut maintenant faire une comparaison des patrons entre les rubriques.
En ce qui concerne le patron ADJ NOM, on voit que les bigrammes les plus fréquents sont complètement différents selon la rubrique.

En ce qui concerne le patron NOM ADJ, les rubriques sont beaucoup plus homogènes. On retrouve dans les trois le bigramme "crise sanitaire". Dans la rubrique International, les autres expressions fréquentes sont spécifiques au contexte diplomatique. On y retrouve notamment "Union européenne", "président Américain", "Commission Européenne" ou encore "affaires étrangères". Les rubriques Idées et Economie sont étonnamment assez proches. On y retrouve dans les deux cas l'expression "passe sanitaire", qui a eu un impact parfois négatif sur l'économie (notamment les secteurs de la restauration et du tourisme) mais a aussi donné lieu à des mouvements de contestation sociale importants. On retrouve de même l'expression "transition écologique", qui représente à la fois un défi pour les entreprises et un sujet de société important, donnant lieu à de nombreux débats.

En ce qui concerne le patron NOM PREP NOM PREP, il aurait fallu faire un patron un peu plus long pour être exploitable. Bien souvent, l'expression est tronquée et on ne peut pas savoir de quoi il s'agit. On trouve ainsi dans les patrons les plus fréquents "rédacteur en chef au", "dizaines de milliers de" ou encore "projet de loi de".

En ce qui concerne le patron VER DET NOM, on voit là encore se dessiner les spécificités de chaque sujet.

Enfin concernant les patrons facultatifs:

PARTIE 2: EXTRATION DE RELATIONS DE DEPENDANCE

#!/usr/bin/perl
#----------------------------------------------------------------------------------
# Pour lancer le script dans le terminal: 
# perl bao3-p2.pl sortie-up-rubrique.xml "relation" > fichier_sortie.txt
# En entrée : sortie UDPIPE formatée en XML + une relation syntaxique
# En sortie la liste triée des couples Gouv,Dep en relation
#----------------------------------------------------------------------------------
use strict;
use utf8;
binmode STDOUT, ':utf8';
#-------------------------------------------------------------------------------------
#
# my $rep="$ARGV[0]";
#la relation à extraire est le deuxième argument
my $relation="$ARGV[1]";
#on crée une table de hachage pour y stocker les couples dépendant-gouverneur et leur fréquence
my %dicoRelation=();
#-------------------------------------------------------------------------------------
# on découpe le texte par phrase (liste d'items annotés et potentiellement dépendants)
$/="</p>";
#on ouvre le fichier d'entrée (premier argument)
open my $IN ,"<:encoding(utf8)","$ARGV[0]";
#tant qu'on n'a pas fini de lire le fichier
while (my $phrase=<$IN>) {
	#-------------------------------------------------------------------------------------
	# on traite chaque "paragraphe" en le decoupant sur "items" (grâce au retour à la ligne). La longueur de la liste est donc égale au nombre de tokens dans la phrase. 
	my @LIGNES=split(/\n/,$phrase);
	#on crée un indice qui sera incrémenté tant qu'il sera inférieur ou égal à la longueur de la liste LIGNES
	for (my $i=0;$i<=$#LIGNES;$i++) {
		# si la ligne lue contient la relation, on ira chercher le dep puis le gouv
		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) {
			#on stocke le POS du dep, le POS du gouv et la forme du dep
			my $posDep=$1;
			my $posGouv=$3;
			my $formeDep=$2;
			# on cherche maintenant la forme du gouv: soit il est avant le dep, soit il est après
			if ($posDep > $posGouv) {
				for (my $k=0;$k<$i;$k++) {
					#si la ligne k contient le POS du gouverneur dans sa première balise <a>
					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>/) {
						#on stocke la forme
						my $formeGouv=$1;
						#on ajoute la forme du gouverneur et du dépendant à la table de hachage
						$dicoRelation{"$formeGouv $formeDep"}++;
					}
				}
			}
			#idem si le gouverneur est après le dep
			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;
						$dicoRelation{"$formeGouv $formeDep"}++;
					}
				}
			}
		}
	}
}
close ($IN);

# on imprime la liste des couples Gouv,Dep et leur fréquence...
foreach my $relation (sort {$dicoRelation{$b}<=>$dicoRelation{$a}} (keys %dicoRelation)) {
	print "$relation\t$dicoRelation{$relation}\n";
}

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

Pour télécharger le fichier XSL : Fichier XSLT

Pour télécharger le fichier XSL : Fichier XQuery

On obtient en sortie un fichier par rubrique et par relation:

  1. Pour la rubrique International (3210) : OBJNSUBJ
  2. Pour la rubrique Economie (3234): OBJNSUBJ
  3. Pour la rubrique Idées (3232): OBJNSUBJ

En ce qui concerne la relation NSUBJ, les résultats sont cohérents avec ce qui a été dit précédemment. Dans la rubrique International, on trouve de nombreuses expressions liées au pouvoir et aux gouvernements comme "président doit", "ministre doit", "ministre estime" ou "gouvernement appelé". D'autres expressions se réfèrent à des événements tragiques ("personnes tuées", "personnes mortes", "bilan s'alourdit"). On peut supposer qu'il s'agit des victimes de la pandémie ou de conflits. Enfin, on retrouve dans les paires de mots les plus fréquents "président russe" et "Etats-Unis vont". Cela semble montrer l'escalade des tensions entre ces deux puissances au cours de l'année.
Dans la rubrique Economie, on trouve des paires de mots très générales qui ne sont pas réellement spécifiques à l'année 2021 ("qui fait", "qui veut", "il s'agit"...). On peut cependant déceler l'arrivée du vaccin contre la Covid-19 et les questions que cela a posé sur le lieu de travail, à travers des expressions comme "l'équipe vaccinée".
Enfin dans la rubrique Idées, on remarque l'importance de l'obligation ("France doit", "qui doit", "communauté doit", "combat doit", "l'Europe doit"...). Cela confirme encore une fois que les participants à cette rubrique donnent leur opinion et expriment leur point de vue sur ce qui devrait être fait dans le pays ou dans le monde pour répondre à certains problèmes. L'expression la plus fréquente est "dessin signé", ce qui indique peut-être la présence d'illustrations ou de caricatures dans le journal.



En ce qui concerne la relation OBJ, on retrouve là encore des observations similaires. Dans la rubrique International, il y a de nombreuses personnalités politiques ("Vladimir Poutine, "l'opposant Alexeï", "der Leyen"...). On y voit également des problématiques internationales comme l'environnement ("réduire émissions"), la pandémie ("se vacciner") ou ce que je suppose être la crise migratoire ("en mer", "mer migrants").
La rubrique économie comprend essentiellement des noms de journalistes : "observe Philippe" se réfère à Philippe Escande, éditorialiste économique au Monde, "observe Laurence" à Laurence Girard, et "observe Jean-Michel" à Jean-Michel Bezat. Les autres expressions fréquentes se réfèrent au contexte économique difficile suite à la pandémie ("relancer l'économie", "supprimer postes", "lève millions").
La rubrique Idées apporte peu d'éléments nouveaux par rapport aux patrons. On y voit la diversité des intervenants (philosophes, sociologues, professeurs, économistes, politistes). Les autres paires de mots fréquentes sont très générales : "faire face", "mettre fin", "jouer rôle"...