Convierta UTF-8 con BOM a UTF-8 sin BOM en Python

81

Aquí dos preguntas. Tengo un conjunto de archivos que suelen ser UTF-8 con BOM. Me gustaría convertirlos (idealmente en su lugar) a UTF-8 sin BOM. Parece codecs.StreamRecoder(stream, encode, decode, Reader, Writer, errors)que manejaría esto. Pero realmente no veo buenos ejemplos de uso. ¿Sería esta la mejor manera de manejar esto?

source files:
Tue Jan 17$ file brh-m-157.json 
brh-m-157.json: UTF-8 Unicode (with BOM) text

Además, sería ideal si pudiéramos manejar diferentes codificaciones de entrada sin saberlo explícitamente (visto ASCII y UTF-16). Parece que todo esto debería ser factible. ¿Existe una solución que pueda tomar cualquier codificación y salida de Python conocida como UTF-8 sin BOM?

editar 1 sol'n propuesto desde abajo (¡gracias!)

fp = open('brh-m-157.json','rw')
s = fp.read()
u = s.decode('utf-8-sig')
s = u.encode('utf-8')
print fp.encoding  
fp.write(s)

Esto me da el siguiente error:

IOError: [Errno 9] Bad file descriptor

Noticias de última hora

En los comentarios me dicen que el error es que abro el archivo con el modo 'rw' en lugar de 'r +' / 'r + b', por lo que eventualmente debería volver a editar mi pregunta y eliminar la parte resuelta.

timpone
fuente
2
Necesita abrir su archivo para lectura más actualización, es decir, con un r+modo. Agregue btambién para que funcione también en Windows sin ningún negocio divertido que termine la línea. Finalmente, querrá volver al principio del archivo y truncarlo al final; consulte mi respuesta actualizada.
Martin Geisler

Respuestas:

123

Simplemente use el códec "utf-8-sig" :

fp = open("file.txt")
s = fp.read()
u = s.decode("utf-8-sig")

Eso le da una unicodecadena sin la lista de materiales. Luego puede usar

s = u.encode("utf-8")

para recuperar una cadena codificada en UTF-8 normal s. Si sus archivos son grandes, debe evitar leerlos todos en la memoria. La lista de materiales tiene simplemente tres bytes al principio del archivo, por lo que puede usar este código para eliminarlos del archivo:

import os, sys, codecs

BUFSIZE = 4096
BOMLEN = len(codecs.BOM_UTF8)

path = sys.argv[1]
with open(path, "r+b") as fp:
    chunk = fp.read(BUFSIZE)
    if chunk.startswith(codecs.BOM_UTF8):
        i = 0
        chunk = chunk[BOMLEN:]
        while chunk:
            fp.seek(i)
            fp.write(chunk)
            i += len(chunk)
            fp.seek(BOMLEN, os.SEEK_CUR)
            chunk = fp.read(BUFSIZE)
        fp.seek(-BOMLEN, os.SEEK_CUR)
        fp.truncate()

Abre el archivo, lee un fragmento y lo escribe en el archivo 3 bytes antes de donde lo leyó. El archivo se reescribe in situ. Una solución más fácil es escribir el archivo más corto en un archivo nuevo como la respuesta de newtover . Eso sería más simple, pero use el doble de espacio en disco durante un período corto.

En cuanto a adivinar la codificación, puede recorrer la codificación de la más a la menos específica:

def decode(s):
    for encoding in "utf-8-sig", "utf-16":
        try:
            return s.decode(encoding)
        except UnicodeDecodeError:
            continue
    return s.decode("latin-1") # will always work

Un archivo codificado en UTF-16 no se decodificará como UTF-8, así que probamos primero con UTF-8. Si eso falla, lo intentamos con UTF-16. Finalmente, usamos Latin-1; esto siempre funcionará ya que los 256 bytes son valores legales en Latin-1. Es posible que desee volver Noneen su lugar en este caso, ya que en realidad es una alternativa y es posible que su código quiera manejar esto con más cuidado (si puede).

Martin Geisler
fuente
hmm, actualicé la pregunta en la edición n. ° 1 con un código de muestra, pero obtuve un descriptor de archivo incorrecto. gracias por cualquier ayuda. Tratando de resolver esto.
timpone
60

En Python 3 es bastante fácil: lea el archivo y vuelva a escribirlo con utf-8codificación:

s = open(bom_file, mode='r', encoding='utf-8-sig').read()
open(bom_file, mode='w', encoding='utf-8').write(s)
Geng Jiawen
fuente
1
La mejor respuesta en la web sobre este tema. Simplemente use utf-8-sig.
QtRoS
6
import codecs
import shutil
import sys

s = sys.stdin.read(3)
if s != codecs.BOM_UTF8:
    sys.stdout.write(s)

shutil.copyfileobj(sys.stdin, sys.stdout)
newtover
fuente
¿Puedes explicar cómo funciona este código? $ remove_bom.py <input.txt> output.txt ¿Estoy en lo cierto?
guneysus
@guneysus, sí, exactamente
newtover
1
acabo de agregarheader = header[3:] if header[0:3] == codecs.BOM_UTF8 else header
chinmayv
5

Esta es mi implementación para convertir cualquier tipo de codificación a UTF-8 sin BOM y reemplazar Windows enlines por formato universal:

def utf8_converter(file_path, universal_endline=True):
    '''
    Convert any type of file to UTF-8 without BOM
    and using universal endline by default.

    Parameters
    ----------
    file_path : string, file path.
    universal_endline : boolean (True),
                        by default convert endlines to universal format.
    '''

    # Fix file path
    file_path = os.path.realpath(os.path.expanduser(file_path))

    # Read from file
    file_open = open(file_path)
    raw = file_open.read()
    file_open.close()

    # Decode
    raw = raw.decode(chardet.detect(raw)['encoding'])
    # Remove windows end line
    if universal_endline:
        raw = raw.replace('\r\n', '\n')
    # Encode to UTF-8
    raw = raw.encode('utf8')
    # Remove BOM
    if raw.startswith(codecs.BOM_UTF8):
        raw = raw.replace(codecs.BOM_UTF8, '', 1)

    # Write to file
    file_open = open(file_path, 'w')
    file_open.write(raw)
    file_open.close()
    return 0
estevo
fuente
2

Puede utilizar códecs.

import codecs
with open("test.txt",'r') as filehandle:
    content = filehandle.read()
if content[:3] == codecs.BOM_UTF8:
    content = content[3:]
print content.decode("utf-8")
wcc526
fuente
snipplet no utilizable en absoluto (filehandle? también codecs.BOM_UTF8 devuelve un error de sintaxis)
Max
2

Encontré esta pregunta porque tuve problemas configparser.ConfigParser().read(fp)al abrir archivos con el encabezado BOM UTF8.

Para aquellos que buscan una solución para eliminar el encabezado para que ConfigPhaser pueda abrir el archivo de configuración en lugar de informar un error de:, File contains no section headersabra el archivo de la siguiente manera:

configparser.ConfigParser().read(config_file_path, encoding="utf-8-sig")

Esto podría ahorrarle mucho esfuerzo al hacer que la eliminación del encabezado de la lista de materiales del archivo sea innecesaria.

(Sé que esto no suena relacionado, pero espero que esto pueda ayudar a las personas que luchan como yo).

Alto.Clef
fuente
1
como estaba trabajando por primera vez con try - excepto -> esto también abre archivos codificados UTF-8 "no BOM" sin problemas
flipSTAR