stage_2023/utilitaire_python/textes_à_tmx_(lxml_objectif...

273 lines
10 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
'''
Créer une mémoire de traduction à partir de fichiers texte parallèles.
Cet utilitaire produit un fichier TMX conforme à la version 1.4 de la spécification TMX (https://www.gala-global.org/tmx-14b, en anglais) à partir de fichiers encodé en UTF-8 qui contiennent le même nombre de lignes.
Limitations:
1. Les fichiers texte doivent être placé dans un répertoire `fichiers_texte`
du répertoire de l'utilitaire.
2. Ces fichiers doivent avoir une extension `.txt` et être encodés en UTF-8.
Leur nom doit se terminer par `_LL`, où LL est le code de la langue du
fichier.
3. On suppose que la mémoire part d'un texte en français, qui est donc
désigné comme langue source. (Cela signifie également qu'il doit
impérativement y avoir un fichier `texte_fr.txt` parmi les fichiers
à aligner.)
4. Le fichier TMX est sauvegardé dans un répertoire `fichier_tmx` créé dans
le répertoire de l'utilitaire, avec le même nom de base que celui du
fichier en français.
Utilisation:
1. Mettez les fichiers à aligner dans le sous-répertoire `fichiers_texte`,
en vous assurant qu'ils sont conformes aux limitations ci-dessus.
2. Lancez le script depuis la ligne de commande ou votre éditeur.
Avec un éditeur, il est possible que le chemin d'exécution ne soit pas
celui du script, et vous obtiendrez une erreur parce que le script ne
trouvera pas les fichiers texte.
Dans un tel cas, vérifiez les réglages de votre éditeur, ou modifiez
le chemin des fichiers dans le code.
'''
##################################################
#
# textes_à_tmx.py
#
##################################################
# Version 0.3.1 du 06/01/2023
#
# Auteur: Philippe Tourigny (kazephil@gmail.com)
#
# Code créé à titre d'exemple dans le cadre du stage
# des M2 TRE du printemps 2023.
#
# À faire:
#
# L'utilitaire a été conçu en supposant la création d'une interface graphique
# dans une version ultérieure.
# L'interface graphique prévoit:
# - permettre de sélectionner les fichiers à aligner;
# - permettre de saisir les langues source et cible(s);
# - permettre de sélectionner le répertoire et le nom du fichier TMX
# - donner le choix de continuer ou d'arrêter si les fichiers ne contiennent
# pas tous le même nombre de lignes.
#
# Autres améliorations à apporter:
# - Ajouter du code pour traiter les erreurs.
#
# Licence
# Le code est distribué sous la license GPL3+
# https://www.gnu.org/licenses/gpl-3.0.en.html
#
##################################################
## Paramètres d'importation ##
# La classe `Path` est importée pour simplifier la manipulation des fichiers.
# La classe `etree` et `objectify` du module `lxml` sont importées pour
# assurer la validité du XML.
# La classe `objectify` du module `lxml` n'est pas essentielle, mais a été
# choisie parce qu'elle simplifie l'accès aux sous-éléments d'un élément XML.
from pathlib import Path
from lxml import etree as et
from lxml import objectify
## Constantes ##
# En Python, contrairement à d'autres langages de programmation, il n'y a pas
# de «vraies» constantes. Par convention, on désigne des variables toute en
# majuscules comme «constantes» dont on évite de changer la valeur ailleurs
# dans le code.
# Contante pour créer l'attribut «xml:lang», qui contient l'espace de
# nom «xml» et qui ne peut être entrée directement dans le code à cause
# du deux-points.
XMLLANG = et.QName('http://www.w3.org/XML/1998/namespace', 'lang')
# J'ai découver la méthode `.QName()` grâce à un stagiaire. À l'origine,
# le code utilisait deux constantes, une pour l'espace de nom, et l'autre
# pour créer l'attribut au complet:
# XML = 'http://www.w3.org/XML/1998/namespace' # Espace de nom «xml»
# LANG = '{' + XML + '}' + 'lang' # Attribut «xml:lang»
# Valeurs à récuperer via une interface graphique dans une version ultérieure
CORPUSPATH = Path(Path().cwd()/'fichiers_texte')
SOURCE_LANG = 'fr'
## Fonctions ##
# Les fonctions permettent de modulariser le code, ce qui le rend plus facile à modifier, déboguer, et simplifie aussi l'ajout de nouvelles fonctinnalités.
def get_languages_and_files(corpuspath):
'''Récupérer les fichiers et leur langues.'''
# La méthode `.glob()` produit une liste de fichiers correspondant
# au modèle précisé.
textfiles = corpuspath.glob('*.txt')
# On utilise ici une technique appelée "dictionary comprehension" pour
# récupérer le code de langue en tant que clé du dictionnaire, et l'objet
# Path correspondant au fichier pour la valeur.
languages_and_files = {l.stem.rsplit('_')[1]:l for l in textfiles}
return languages_and_files
def read_content_by_language(languages_and_files):
'''Lire le contenu du fichier pour chacune des langues'''
# Créer une liste de language avec la langue source en tête.
# On produit d'abord une liste qui ne contient que les langues.
languages = list(languages_and_files.keys())
# On s'assure que le français arrive en premier en le retirant
# de la liste (méthode .pop()) et en le réinsérant en tête de
# liste (méthode .insert()).
languages.insert(0,languages.pop(languages.index(SOURCE_LANG)))
# Lire le contenu du fichier correspondant à chacune des langues.
contents = []
# Pour chacune des langues, on crée une liste qui contient toutes les
# lignes du fichier correspondant, et on met ces listes dans une liste
# avec tout le contenu.
for language in languages:
with open(languages_and_files[language], 'r', encoding='utf8') as c:
contents.append(c.read().splitlines())
# On renvoie une contruction "tuple" qui contient la liste des langues
# et la liste avec les listes de lignes correspondantes.
return (languages, contents)
def prepare_units(contents):
'''Jumeler les phrases correspondantes dans chacune des langues.'''
# Le '*' devant la variable contents est un "unpacking operator".
# Il sépare la liste de listes de la variable `contents` en listes
# individuelles.
# La fonction `.zip()` se charge en suite de jumeler les lignes de chacune des listes.
units = zip(*contents)
return units
def create_tmx_document():
'''Créer la structure de base de la TMX.'''
# Créer la racine.
tmx = objectify.Element('tmx', {'version': '1.4'})
# Dictionnaire pour définir les attributs requis de l'élément <header>.
# La valeur de l'attribut `srclang` est récupérée depuis la variable
# SOURCE_LANG.
tmxheader = {'creationtool':'Utilitaire stage 2023',
'creationtoolversion': '0.3', 'datatype': 'plaintext',
'segtype': 'sentence', 'adminlang': 'en-US',
'srclang': SOURCE_LANG, 'o-tmf': 'text files'}
# Créer les éléments `<header>` et `<body>`.
# On utiliser le dictionnaire ci-dessus pour ajouter tous les attributs
# requis à l'élément `<header`
header = objectify.SubElement(tmx, 'header', tmxheader)
body = objectify.SubElement(tmx, 'body')
return tmx
def make_tu(languages, unit):
'''Créer un élément <tu> contenant les <tuv> et <seg> pour toutes les langues d'un segment.'''
tu = objectify.Element('tu')
# On utilise la fonction `enumerate()` fournit un compte des itérations
# en plus de la valeur de l'itérateur. Ici, à chaque itération, on obtient
# le compte correspondant à la langue du `<tuv`, et le code de cette langue.
for l, language in enumerate(languages):
# Définir et ajouter l'élément <tuv>
tuv = objectify.SubElement(tu, 'tuv', {XMLLANG: language})
# Définir et ajouter l'élément <seg>
# On ne peut pas assigner directement l'attribut `@text` avec la
# classe `objectify`.
# Syntaxe pour insérer le texte trouvée ici:
# https://stackoverflow.com/a/2151303/8123921
# On utilise le compte pour récupérer le texte de la phrase
# correspondant à la langue du `<tuv>` dans le tuple de la
# variable `unit`.
tuv.seg = objectify.E.seg(unit[l])
return tu
def build_final_tree(tmx):
'''Créer l'arbre XML au complet pour la TMX.'''
# Créer l'arbre
# Insérer le document TMX dans un `ElementTree` confère certains avantages
# au niveau de l'écriture du fichier.
tmxtree = et.ElementTree(tmx)
# Ajouter le DTD au niveau du système.
tmxtree.docinfo.system_url = 'tmx14.dtd'
return tmxtree
def save_tmx(basename, tmxtree):
'''Définir le chemin et le nom complet du fichier TMX, et le sauvegarder.'''
# Définir le chemin et nom du fichier.
tmxext = '.tmx'
tmxname = basename + tmxext
tmxpath = Path(CORPUSPATH.parent/'fichier_tmx')
# Créer le chemin s'il n'existe pas.
if not tmxpath.exists():
tmxpath.mkdir(parents=True, exist_ok=True)
tmxfile = Path(tmxpath/tmxname)
# Écrire le fichier
objectify.deannotate(tmxtree, cleanup_namespaces=True)
tmxtree.write(tmxfile, encoding='utf-8',
xml_declaration=True, pretty_print=True)
## Programme principal ##
if __name__ == '__main__':
# Récupérer les langues et le contenu des fichiers
languages_and_files = get_languages_and_files(CORPUSPATH)
languages, contents = read_content_by_language(languages_and_files)
# Définir le nom de base du fichier
basename = languages_and_files[SOURCE_LANG].stem.strip('_'+SOURCE_LANG)
# Créer un document TMX
tmx = create_tmx_document()
# Jumeler les unités de traduction
units = prepare_units(contents)
# Créer les éléments `<tu>` avec les `<tuv>` contenant le texte des
# segments pour chacune des langues.
for unit in units:
tu = make_tu(languages, unit)
tmx.body.append(tu)
# Compléter l'arbre XML
tmxtree = build_final_tree(tmx)
# Sauvegarder le fichier TMX
save_tmx(basename, tmxtree)