Escribir en el archivo UTF-8 en Python

204

Estoy realmente confundido con el codecs.open function. Cuando lo hago:

file = codecs.open("temp", "w", "utf-8")
file.write(codecs.BOM_UTF8)
file.close()

Me da el error

UnicodeDecodeError: el códec 'ascii' no puede decodificar el byte 0xef en la posición 0: el ordinal no está en el rango (128)

Si lo hago:

file = open("temp", "w")
file.write(codecs.BOM_UTF8)
file.close()

Funciona bien.

La pregunta es ¿por qué falla el primer método? ¿Y cómo inserto el bom?

Si el segundo método es la forma correcta de hacerlo, ¿cuál es el punto de usar codecs.open(filename, "w", "utf-8")?

John Jiang
fuente
54
No use una lista de materiales en UTF-8. Por favor.
tchrist
77
@tchrist ¿eh? Por qué no?
Salman von Abbas
8
@SalmanPK BOM no es necesario en UTF-8 y solo agrega complejidad (por ejemplo, no puede simplemente concatenar archivos BOM'd y dar como resultado texto válido). Ver este Q&A ; no te pierdas el gran comentario bajo Q
Alois Mahdal

Respuestas:

271

Creo que el problema es que codecs.BOM_UTF8es una cadena de bytes, no una cadena Unicode. Sospecho que el controlador de archivos está tratando de adivinar lo que realmente quieres decir en base a "¡Estoy destinado a escribir Unicode como texto codificado en UTF-8, pero me has dado una cadena de bytes!"

Intente escribir la cadena Unicode para la marca de orden de bytes (es decir, Unicode U + FEFF) directamente, de modo que el archivo solo codifique eso como UTF-8:

import codecs

file = codecs.open("lol", "w", "utf-8")
file.write(u'\ufeff')
file.close()

(Eso parece dar la respuesta correcta: un archivo con bytes EF BB BF).

EDITAR: la sugerencia de S. Lott de usar "utf-8-sig" como codificación es mejor que escribir explícitamente la lista de materiales, pero dejaré esta respuesta aquí, ya que explica lo que estaba sucediendo antes.

Jon Skeet
fuente
Advertencia: abrir y abrir no es lo mismo. Si hace "desde los códecs import open", NO será lo mismo que simplemente escribiría "open".
Apache
2
también puede usar codecs.open ('test.txt', 'w', 'utf-8-sig') en su lugar
beta cerrado el
1
Me aparece "TypeError: se requiere un número entero (tengo el tipo str)". No entiendo lo que estamos haciendo aquí. ¿Puede ayudarme alguien, por favor? Necesito agregar una cadena (párrafo) a un archivo de texto. ¿Necesito convertir eso en un número entero antes de escribir?
Mugen
@Mugen: El código exacto que he escrito funciona bien hasta donde puedo ver. Le sugiero que haga una nueva pregunta que muestre exactamente qué código tiene y dónde se produce el error.
Jon Skeet
@Mugen, debe llamar en codecs.openlugar de soloopen
northben
179

Lea lo siguiente: http://docs.python.org/library/codecs.html#module-encodings.utf_8_sig

Hacer esto

with codecs.open("test_output", "w", "utf-8-sig") as temp:
    temp.write("hi mom\n")
    temp.write(u"This has ♭")

El archivo resultante es UTF-8 con la lista de materiales esperada.

S.Lott
fuente
2
Gracias. Eso funcionó (Windows 7 x64, Python 2.7.5 x64). Esta solución funciona bien cuando abre el archivo en modo "a" (agregar).
Mohamad Fakih
Esto no funcionó para mí, Python 3 en Windows. Tuve que hacer esto con open (file_name, 'wb') como bomfile: bomfile.write (codecs.BOM_UTF8) y luego reabrí el archivo para agregarlo.
Dustin Andrews
Tal vez agregar temp.close()?
user2905353
2
@ usuario2905353: no requerido; esto se maneja mediante la gestión de contexto de open.
matheburg
11

@ S-Lott ofrece el procedimiento correcto, pero al ampliar los problemas de Unicode , el intérprete de Python puede proporcionar más información.

Jon Skeet tiene razón (inusual) sobre el codecsmódulo: contiene cadenas de bytes:

>>> import codecs
>>> codecs.BOM
'\xff\xfe'
>>> codecs.BOM_UTF8
'\xef\xbb\xbf'
>>> 

Al elegir otra liendre, BOMtiene un nombre Unicode estándar y se puede ingresar como:

>>> bom= u"\N{ZERO WIDTH NO-BREAK SPACE}"
>>> bom
u'\ufeff'

También es accesible a través de unicodedata:

>>> import unicodedata
>>> unicodedata.lookup('ZERO WIDTH NO-BREAK SPACE')
u'\ufeff'
>>> 
gimel
fuente
8

Uso el comando file * nix para convertir un archivo charset desconocido en un archivo utf-8

# -*- encoding: utf-8 -*-

# converting a unknown formatting file in utf-8

import codecs
import commands

file_location = "jumper.sub"
file_encoding = commands.getoutput('file -b --mime-encoding %s' % file_location)

file_stream = codecs.open(file_location, 'r', file_encoding)
file_output = codecs.open(file_location+"b", 'w', 'utf-8')

for l in file_stream:
    file_output.write(l)

file_stream.close()
file_output.close()
Ricardo
fuente
1
Usar en # coding: utf8lugar de lo # -*- coding: utf-8 -*-cual es mucho más fácil de recordar.
show0k