Je me rends compte avec effroi que j'ai oublié d'écrire ce billet promis à un ami : c'est qu'il a rencontré des soucis de création de fichiers Excel dans une application Python devant faire du reporting. Problématique déjà rencontrée au sein de l'équipe embarqué, la solution a consisté en l'utilisation de PyUno, le binding Python-OpenOffice.org (et ça tombe bien, puisqu'on propose aussi des migrations sur la suite bureautique libre). La bonne nouvelle, c'est que l'on peut trouver des exemples et autres documentations très complètes (je ne vais donc pas m'amuser à tout reprendre) ; la mauvaise est qu'il faut savoir deux ou trois choses pour que ça marche dans la vie réelle (ou alors l'information importante est un peu perdue).

En premier lieu, il faut veiller à ce que votre environnement soit correct. D'une part, bien vérifier que la version de PyUno correspond à la bonne version de Python et de OOo que vous utilisez (sous Linux, les distributions assurent l'homogénéité, mais il en va autrement sous windows). Ensuite, mettre en place les bonnes variables d'environnement (évidemment, si PYTHONPATH est déjà défini, on concatène avec des ":" sous Linux, mais des ";" sous Windows qui ne fait jamais rien comme les autres -- il faut assumer son péché originel de "C:\" et les ambigüité consécutives). Sous Linux, cela se résume à :

export PYTHONPATH=:/usr/lib/ooo3/basis3.0/program/

Sous Windows en revanche, il faut définir (vous savez, en passant par le panneau de conf, "Système", onglet "Avancé", bouton "Variables d'environnement" -- mais puisqu'on vous dit que c'est simple et intuitif !) PYTHONPATH  à "C:\Program Files\OpenOffice.org 3\Basis\Program" et URE_BOOTSTRAP à "file:///C:/program%20File/OpenOffice.org%203/program/fundamental.ini" (bug documenté).

On peut à présent dans son code python faire un magnifique :

import uno

Et ça marche (rappel : on peut try-catcher des import sous Python, histoire de faire des messages d'erreur sexy au cas où l'environnement ne serait pas d'équerre, par exemple).

Arrive la subtilité de fonctionnement : PyUno s'adresse à un OOo en train de tourner via socket à travers une API, il n'utilise pas l'API de OOo directement ! Il faut donc avoir un serveur OOo qui tourne. Pour cela, il existe deux méthodes : avec ou sans l'option "-invisible" (notons aussi "-headless" qui fait la même chose mais sans avoir besoin de display -- explications par là --, et "-nologo" pour un lancement sans logo à l'affichage). Avec, nous obtenons un démon, c'est-à-dire que OOo est lancé sans fenêtre visible. Si on l'omet, une fenêtre "standard" (en apparence) s'ouvre. On pourrait penser qu'il suffit alors de lancer le serveur au démarrage de la machine, et de l'adresser ensuite. En fait c'est une erreur (attention, le passage qui suit ne convient pas aux âmes sensibles et aux enfants) : si l'on ouvre une fenêtre OOo alors que l'option "invisible" était invoqué, puis qu'on la ferme, le serveur disparaît aussi ; et de même, si l'on ferme la fenêtre-serveur (sans l'option "invisible"), le serveur aussi se ferme. C'est très, très stupide et contre-intuitif (de mémoire, j'ai eu le même problème avec "-headless"). Résultat des courses : mieux vaut lancer le serveur OOo à la volée, quand on en a besoin (au pire, si le serveur tourne déjà, c'est automatiquement détecté, aucun risque de doublon). Pour cela, rien ne vaut une fonction Python à appeler plus tard.

    def ooo_start(self):
        """ start OOo in server mode """
        if os.name == "nt":
                prog = 'start "" "C:\Program Files\OpenOffice.org 3\program\soffice.exe" -invisible -accept="socket,host=localhost,port=2002;urp;"'
        else:
                prog = 'soffice -invisible -accept="socket,host=localhost,port=2002;urp;"'
        if os.system(prog) == 0:
                self.wait_time(5)
                return True
        else:
                return False

Comme on peut le constater, cette procédure gère à la fois Windows et le reste du monde (en l'occurrence Linux, c'est bien connu). La partie optionnelle "accept=..." définit l'état de serveur par socket et l'hôte et le port. La commande Python "os.system" utilise "system" de la libC pour lancer l'application, et si cela réussit, on attend 5 secondes le temps que ça se lance (oui, gruik, je sais, mais ça marche et il n'y a pas trop le choix, puisque ça rend la main alors que ce n'est pas encore opérationnel). Il ne reste alors plus qu'à mettre en oeuvre :

        if not self.ooo_start():
                return
        local = uno.getComponentContext()
        resolver = local.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", local)
        context = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext")
        desktop = context.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", context)

Comme on peut le constater, la succession de commandes est ésotérique, mais la bonne nouvelle est qu'il ne s'agit pas forcément de comprendre ce que l'on fait. À ce niveau, on est connecté à notre serveur PyUno, et l'idée va être maintenant de créer un document que l'on va manipuler. Par exemple, le document /home/gblanc/Documents/example.xls :

       document = desktop.loadComponentFromURL("file:///home/gblanc/Documents/example.xls" ,"_blank", 0, ())

L'idée pourrait être, pour un rapport, de copier un fichier-template à l'aide de "shutil.copyfile" et d'ouvrir la copie. Je rappelle aussi les fonctionnalités portables de manipulation de fichiers que l'on trouve dans "os.path" (qu'il s'agit d'importer auparavant) : os.path.join, os.path.basename, os.path.realpath, etc. On a vu au dessus que l'URL du document à ouvrir a été retravaillée : en effet, "file://" a été ajouté ; quiconque a déjà navigué en local avec Firefox se dit qu'il a déjà vu ce genre de syntaxe. Et que sous Windows, ça va devenir plus complexe : il va en effet falloir ajouter un "/" devant, remplacer les espaces par des "%20" (comme sous le ouèbe avant l'invention des IRL), et changer les "\" en "/" (toujours pareil, assumer les conneries des développeurs sous-doués de Microsoft des années 80). Ce qui donne au final, en prenant notre hypothèse que "fineName" contient un chemin standard de fichier (tel que retourné par une boîte de dialogue Qt, au hasard) :

        if os.name == "nt":
                fileName = "/" + os.path.realpath(fileName).replace(" ", "%20").replace('\\', '/')
        else:
                fileName = os.path.realpath(fileName)
        document = desktop.loadComponentFromURL("file://" + fileName, "_blank", 0, ())

Notre variable "document" est donc à présent notre document de travail sur lequel on va pouvoir agir (remarque : notre fileName se terminant par ".xls", ".ods" ou assimilé, c'est Calc qui est lancé automatiquement par OOo). À ce niveau-là, je ne saurais que trop recommander d'entrer les commandes précédentes sur un iPython, et d'utiliser la tabulation pour constater à quel point l'API est aussi riche que labyrinthique, et qu'un outil d'autocomplétion et de documentation (souvent absente) peut nous être utile. L'API est calquée en réalité (m'a-t-on affirmé) sur celle des macros. D'un autre côté, ai-je une tête à faire des macros sous OOo ?? (en revanche, nous avons ça à Linagora, mais ce n'est pas moi qui m'en occuperait, n'est-ce pas ?). Bref, quelques fonctions utiles :

        document.Sheets.getByIndex(0).getCellByPosition(0, 0).setString("du texte")
        document.Sheets.getByIndex(0).getCellByPosition(0, 1).setValue(42)

