9
votes

Lire un fichier Unicode dans Python qui déclare son codage de la même manière que la source Python

Je souhaite écrire un programme Python qui lit les fichiers contenant du texte Unicode. Ces fichiers sont normalement codés avec UTF-8, mais ne peuvent pas être; S'ils ne le sont pas, le codage alternatif sera explicitement déclaré au début du fichier. Plus précisément, il sera déclaré en utilisant exactement les mêmes règles que Python lui-même utilisées pour permettre à Python Source Code d'avoir un codage explicitement déclaré (comme dans Pep 0263, voir https://www.python.org/dev/peps/pep-0263/ pour plus de détails). Juste pour être clair, les fichiers en cours de traitement ne sont pas réellement de la source python, mais ils déclarent leurs codages (lorsqu'ils ne sont pas dans UTF-8) en utilisant les mêmes règles.

Si on connaît le codage d'un fichier avant que l'on ne l'ouvre avant l'ouverture Python offre un moyen très facile de lire le fichier avec décodage automatique: la commande codecs.open ; Par exemple, on pourrait faire: xxx

et chaque ligne nous entrons dans la boucle sera une chaîne unicode. Existe-t-il une bibliothèque Python qui fait une chose similaire, mais choisissant le codage selon les règles ci-dessus (qui sont les règles de Python 3.0, je pense)? (par exemple, Python expose le «fichier de lecture avec codage auto-déclaré» qu'il utilise pour lire la source à la langue?) Sinon, quel est le moyen le plus simple d'atteindre l'effet souhaité?

Une pensée est d'ouvrir Le fichier à l'aide de l'habituel Ouvrir , lisez les deux premières lignes, interprétez-les sous UTF-8, recherchez une déclaration de codage à l'aide du REGEXP dans le PEP, et si l'on trouve un démarrage décodant toutes les lignes suivantes à l'aide de le codage déclaré. Pour que cela soit sûr de travailler, nous devons savoir que pour tous les codages que Python permet à Python Source, le python habituel readline divisera correctement le fichier en lignes - c'est-à-dire que nous devons savoir Pour que tous les codages Python permettent à Python Source, la chaîne d'octets '\ N' signifie toujours vraiment nouvelle ligne et ne fait pas partie de certaines séquences multi-octets codant pour un autre caractère. (En fait, j'ai aussi besoin de vous inquiéter de "\ r \ n" aussi.) Est-ce que quelqu'un sait-il si cela est vrai? Les documents n'étaient pas très précis.

Une autre pensée consiste à regarder dans les sources Python. Est-ce que quelqu'un sait où dans la source Python Le traitement du codage de code source est effectué?


1 commentaires

Il semble que Python 3.4 apporte une réponse à votre question - voir ma réponse.


5 Réponses :


2
votes

à partir de ladite PEP (0268) :

Le combo Tokenizer / compilateur de Python sera Besoin d'être mis à jour pour fonctionner comme suit:

  1. lisez le fichier

  2. décode dans unicode en supposant un codage par fichier fixe

  3. Convertissez-le en une chaîne d'octets UTF-8

  4. TOKENISEZ le contenu UTF-8

  5. Compilez-le, créant des objets Unicode à partir des données Unicode données et créer des objets de chaîne à partir des données littérales Unicode en réenregistrant d'abord les données UTF-8 dans des données de chaîne de 8 bits Utilisation du fichier de fichier donné codant

    En effet, si vous vérifiez parser / tokenizer.c dans la source Python, vous trouverez des fonctions get_coding_spec et check_coding_spec qui sont responsables de la recherche de ces informations sur une ligne étant examinée dans décodage_fgets .

    Il ne semble pas que cette capacité soit exposée n'importe où en tant qu'API Python (au moins ces fonctions spécifiques ne soit pas py préfixée -, de sorte que vos options sont donc une bibliothèque tierce et / / ou réaffirmer ces fonctions comme une extension. Je ne connais personnellement aucune bibliothèque tierce partie - je ne peux pas voir cette fonctionnalité dans la bibliothèque standard non plus.


1 commentaires

Merci pour le lien avec les sources. Je suis d'accord avec vous que cela ressemble à la fonctionnalité n'est exposé nulle part où j'avais peur. Je pense que je prévois d'analyser l'algorithme dans les sources Python, puis de le recoder en python ...



7
votes

Vous devriez être capable de rouler votre propre décodeur en Python. Si vous ne supportez que les codages 8 bits qui sont des supersets d'ASCII, le code ci-dessous doit fonctionner comme.

Si vous avez besoin de support 2-byte Encodings comme UTF-16 Vous auriez besoin d'augmenter le modèle pour correspondre à \ x00c \ x00o .. ou l'inverse, en fonction de Le point d'ordre d'octet . Tout d'abord, générez quelques fichiers de test qui annoncent leur codage: xxx

puis écrivez le décodeur: xxx

sortie: < / p> xxx


1 commentaires

Merci! C'est bien et simple et fonctionnera plutôt bien dans des exemples pratiques. C'est probablement la meilleure solution dans la plupart des cas! Pour mes propres exigences, la fidélité exacte à ce que Python est important, alors je finirai donc de faire quelque chose de plus élaboré, comme décrit dans ma réponse ci-dessous, que je comprends au cas où quelqu'un d'autre qui se soucie de la fidélité exacte trouve cette question sur Stackoverflow .



3
votes

J'ai examiné les sources de tokenizer.c (grâce à @nineFingers pour le suggérer dans une autre réponse et donnant un lien vers le navigateur source). Il semble que l'algorithme exact utilisé par Python soit (équivalent à) ce qui suit. Dans divers endroits, je vais décrire l'algorithme en tant qu' octet de lecture par octet - - évidemment, on veut faire quelque chose de tamponné dans la pratique, mais il est plus facile de décrire de cette façon. La partie initiale du fichier est traitée comme suit:

  1. Lors de l'ouverture d'un fichier, essayez de reconnaître le nom de l'UTF-8 au début du fichier. Si vous le voyez, mangez-le et faites une note du fait que vous avez vu. ne pas reconnaître le point de commande UTF-16 octets.
  2. Lire "une ligne" de texte du fichier. "Une ligne" est défini comme suit: Vous continuez à lire octets jusqu'à ce que vous voyiez l'une des chaînes '\ n', '\ r' ou '\ r \ n' (essayant de correspondre aussi longtemps que possible. signifie que si vous voyez "\ r", vous devez être lu de manière spéculativement le caractère suivant, et si ce n'est pas un "\ n", le remettez-le). Le terminateur est inclus dans la ligne, de même que la pratique de Python habituelle.
  3. Décode de cette chaîne à l'aide du codec UTF-8. Sauf si vous avez vu le nom de l'UTF-8, génère un message d'erreur si vous voyez des caractères non-ASCII (c'est-à-dire des caractères ci-dessus 127). (Python 3.0 ne génère pas, bien sûr, générer une erreur ici.) Passez cette ligne décodée sur l'utilisateur pour le traitement.
  4. tentative d'interprétation de cette ligne en tant que commentaire contenant une déclaration de codage, à l'aide de la REGEXP dans Pep 0263 . Si vous trouvez une déclaration de codage, passez aux instructions ci-dessous pour «J'ai trouvé une déclaration de codage».
  5. OK, vous n'avez donc pas trouvé de déclaration de codage. Lisez une autre ligne de l'entrée, en utilisant les mêmes règles qu'à l'étape 2 ci-dessus.
  6. décodez, en utilisant les mêmes règles que l'étape 3 et transmettez-la à l'utilisateur pour le traitement.
  7. Tentative à nouveau pour interpréter cette ligne en tant que commentaire de déclaration de codage, comme à l'étape 4. Si vous en trouvez une, passez aux instructions ci-dessous pour "J'ai trouvé une déclaration de codage".
  8. OK. Nous avons maintenant vérifié les deux premières lignes. Selon PEP 0263, s'il y avait une déclaration de codage, cela aurait été sur les deux premières lignes. Nous savons donc maintenant que nous n'allons pas en voir un. Nous lisons maintenant le reste du fichier en utilisant les mêmes instructions de lecture que nous lisions les deux premières lignes: nous lisons les lignes à l'aide des règles à l'étape 2, décodez à l'aide des règles à l'étape 3 (faire une erreur si nous voyons non Octets ASCII à moins que nous ayons vu un bom).

    Maintenant les règles de quoi faire quand ' J'ai trouvé une déclaration de codage ':

    1. Si nous avons déjà vu un nom d'UTF-8, vérifiez que la déclaration de codage indique «UTF-8» sous une forme. Jeter une erreur sinon. ('' utf-8 'sous quelque forme forme "signifie tout ce qui, après la conversion en minuscule et la conversion de sous-traits en traits d'union, est soit la chaîne littérale ' utf-8 ', soit quelque chose en commençant par 'utf-8 -' .)
    2. Lisez le reste du fichier à l'aide du décodeur associé au codage donné dans le module Python codecs . En particulier, la division du reste des octets dans le fichier dans les lignes est le travail du nouveau codage.
    3. Une dernière ride: Type Universel Newline Type. Les règles ici sont les suivantes. Si le codage est quelque chose, sauf «utf-8» sous une forme ou «latin-1» sous une forme, ne faites pas de choses universelles-Newline du tout; Il suffit de passer des lignes exactement comme ils viennent du décodeur dans le module codecs . D'autre part, si l'encodage est "UTF-8" sous une forme ou "latin-1" sous une forme, transformez les lignes finissant "\ r" ou "\ r \ n 'en lignes finissant" \ n ". ('' utf-8 '"utf-8" sous une forme "signifie la même chose qu'auparavant." "Latin-1" sous une forme "signifie quelque chose qui, après la conversion en minuscules et la conversion de soulignement en traits d'union, est l'une des chaînes littérales < Code> 'Latin-1' , 'iso-latin-1' ou 'iso-8859-1' ou n'importe quelle chaîne commençant par l'une des < Code> 'Latin-1 -' , 'iso-latin-1 -' ou 'iso-8859-1 -' . .

      Pour ce que je fais, la fidélité au comportement de Python est importante. Mon plan est de faire valoir une mise en œuvre de l'algorithme ci-dessus en Python et d'utiliser ceci. Merci pour tous ceux qui ont répondu!


8 commentaires

Cela signifie que si vous voyez "\ r", vous devez être lu de manière spéculativement le caractère suivant, et si ce n'est pas un "\ n", placez-le pourquoi ne traitez-vous pas \ r comme un caractère EOL valide? Décode de cette chaîne à l'aide du codec UTF-8. Sauf si vous avez vu le nom de l'UTF-8, génère un message d'erreur si vous voyez des caractères non-ASCII (...) ne devriez-vous pas être inversé?


@PIODRDOBROGOST: Vous traitez \ r en tant que caractère EOL valide. Le point est que si le caractère suivant immédiatement le \ r est un \ n , alors que \ n doit être "mangé" dans le cadre du ligne se terminant par le \ r et pas traité comme faisant partie de la ligne suivante. Si le personnage suivant immédiatement le \ r est autre chose que \ n , il doit alors être laissé dans le flux pour être le premier caractère de la ligne suivante. C'est pourquoi vous avez besoin de la logique «remise» décrite.


@PIOLDOBROGROGOST: "Sauf si vous avez vu le nom de l'UTF-8, génère un message d'erreur si vous voyez des caractères non-ASCII". Je pense que cela est correct comme écrit. La règle est la suivante: Si vous (a) Voir un caractère non-ASCII et (B), vous n'avez pas vu de naissance UTF-8, vous devez générer une erreur. (Si vous voyez un personnage non ASCII, mais vous avez vu un nom d'erreur UTF-8, vous ne faites pas de message d'erreur; et évidemment si tous les caractères sont ASCII, vous ne faites pas de message d'erreur.) J'espère que cela clarifie les choses. S'il vous plaît demander plus de questions si je suis toujours pas clair et / ou si je ressemble vraiment à ce que je me trompe vraiment ici.


En ce qui concerne \ r - j'ai lu si ce n'est pas un "\ n", le remettez-le pour signifier mettre juste lire \ r retour pendant que cela ne s'applique pas à \ r mais le prochain caractère. Ma faute. Pour voir l'UTF-8 BOM ce que je veux dire, c'est la première chose à faire de vérifier s'il y a une chômée ou non et seulement s'il est présent, essayez de décoder à l'aide du codec UTF-8. Merci pour la clarification.


@PIOLDOBROGROGOST pour la chose de décodage UTF8, vous avez raison. Il est plus logique de penser à cela comme vous dites (erreur si vous voyez des octets ci-dessus \ x7f sauf si vous avez vu le bom, O'wise Decoder), mais d'autre part je pense Que ce que j'ai dit (premier décodage, puis Erreur si vous obtenez des erreurs de décodage ou si le résultat contient des caractères unicode ci-dessus \ u7f ) est équivalent et était comme cela se produit comme je pensais à des choses. BTW, voudriez-vous voir mon code Python pour cela? (Ce n'est en fait pas 100% fidèle à l'algo ci-dessus en ce que mon code reconnaît les boms UTF16, contrairement à l'interprète de Python).


@ circulaire-ruine, avez-vous déjà fait une bibliothèque Python?


@Leorochael j'ai mis en œuvre ce qui précède comme module Python; Ce n'est pas publié nulle part mais je pourrais la mettre sur GitHub si cela vous sera utile --- Seriez-vous?


@Leorochael j'ai fait un gist: gist.github.com/anonymous/e7e8a33dbfd9CB645D74 , que je pense a le code pertinent dedans. J'ai bien peur que ce ne soit pas bien commenté. NB que le projet ceci a été utilisé pour finalement décider de faire une pause de Pep 0263: il sera reconnaître et honorer une marque d'ordre d'octets UTF-16, contrairement à Python. Donc, le code dans l'essentiel peut avoir besoin de modification si vous avez besoin de la fidélité exacte de Python.



1
votes

À partir de Python 3.4 Il existe une fonction qui vous permet de faire ce que vous demandez - importatlib.util.decode_source

Selon Documentation :

importateurLib.util.decode_source (source_bytes)
Décodez les octets donnés représentant le code source et renvoyez-la sous la forme d'une chaîne avec des nouvelles lignes universelles (comme requis par ImportLib.abc.infectLoader.get_source () ).

Brett cannon Talks À propos de cette fonction dans sa conversation de la source au code: comment Le compilateur de CPPHON fonctionne .


1 commentaires

Merci! Il est utile que cela soit apparu. Mieux vaut tard que jamais :)



2
votes

Il y a une prise en charge dans la bibliothèque standard, même en Python 2. Voici le code que vous pouvez utiliser:

def read_source_file(filename):
    from lib2to3.pgen2.tokenize import cookie_re

    with open_with_encoding_check(filename) as f:
        return ''.join([
            '\n' if i < 2 and cookie_re.match(line)
            else line
            for i, line in enumerate(f)
        ])


1 commentaires

Merci - - C'est une solution soignée pour Python 2. En outre, votre débogueur a l'air vraiment cool :)