Une bonne partie du domaine d’application des langages de script concerne le traitement du texte. La manipulation du texte est une opération très courante car on a constamment besoin de reformater ou de transformer de différentes façons des morceaux de textes. Cependant, chaque besoin est légèrement différent de tous les autres et il est donc difficile d’écrire un code réutilisable qui fonctionne avec les différents formats de fichiers, quelles que soient leurs similitudes.
Un texte n’est, finalement, qu’une suite de caractères et c’est ce qui le distingue des données binaires, qui ne sont que des suites d’octets.
Malheureusement, les données entrent toutes dans nos applications sous la forme de suites d’octets. Il n’existe pas de fonction nous permettant de savoir si une suite particulière d’octets représente du texte, bien que nous puissions mettre au point des heuristiques nous indiquant si ces données peuvent être traitées comme du texte (pas forcément correctement) en toute sécurité. La recette 1.11 présente l’une de ces heuristiques.
En Python, les chaînes sont des suites non modifiables d’octets ou de caractères que nous créons et traitons la plupart du temps comme des séquences de caractères, mais la majorité des traitements sur les chaînes peuvent également s’appliquer à des suites d’octets. Les chaînes Unicode sont des suites non modifiables de caractères Unicode : les conversions entre chaînes Unicode et chaînes classiques utilisent des objets codecs (codeurs-décodeurs) qui savent comment représenter les suites de caractères par des suites d’octets (c’est ce que l’on appelle également les encodages et les jeux de caractères ). Les chaînes Unicode ne peuvent pas être considérées comme de simples suites d’octets. Les recettes 1.20, 1.21 et 1.22 illustrent les bases de l’utilisation d’Unicode en Python.
Notre application peut savoir qu’elle est en train d’examiner du texte en utilisant le contexte, c’est-à-dire ses entrées externes. Nous savons qu’un fichier contient du texte parce qu’il a un nom connu et un format bien défini (typique du monde « Unix ») ou parce qu’il a une extension de nom indiquant le format de son contenu (typique du monde « Windows »). Mais nous avons dû utiliser le mot format pour que ce paragraphe ait un sens. Le texte n’était-il pas censé être simple ?
En fait, il n’existe pas de texte « pur » et, si c’était le cas, on ne s’en occuperait sûrement pas (sauf, éventuellement, dans les applications de linguistique computationnelle, où le texte pur peut, bien sûr, être étudié pour lui-même). Ce que veulent traiter nos applications, ce sont les informations qui sont contenues dans le texte : données de configuration, commandes pour contrôler ou définir des processus, documents exprimés en langue naturelle ou même données tabulées. Les textes contenant des données de configuration ou une suite de commandes respectent généralement une syntaxe assez stricte qui peut être vérifiée avant d’utiliser les informations qu’ils contiennent. Lorsque tout ne se passe pas comme l’on s’y attend, le plus simple est généralement d’informer l’utilisateur qu’il y a une erreur dans le texte en entrée.
Les documents destinés à être lus par les utilisateurs ont tendance à être simples, mais ils varient beaucoup dans les détails. Comme ils sont généralement écrits en langage naturel, il est difficile de vérifier leur syntaxe et leur grammaire. Ils peuvent utiliser des jeux de caractères ou des encodages différents et il peut être difficile, voire impossible, de retrouver le jeu de caractères ou l’encodage utilisés pour créer un texte si cette information n’est pas fournie en plus du texte lui-même. Cependant, il est important de pouvoir représenter correctement les documents en langage naturel, même si leur structure est souvent peu explicite et nécessite au moins une certaine compréhension de la langue dans laquelle ils ont été écrits. Les caractères forment des mots, qui forment des phrases, qui forment des paragraphes et il peut y avoir des structures encore plus grandes. Les paragraphes, notamment, peuvent être particulièrement difficiles à localiser, sauf si vous connaissez les conventions typographiques qui ont été utilisées lors de l’écriture du document : est-ce que chaque ligne forme un paragraphe ou est-ce qu’un paragraphe peut comporter plusieurs lignes ? Dans ce dernier cas, comment savoir quelles lignes sont regroupées pour former un paragraphe ? Les paragraphes peuvent être séparés par des lignes blanches, comporter des alinéas ou toute autre marque spéciale. Dans la recette 11.10 vous verrez comment lire un fichier texte comme une suite de paragraphes séparés par des lignes blanches.
Les données tabulées posent de nombreux problèmes du même ordre que ceux rencontrés avec les textes en langage naturel, mais elles ajoutent une deuxième dimension au format d’entrée : le texte n’est plus une suite linéaire de caractères mais plutôt une matrice à partir de laquelle on doit identifier et organiser les différents blocs de texte.
Comme avec n’importe quel autre format de données, nous avons besoin d’effectuer différents traitements sur le texte et à différents moments. Cependant, on rencontre toujours ces trois opérations de base :
L’analyse lexicale peut être effectuée de nombreuses façons différentes et des analyseurs ad hoc permettent de gérer correctement de nombreux formats très spécifiques. Les analyseurs des en-têtes de courriers électroniques, tels qu’ils sont définis par la RFC 2822 (voir le module rfc822 dans la bibliothèque standard de Python) et le module ConfigParser, qui permet de traiter les fichiers de configuration, en sont de bons exemples. Le module netrc est un autre exemple d’analyseur lexical : il repose sur le module shlex qui offre un découpage en tokens assez classique pour les langages de base. Ce module permet de créer aisément des fichiers de configuration lisibles et autorise les utilisateurs à entrer des commandes en réponse à une invite interactive. Ces types d’analyseurs ad hoc sont nombreux dans la bibliothèque standard de Python et vous trouverez des recettes qui les utilisent dans les chapitres 2 et 9. Python dispose également d’outils d’analyse lexicale plus formels.
La transformation d’un texte d’un format dans un autre est plus intéressante lorsqu’elle est considérée comme un traitement du texte. Dans ce chapitre, nous étudierons plusieurs approches de transformations pouvant s’appliquer dans différentes situations. Parfois, nous travaillerons sur du texte stocké dans des fichiers externes, d’autres fois nous utiliserons simplement des chaînes en mémoire.
Pour produire du texte à partir de structures de données spécifiques à une application, le plus simple consiste à utiliser l’instruction print de Python ou la méthode write d’un objet fichier ou assimilé :
Ces deux lignes produisent une sortie dans le fichier un_fichier. Cependant, ce genre de code n’est généralement pas considéré comme du traitement de texte puisqu’on ne traite pas de texte en entrée. Vous trouverez, bien sûr, des exemples d’utilisation de print et write tout au long de ce livre.
Lorsqu’un texte n’est pas trop long, il est plus simple de travailler sur une représentation de celui-ci sous forme d’une chaîne en mémoire. Les opérations de recherche peuvent alors s’effectuer très facilement et très rapidement sur plusieurs lignes sans qu’il y ait besoin de s’inquiéter d’un dépassement des limites du tampon. En outre, si l’on peut mettre un texte en mémoire sous la forme d’une simple chaîne, il devient également très facile de tirer parti des opérations prédéfinies sur les chaînes, disponibles via les méthodes de l’objet chaîne.
Les transformations utilisant des fichiers méritent un traitement spécial car elles peuvent dépendre de façon non négligeable des performances des entrées/sorties et du volume de données qui doivent être stockées en mémoire. Lorsque l’on travaille sur des données stockées sur disque, on souhaite souvent éviter de charger les fichiers entiers en mémoire : mettre en mémoire un fichier de 80 Mo ne doit pas devenir une habitude ! Si notre application n’a besoin que d’une partie des données à la fois, travailler sur des segments plus petits peut provoquer un gain de performances non négligeable, tout simplement parce que nous aurons donné assez d’espace à notre programme pour s’exécuter. Si nous prenons soin de la gestion du tampon, nous pouvons encore améliorer les performances en utilisant un petit nombre d’opérations de lecture et d’écriture de blocs disques relativement gros et en travaillant donc à chaque fois sur de gros morceaux de données. Les recettes liées aux fichiers se trouvent au chapitre 2.
Le réseau est également une autre source intéressante de texte, récupéré le plus souvent à l’aide d’une socket. Bien que l’on puisse toujours considérer une socket comme un fichier (en utilisant la méthode makefile de l’objet socket), les données ainsi obtenues peuvent arriver par morceaux et on peut devoir attendre que d’autres données arrivent. En d’autres termes, les données textuelles peuvent ne pas contenir toutes les données tant que la fin du flux de données n’a pas été atteinte : un objet fichier créé par makefile peut donc ne pas être très approprié pour passer ces données au code de traitement du texte. Lorsque l’on travaille sur du texte provenant d’une connexion réseau, on a donc souvent besoin de lire toutes les données de la connexion avant de les transmettre à d’autres traitements : si le volume de ces données est important, on peut les sauvegarder dans un fichier à mesure qu’elles arrivent, puis utiliser ensuite ce fichier pour les opérations de traitement. Si le texte doit être traité avant que toutes les données ne soient disponibles, il faut utiliser des solutions plus élaborées. Les analyseurs des modules htmllib et HTMLParser de la bibliothèque standard, par exemple, sont tout à fait adaptés à ces types de situations.
Pour traiter le texte, Python met à notre disposition les chaînes — des suites non modifiables de caractères. Il en existe en fait deux sortes : les chaînes normales, qui contiennent des caractères sur 8 bits (ASCII) et les chaînes Unicode, qui contiennent des caractères Unicode. Nous ne nous occuperons pas beaucoup des chaînes Unicode ici : leurs fonctionnalités sont similaires à celles des chaînes normales, sauf que chaque caractère occupe 2 (ou 4) octets et que le nombre de caractères différents se chiffre donc en dizaines de milliers (voire en milliards) au lieu des 256 caractères différents du code ASCII des chaînes normales. Les chaînes Unicode sont intéressantes si vous devez gérer du texte dans plusieurs alphabets différents et que vous avez besoin, notamment, d’utiliser des idéogrammes asiatiques. Pour traiter l’anglais et un certain nombre de langues non asiatiques, les chaînes normales suffisent. Tous les alphabets de l’Europe occidentale, par exemple, peuvent être encodés dans des chaînes normales, généralement en utilisant l’encodage international ISO-8859-1 (ou ISO-8859-15, si vous avez également besoin du symbole monétaire Euro).
En Python, une chaîne littérale (bizarrement souvent appelée littéral chaîne) s’exprime de la façon suivante :
Les valeurs des chaînes sont placées entre apostrophes simples ou doubles. Ces deux types d’apostrophes fonctionnent de la même façon, mais disposer des deux permet d’inclure l’une dans l’autre sans devoir la protéger par un caractère anti-slash :
Pour qu’un littéral chaîne s’étende sur plusieurs lignes, vous pouvez placer un anti-slash à la fin de la ligne pour indiquer que la ligne suivante est une ligne de continuation :
Pour que la chaîne s’affiche sur deux lignes, vous devez utiliser des caractères de nouvelle ligne :
Une autre possibilité consiste à entourer la chaîne par une paire d’apostrophes (simples ou doubles) triplées :
Grâce aux apostrophes triplées, vous n’avez plus besoin d’utiliser de caractère de continuation. En outre, les coupures de lignes dans le littéral chaîne seront préservées sous forme de caractères nouvelle ligne dans l’objet chaîne résultant. Vous pouvez également créer un littéral chaîne « brut » en le précédant d’un r ou d’un R :
Dans une chaîne brute, les séquences d’échappement ne sont pas interprétées mais traitées littéralement. Enfin, vous pouvez précéder un littéral chaîne d’un u ou d’un U pour en faire une chaîne Unicode :
Les chaînes ne sont pas modifiables1 : quelle que soit l’opération appliquée à une chaîne, vous obtiendrez toujours un nouvel objet chaîne au lieu de modifier la chaîne existante. Une chaîne est une séquence de caractères, ce qui signifie que vous pouvez accéder à un caractère précis par son indice :
Pour accéder à une portion d’une chaîne, vous pouvez utiliser une tranche :
Les tranches peuvent être améliorées en leur ajoutant un troisième paramètre, appelé pas de la tranche :
Vous pouvez parcourir une chaîne caractère par caractère :
Cette instruction lie successivement c à chaque caractère de ma_chaîne. Vous pouvez également former une autre séquence à partir d’une chaîne :