1 Exemple de script pour créer une TMX
Philippe Tourigny edited this page 2023-06-01 18:34:40 +02:00
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.

Créer une TMX à partir de fichiers texte  — script complet

Après la démarche présentée ici, je vous présente le script au complet dans la section code du site.

J'ai apporté une petite modification pour bénéficier de la trouvaille d'Anand avec la méthode .QName().

Variantes

En fait, je vous présente trois variantes du script, deux avec lxml et une avec xml.etree.ElementTree.

Avec lxml on a le choix de deux modules, lxml.etree et lmxl.objectify. Le premier suit de très près le module original de la bibliothèque standard, tandis que le deuxième offre une interface de programmation d'application (API en anglais) qui permet de traiter le XML comme une hiérachie ordinaire d'objets Python.

Le module xml.etree.ElementTree a l'avantage d'être dans la bibliothèque standard et ne pas nécessiter l'installation de bibliothèques tierces. Si vous avez toute liberté pour votre environnement Python, ça ne fait pas une grosse différence, mais si vous travailler dans une société, il est toujours possible qu'on vous permette d'installer Python, mais pas de bibliothèques tierces.

J'ai choisi l'API objectify du module lxml pour la version de base de mon script, en partie parce que je trouve l'idée de traiter le XML comme une hiérarchie d'objets Python attrayante, et en partie parce que je voulais explorer cette API un peu plus que je ne l'avais fait avant.

Par la suite, j'ai apporté les modifications mineures nécessaires à adapter le code aux modules lxml.etree et xml.etree.ElementTree.

La variante objectify contient le docstring pour le script ainsi que les commentaires avec le nom du script et l'information sur la version, l'auteur, et la license.

Elle contient aussi beaucoup de commentaires supplémentaires pour expliquer le code.

Les deux autres variantes ne contiennent que les commentaires que j'avais mis pour moi-même pendant la création du script. Je les ai aussi modifiées légèrement pour ajouter lxml_etree et xml_etree_ElementTree au nom du fichier TMX pour faciliter la comparaison entre les résultats de chacune des variantes.

Explications supplémentaires et comparatifs des variantes

Cette section présente quelques explications qui se prêtent mal à être ajoutées en tant que commentaire dans le code. Elle offre aussi une petite domparaison des différences entre les variantes.

1. Fonction get_languages_and_files(corpuspath)

languages_and_files = {l.stem.rsplit('_')[1]:l for l in textfiles}

Cette ligne de code fait tout le travail grâce à une «compréhension de dictionnaire». Cette technique combine une boucle et une transformation pour créer un dictionnaire à partir d'une liste.

Le l dans la compréhension est simplement une variable arbitraire pour représenter l'élément actuel de la liste textfiles. (J'ai simplement choisi la première lettre de languages_and_files.)

À chaque itération, l prend la valeur du prochain objet Path dans la liste textfiles. De là, la compréhension créé la clé du dictionnaire avec la formule l.stem.rsplit('_')[1], où:

  • l.stem renvoie une chaîne de caractère correspondant au nom du fichier, sans chemin ni extension.
  • .rsplit('_')[1] sépare la chaîne au caractère indiqué ('_') à partir de la fin (droite) de la chaîne, et crée une liste avec deux éléments, soit les parties du nom avant et après le '_', et choisi le deuxième ([1], puisque les listes son numérotées à partir de 0).

Exemple

Si, dans la première itération, on a le chemin '/utilitaire_python/fichiers_texte/étapes_fr.txt':

  • l = '/utilitaire_python/fichiers_texte/étapes_fr.txt'
  • l.stem= 'étapes_fr'
  • l.stem.rsplit('_') = ['étapes', 'fr']
  • l.stem.rsplit('_')[1] = 'fr'

La première entrée du dictionnaire devient donc: {'fr': '/utilitaire_python/fichiers_texte/étapes_fr.txt'}

2. Fonction read_content_by_language(languages_and_files)

languages.insert(0,languages.pop(languages.index(SOURCE_LANG)))