Trois remarques : d'abord dans une feuille Excel (ou ods), se déplacer plutôt par "Sheets.getByIndex" plutôt qu'en appelant le raccourci du nom de l'onglet, histoire de pouvoir s'en sortir le jour où un gus renommera l'onglet du modèle. Ensuite, "getCellByPosition" se déplace selon le même système de coordonnées que sur une matrice. Enfin, il faut différencier "setString" qui ajoute du texte (qui peut être un chiffre) de "setValue" qui ajoute un nombre à la cellule : pour faire des calculs, par exemple, c'est "setValue" qu'il faut utiliser, sinon le résultat est juste catastrophique (et lorsqu'on n'est pas au courant, pour débuguer, on reste un peu bête).

Comme on peut s'en apercevoir lorsqu'on entre beaucoup de valeurs (plus d'une centaine, disons), c'est franchement super-lent comme traitements. Rassurez-vous : toute usine à gaz aura les mêmes problèmes, inutile de pester. On peut rendre en revanche la chose plus intéractive pour l'utilisateur, qui devant son écran et au lieu de se tourner les pouces (ou jouer au démineur) pourra voir les jolies valeurs être entrées auto-magiquement, ce qui est toujours ulta-sex et donne l'impression au client d'avoir payer une appli qui poutre. Pour cela, il existe dans notre document une sous-classe "controller" qui va permettre d'en prendre le contrôle (on l'aura deviné) :

        controller = document.getCurrentController()
        controller.setActiveSheet(document.Sheets.getByIndex(1))

Et paf que ça change l'onglet de la feuille Excel (ou Ods, etc, bref Calc) dans notre OOo Calc ! C'est juste beau.

Il s'agit à présent que toutes les données ont été entrées de sauver le document (sinon, il est perdu, ce serait bête) :

        document.store()

On peut aussi faire un :

       document.dispose()

Mais cela ferme la fenêtre et le serveur, ce qui peut être regrettable lorsqu'il s'agit de vérifier qu'un rapport a été bien rempli comme il convient (cf notre problématique initiale).

Voilà c'est tout (ouf !). Aucun plantage n'a jamais été signalé ni constaté, et aucune mouette n'a été blessé (ni mordu par un serpent). Vous avez économisé une licence de l'affreux concurrent Microsoft Office (qui dispose d'un binding Python crassouillet sous windows ; et d'une méthode par ActiveX sinon, il me semble, mais c'est gore, n'est-ce pas ?), vous avez un code portable, sans passer par cette horreur de Perl et son API Excel (qui a le mérite d'exister, mais le problème intrinsèque de Perl, c'est que ça existe tout court), et grâce à ce tutoriel, c'est d'une simplicité relative (sinon, contactez notre zélé commercial Cédric Ravalec, il se fera une joie de vous proposer de l'AT dans la demi-heure). C'est beau.