From 75b6eec000a934785b89f9fc59867b5766400901 Mon Sep 17 00:00:00 2001 From: Kazephil Date: Thu, 1 Jun 2023 00:07:19 +0900 Subject: [PATCH] Ajouter les variantes du script --- .../textes_à_tmx_(lxml_etree).py | 148 ++++++++++ .../textes_à_tmx_(lxml_objectify).py | 272 ++++++++++++++++++ .../textes_à_tmx_(xml_etree_ElementTree).py | 145 ++++++++++ 3 files changed, 565 insertions(+) create mode 100644 utilitaire_python/textes_à_tmx_(lxml_etree).py create mode 100644 utilitaire_python/textes_à_tmx_(lxml_objectify).py create mode 100644 utilitaire_python/textes_à_tmx_(xml_etree_ElementTree).py diff --git a/utilitaire_python/textes_à_tmx_(lxml_etree).py b/utilitaire_python/textes_à_tmx_(lxml_etree).py new file mode 100644 index 0000000..96a7c67 --- /dev/null +++ b/utilitaire_python/textes_à_tmx_(lxml_etree).py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- + +from pathlib import Path + +from lxml import etree as et + +### Définir les constantes ### + +# Espace de nom XML et attribut @xml:lang +XMLLANG = et.QName('http://www.w3.org/XML/1998/namespace', 'lang') + +# Valeurs à saisir via une interface graphique dans la version finale. +CORPUSPATH = Path(Path().cwd()/'fichiers_texte') +SOURCE_LANG = 'fr' + +### Fonctions ### + +def get_languages_and_files(corpuspath): + '''Récupérer les fichiers et leur langues.''' + + textfiles = corpuspath.glob('*.txt') + 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. + languages = list(languages_and_files.keys()) + languages.insert(0,languages.pop(languages.index(SOURCE_LANG))) + + # Lire le contenu du fichier correspondant à chacune des langues. + contents = [] + + for language in languages: + with open(languages_and_files[language], 'r', encoding='utf8') as c: + contents.append(c.read().splitlines()) + + return (languages, contents) + + +def prepare_units(contents): + '''Jumeler les phrases correspondantes dans chacune des langues.''' + + units = zip(*contents) + + return units + + +def create_tmx_document(): + '''Créer la structure de base de la TMX.''' + + # Créer la racine. + tmx = et.Element('tmx', attrib={'version': '1.4'}) + + # Définir les attributs de l'élément
. + 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
et . + header = et.SubElement(tmx, 'header', attrib=tmxheader) + body = et.SubElement(tmx, 'body') + + return tmx + + +def make_tu(languages, unit): + '''Créer un élément contenant les et pour toutes les langues d'un segment.''' + + tu = et.Element('tu') + + for l, language in enumerate(languages): + # Définir et ajouter l'élément + tuv = et.SubElement(tu, 'tuv', {XMLLANG: language}) + + # Définir et ajouter l'élément + seg = et.SubElement(tuv, 'seg') + seg.text = unit[l] + + return tu + + +def build_final_tree(tmx): + '''Créer l'arbre XML au complet pour la TMX.''' + + # Créer l'arbre + 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 + '_lxml_etree' + 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 + 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 `` avec les `` contenant le texte des + # segments pour chacune des langues. + for unit in units: + tu = make_tu(languages, unit) + tmx[1].append(tu) + + # Compléter l'arbre XML + tmxtree = build_final_tree(tmx) + + # Sauvegarder le fichier TMX + save_tmx(basename, tmxtree) + + diff --git a/utilitaire_python/textes_à_tmx_(lxml_objectify).py b/utilitaire_python/textes_à_tmx_(lxml_objectify).py new file mode 100644 index 0000000..e7cc0c8 --- /dev/null +++ b/utilitaire_python/textes_à_tmx_(lxml_objectify).py @@ -0,0 +1,272 @@ +# -*- 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
. + # 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 `
` et ``. + + # On utiliser le dictionnaire ci-dessus pour ajouter tous les attributs + # requis à l'élément ` contenant les et 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 = objectify.SubElement(tu, 'tuv', {XMLLANG: language}) + + + # Définir et ajouter l'élément + # 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 `` 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 `` avec les `` 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) diff --git a/utilitaire_python/textes_à_tmx_(xml_etree_ElementTree).py b/utilitaire_python/textes_à_tmx_(xml_etree_ElementTree).py new file mode 100644 index 0000000..c584e73 --- /dev/null +++ b/utilitaire_python/textes_à_tmx_(xml_etree_ElementTree).py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- + +from pathlib import Path +import xml.etree.ElementTree as et + +# Définir les constantes + +# Valeurs à saisir via une interface graphique dans la version finale. +CORPUSPATH = Path(Path().cwd()/'fichiers_texte') +SOURCE_LANG = 'fr' + +def get_languages_and_files(corpuspath): + '''Récupérer les fichiers et leur langues.''' + + textfiles = corpuspath.glob('*.txt') + 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 + languages = list(languages_and_files.keys()) + languages.insert(0,languages.pop(languages.index(SOURCE_LANG))) + + # Lire le contenu du fichier correspondant à chacune des langues + contents = [] + + for language in languages: + with open(languages_and_files[language], 'r', encoding='utf8') as text: + contents.append(text.read().splitlines()) + + return (languages, contents) + + +def prepare_units(contents): + '''Jumeler les phrases correspondantes dans chacune des langues.''' + + units = zip(*contents) + + return units + + +def create_tmx_document(): + '''Créer la structure de base de la TMX.''' + + # Créer l'élément racine + tmx = et.Element('tmx', {'version': '1.4'}) + + # Définir les attributs de l'élément
+ 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
et + header = et.SubElement(tmx, 'header', tmxheader) + body = et.SubElement(tmx, 'body') + + return tmx + + +def make_tu(languages, unit): + '''Créer un élément contenant les et pour toutes les langues d'un segment.''' + + tu = et.Element('tu') + + for l, language in enumerate(languages): + # Définir et ajouter l'élément + tuv = et.SubElement(tu, 'tuv', {'xml:lang': language}) + + # Définir et ajouter l'élément + seg = et.SubElement(tuv, 'seg') + seg.text = unit[l] + + return tu + + +def build_final_tree(tmx): + '''Créer l'arbre XML au complet pour la TMX.''' + + # Créer l'arbre + tmxtree = et.ElementTree(tmx) + + 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 + '_xml_etree' + tmxext + + tmxpath = Path(CORPUSPATH.parent/'sortie_xml_etree') + + # 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 + + # Écrire la déclaration et le doctype dans le fichier + # Solution adaptée de la réponse suivante sur Stack Overflow : + # https://stackoverflow.com/a/8868551/8123921 + + xmldoc = '\n\n' + + # Écrire le fichier + with open(tmxfile, 'wb') as tree: + tree.write(xmldoc.encode('utf-8')) + et.indent(tmxtree) + tmxtree.write(tree, 'utf-8') + + +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 `` avec les `` contenant le texte des + # segments pour chacune des langues. + for unit in units: + tu = make_tu(languages, unit) + tmx[1].append(tu) + + # Compléter l'arbre XML + tmxtree = build_final_tree(tmx) + + # Sauvegarder le fichier TMX + save_tmx(basename, tmxtree)