Cette ligne met la langue source en tête de liste. On commence par la parenthèse intérieure, et on va vers l'extérieur:

  • languages.index(SOURCE_LANG) donne la position de SOURCE_LANG ('fr' en l'occurrence) dans la liste languages
  • languages.pop(…) retire l'élément à la position donnée de la liste languages.
  • languages.insert(0, …) insère l'élément à la position 0, soit en tête de liste.

Ainsi, le premier <tuv>de chacun des <tu> sera celui de la langue source quand on va faire une itération sur la liste de langues.

Fonction prepare_units(contents)

units = zip(*contents)

Pour visualiser le dépaquetage produit par l'opérateur *,il faut d'abord se rappeler que contents est une liste de listes. Avec mes fichiers en français, anglais, et japonais, ça donne ceci:

contents = [[ļiste des lignes fr], [liste des lignes en], [liste des lignes ja]]

L'opérateur * sort les listes de langues de la liste contents pour passer trois listes individuelles à la fonction zip():

Au sein de la fonction zip, *contents devient trois arguments séparés:[ļiste des lignes fr], [liste des lignes en], et [liste des lignes ja]

Enfin, la fonction zip() jumelle les éléments de chaque liste:

('élément 0 fr', 'élément 0 en', 'élément 0 ja )
('élément 1 fr, 'élément 1 en', 'élément 1 ja)
.
.
.
('élément n fr, 'élément n en', 'élément n ja)

Si les listes n'ont pas la même longueur, la fonction jumelle le nombre d'éléments dans la plus courte des listes et laisse tomber les autres éléments.

Fonction create_tmx_document()

La seule différence c'est qu'on utilise objectify.Element() ou objectify.SubElement() avec lxml.objectify, et et.Element() ou et.SubElement() avec lxml.etree et tmx.etree.ElementTree (où 'et' est le nom sous lequel on a importé etree.)

3. Fonction make_tu(languages, units)

Avec lxml (autant objectify que etree), on utilise la méthode .QName() pour enregistrer l'attribut xml:lang, tandis qu'avec xml.etree.ElementTree on peut le saisir directement:

# lxml.objectify:
tuv = objectify.SubElement(tu, 'tuv', {XMLLANG: language})

# lxml.etree:
tuv = et.SubElement(tu, 'tuv', {XMLLANG: language})

# xml.etree.ElementTree
tuv = et.SubElement(tu, 'tuv', {'xml:lang': language})

On pourrait bien sûr aussi utiliser la variable XMLLANG avec xml.etree.ElementTree.

La création du segment devient plus intéressante au niveau des différences.

# lxml.objectify:
tuv.seg = objectify.E.seg(unit[l])

# lxml.etree et xml.etree.ElementTree:
seg = et.SubElement(tuv, 'seg')
seg.text = unit[l]

Avec objectify, on utilise E, une «usine» («E factory» en anglais) de construction d'élément XML pour créer un élément seg avec la valeur de la ligne de texte pour la langue en cours d'itération, et on met cet élément dans un sous-élément du <tuv> avec tuv.seg, parce que objectify ne permet pas d'assigner une valeur à l'attribut text d'un élément.

Avec lxml.etree et xml.etree.ElementTree, on peut assigner une valeur à l'attribut text, mais on ne peut pas créer un séquence d'éléments comme le tuv.seg utilisé avec objectify. On crée donc le sous-élément de la façon ordinaire, puis on modifie l'attribut text par la suite.

Je n'ai pas utilisé la méthode .strip() sur le texte du segment principalement parce que j'avais l'idée de mettre une option du genre «Enlever les espaces en début ou fin de ligne» dans la version avec interface graphique. Ce n'est probablement pas très fréquent, mais on pourrait envisager des cas où l'on veut conserver de tels espaces. (À bien y penser, j'aurais peut-être dû utiliser .strip() et plutôt songer à «Conserver les espaces en début ou fin de ligne» comme option.)

4. Fonction build_final_tree(tmx)

On peut créer un arbre ElementTree créé avec lxml.etree. (Le module objectify n'offre pas une classe ElementTree, mais on peut mettre un document XML créé avec objectify dans un arbre etree.ElementTree.)

Cet arbre contient une classe docinfo, qui contient un attribut doctype, mais ce dernier est limité à lire le doctype existant et ne peut pas être modifié.

Par contre, j'ai fait une nouvelle découverte: cette classe contient aussi les attributs public_id et system_url, auxquels on peut assigner une valeur pour saisir un doctype dans l'arbre XML.

Avec lxml.etree et lxml.objectify, j'ai donc pu utiliser le code suivant pour définir le doctype.

tmxtree.docinfo.system_url = 'tmx14.dtd'

Avec xml.etree.ElementTree, il n'est pas possible de créer un doctype dans l'arbre XML.

4. Fonction save_tmx(basename, tmxtree)

if not tmxpath.exists():
    tmxpath.mkdir(parents=True, exist_ok=True)

Ici, tmxpath et un objet Path(), et on utilise la méthode .exists() de cet object pour s'assurer que le chemin défini existe. S'il n'existe pas, on utilise la méthode .mkdir() du même objet pour créer le chemin. Dans la méthode .mkdir(), parents=True permet de créer aussi les chemins intermédiaires s'ils n'existent pas non plus, et exist_ok=True évite les erreurs si le chemin existe déjà. Cet attribut a la valeur False par défaut, alors il est important de le préciser dans un script qui va réutiliser un même chemin chaque fois qu'il est exécuté.

objectify.deannotate(tmxtree, cleanup_namespaces=True)
tmxtree.write(tmxfile, encoding='utf-8',
                xml_declaration=True, pretty_print=True)

Une particularité du module objectify est de produire un fichier XML avec des espaces de nom explicites si on ne précise pas le contraire, d'ou la première ligne avec la méthode deannotate. Cette ligne n'est pas nécessaire dans la variante lxml.etree.

Avec un arbre ElementTree créé par -lxml, on peut utiliser l'attribut pretty_print=True dans la méthode .write() pour obtenir un fichier bien indenté.

Par contre, avec xml.etree.ElementTree, on doit travailler un peu plus fort pour obtenir la déclaration XML et le doctype puisqu'on ne pouvait pas mettre le doctype directement dans le l'arbre XML de la TMX:

xmldoc = '<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE tmx SYSTEM "tmx14.dtd">\n'

with open(tmxfile, 'wb') as tree:
    tree.write(xmldoc.encode('utf-8'))
    et.indent(tmxtree)
    tmxtree.write(tree, 'utf-8')

On met d'abord (étape optionnelle) la déclaration et le doctype dans une variable (avec un \n pour assurer un saut de ligne) pour me simplifier la vie au moment de l'écriture du fichier.

Ensuite, on écrit manuellement la déclaration et le doctype avec la méthode .write() normale d'un fichier (tree.write(xmldoc.encode('utf-8'))), puis on applique l'indentation et on écrit le reste de la TMX avec la méthode .write() de etree.ElementTree.

Attention de ne pas inclure l'attribut xml_declaration=True dans ce cas. Vous vous retrouveriez avec une deuxième déclaration après le doctype, et le fichier XML serait invalide.

5. Programme principal

Presque tout le code ne fait qu'appeler une des fonctions et mettre le résultat dans une variable qui est ensuite passée à une autre fonction.

Les quelques exceptions sont décrites ci-dessous.

basename = languages_and_files[SOURCE_LANG].stem.strip('_'+SOURCE_LANG)

Cette ligne sert tout simplement à récupérer le nom sans la partie _LL (donc 'étapes' dans 'étapes_fr') pour le passer à la fonction save_tmx afin d'obtenir un fichier de sortie nommé 'étapes.tmx'.

On arrive ensuite au cœur du programme.

for unit in units:
    tu = make_tu(languages, unit)
    tmx.body.append(tu)    

Ces lignes passent chacune des unités de traductions jumelées à la fonction make_tu, et ajoute le <tu> dans l'élément <body> du document TMX.

Avec lxml.objectify, on peut simplement désigner tmx.body comme élément, et appliquer la méthode .append() pour ajouter chacun des nouveaux <tu>.

Avec lxml.etree et xml.etree.ElementTree, les éléments sont traités comme des listes, et on ne peut pas accéder à un sous-élément avec la notation par attribut comme le tmx.body de lxml.objectify.

On utilise donc:

tmx[1].append(tu)

Vu comme une liste, l'élément tmx contient ['header', 'body'], et comme les listes sont numérotées à partir de 0, l'élément <body> est à la position 1 dans la liste, donc tmx[1].

Je n'en ai pas fait une fonction parce que je voyais ces lignes de code comme la partie centrale du programme, mais une fonction aurait aussi été possible.

Dans la section «Fonctions»

def build_tmx_body(tmx, languages, units):
    '''Créer les éléments `<tu>` et les ajouter à la TMX.'''

    for unit in units:
        tu = make_tu(languages, unit)
        tmx.body.append(tu)
    
    return tmx

Puis dans la section principale du programme, on remplacerait les lignes actuelles par un simple appel de fonction:

tmx = build_tmx_body(tmx, languages, units)

Et voilà! Il y a sûrement des choses à améliorer, mais j'espère que ça vous sera quand même utile.