Ajouter 'Exemple de script pour créer une TMX'

Philippe Tourigny 2023-06-01 18:34:40 +02:00
parent cca6ddfb3a
commit 936f55b636
1 changed files with 229 additions and 0 deletions

@ -0,0 +1,229 @@
# Créer une TMX à partir de fichiers texte  — script complet
Après la démarche présentée [ici](https://forge.chapril.org/ciri/stage_2023/wiki/Exemple-de-cr%C3%A9ation-de-script-en-Python), je vous présente le script au complet dans la [section code du site](https://forge.chapril.org/ciri/stage_2023/src/branch/kazephil/utilitaire_python).
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)`
```python
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)`
```python
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)`
```python
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:
```python
# 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.
```python
# 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.
```python
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)`
```python
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é.
```python
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:
```python
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.
```python
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.
```python
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:
```python
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»
```python
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:
```python
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.