web-dev-qa-db-fra.com

Lecture des données d'un fichier Unicode avec des caractères de nomenclature dans Python

Je lis une série de fichiers de code source en utilisant Python et je rencontre une erreur de nomenclature unicode. Voici mon code:

bytes = min(32, os.path.getsize(filename))
raw = open(filename, 'rb').read(bytes)
result = chardet.detect(raw)
encoding = result['encoding']

infile = open(filename, mode, encoding=encoding)
data = infile.read()
infile.close()

print(data)

Comme vous pouvez le voir, je détecte l'encodage à l'aide de chardet, puis je lis le fichier en mémoire et j'essaie de l'imprimer. L'instruction print échoue sur les fichiers Unicode contenant une nomenclature avec l'erreur:

UnicodeEncodeError: le codec 'charmap' ne peut pas coder les caractères en position 0-2:
mappage de caractères à <non défini>

Je suppose qu'il essaie de décoder la nomenclature en utilisant le jeu de caractères par défaut et qu'il échoue. Comment supprimer la nomenclature de la chaîne pour éviter cela?

35
Chris

Il n'y a aucune raison de vérifier si une nomenclature existe ou non, utf-8-sig gère cela pour vous et se comporte exactement comme utf-8 si la nomenclature n'existe pas:

# Standard UTF-8 without BOM
>>> b'hello'.decode('utf-8')
'hello'
>>> b'hello'.decode('utf-8-sig')
'hello'

# BOM encoded UTF-8
>>> b'\xef\xbb\xbfhello'.decode('utf-8')
'\ufeffhello'
>>> b'\xef\xbb\xbfhello'.decode('utf-8-sig')
'hello'

Dans l'exemple ci-dessus, vous pouvez voir utf-8-sig décode correctement la chaîne donnée indépendamment de l'existence de la nomenclature. Si vous pensez qu'il y a même une petite chance qu'un caractère BOM puisse exister dans les fichiers que vous lisez, utilisez simplement utf-8-sig et ne vous en faites pas

36
lightswitch05

Les caractères de nomenclature doivent être automatiquement supprimés lors du décodage UTF-16, mais pas UTF-8, sauf si vous utilisez explicitement le utf-8-sig encodage. Vous pouvez essayer quelque chose comme ceci:

import io
import chardet
import codecs

bytes = min(32, os.path.getsize(filename))
raw = open(filename, 'rb').read(bytes)

if raw.startswith(codecs.BOM_UTF8):
    encoding = 'utf-8-sig'
else:
    result = chardet.detect(raw)
    encoding = result['encoding']

infile = io.open(filename, mode, encoding=encoding)
data = infile.read()
infile.close()

print(data)
50
Chewie

J'ai composé un détecteur basé sur BOM astucieux basé sur la réponse de Chewie. C'est suffisant dans le cas d'utilisation courant où les données peuvent être dans un encodage local connu ou Unicode avec BOM (c'est ce que les éditeurs de texte produisent généralement). Plus important encore, contrairement à chardet, il ne fait aucune supposition aléatoire, donc il donne des résultats prévisibles:

def detect_by_bom(path,default):
    with open(path, 'rb') as f:
        raw = f.read(4)    #will read less if the file is smaller
    for enc,boms in \
            ('utf-8-sig',(codecs.BOM_UTF8,)),\
            ('utf-16',(codecs.BOM_UTF16_LE,codecs.BOM_UTF16_BE)),\
            ('utf-32',(codecs.BOM_UTF32_LE,codecs.BOM_UTF32_BE)):
        if any(raw.startswith(bom) for bom in boms): return enc
    return default
21
ivan_pozdeev

chardet détecte automatiquement BOM_UTF8 depuis version 2.3.0 publiée le 7 octobre 2014 :

#!/usr/bin/env python
import chardet # $ pip install chardet

# detect file encoding
with open(filename, 'rb') as file:
    raw = file.read(32) # at most 32 bytes are returned
    encoding = chardet.detect(raw)['encoding']

with open(filename, encoding=encoding) as file:
    text = file.read()
print(text)

Remarque: chardet peut renvoyer 'UTF-XXLE', 'UTF-XXBE' encodages qui laissent la nomenclature dans le texte. 'LE', 'BE' devrait être supprimé pour l'éviter - bien qu'il soit plus facile de détecter la nomenclature vous-même à ce stade, par exemple, comme dans réponse de @ ivan_pozdeev .

Pour éviter UnicodeEncodeError lors de l'impression de texte Unicode sur la console Windows, voir Python, Unicode et la console Windows .

8
jfs

Je trouve les autres réponses trop complexes. Il existe un moyen plus simple qui n'a pas besoin de descendre dans l'idiome de niveau inférieur des E/S de fichiers binaires, ne repose pas sur une heuristique de jeu de caractères (chardet) qui ne fait pas partie de Python bibliothèque standard, et n'a pas besoin d'une autre signature d'encodage rarement vue (utf-8-sig contre le commun utf-8) qui ne semble pas avoir d'analogue dans la famille UTF-16.

L'approche la plus simple que j'ai trouvée consiste à traiter les caractères de nomenclature dans Unicode et à laisser les codecs faire le gros du travail. Il n'y a qu'un seul Unicode marque d'ordre des octets , donc une fois les données converties en caractères Unicode, il est facile de déterminer si elles sont là et/ou de les ajouter/supprimer. Pour lire un fichier avec une nomenclature possible:

BOM = '\ufeff'
with open(filepath, mode='r', encoding='utf-8') as f:
    text = f.read()
    if text.startswith(BOM):
        text = text[1:]

Cela fonctionne avec tous les codecs UTF intéressants (par exemple utf-8, utf-16le, utf-16be, ...), ne nécessite pas de modules supplémentaires, et ne nécessite pas de descendre dans le traitement de fichiers binaires ou des constantes codec spécifiques.

Pour écrire une nomenclature:

text_with_BOM = text if text.startswith(BOM) else BOM + text
with open(filepath, mode='w', encoding='utf-16be') as f:
    f.write(text_with_BOM)

Cela fonctionne avec n'importe quel encodage. Le big endian UTF-16 n'est qu'un exemple.

Ce n'est pas, btw, de rejeter chardet. Cela peut vous aider lorsque vous n'avez aucune information sur l'encodage utilisé par un fichier. Il n'est tout simplement pas nécessaire pour ajouter/supprimer des nomenclatures.

7
Jonathan Eunice

Une variante de la réponse de @ ivan_pozdeev pour les chaînes/exceptions (plutôt que pour les fichiers). Je traite du contenu HTML unicode qui a été bourré dans une exception python (voir http://bugs.python.org/issue2517 )

def detect_encoding(bytes_str):
  for enc, boms in \
      ('utf-8-sig',(codecs.BOM_UTF8,)),\
      ('utf-16',(codecs.BOM_UTF16_LE,codecs.BOM_UTF16_BE)),\
      ('utf-32',(codecs.BOM_UTF32_LE,codecs.BOM_UTF32_BE)):
    if (any(bytes_str.startswith(bom) for bom in boms): return enc
  return 'utf-8' # default

def safe_exc_to_str(exc):
  try:
    return str(exc)
  except UnicodeEncodeError:
    return unicode(exc).encode(detect_encoding(exc.content))

Alternativement, ce code beaucoup plus simple est capable de supprimer les caractères non-ascii sans trop de bruit:

def just_ascii(str):
  return unicode(str).encode('ascii', 'ignore')
1
Dave Dopson