Konwertuj UTF-8 z BOM na UTF-8 bez BOM w Pythonie

82

Tutaj dwa pytania. Mam zestaw plików, które zwykle są w formacie UTF-8 z BOM. Chciałbym je przekonwertować (najlepiej na miejscu) do UTF-8 bez BOM. Wygląda na to, codecs.StreamRecoder(stream, encode, decode, Reader, Writer, errors)że poradzi sobie z tym. Ale tak naprawdę nie widzę dobrych przykładów użycia. Czy byłby to najlepszy sposób na rozwiązanie tego problemu?

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

Ponadto byłoby idealnie, gdybyśmy mogli obsłużyć różne kodowanie danych wejściowych bez wyraźnej wiedzy (patrz ASCII i UTF-16). Wydaje się, że to wszystko powinno być wykonalne. Czy istnieje rozwiązanie, które może przyjąć dowolne znane kodowanie w Pythonie i wyprowadzać jako UTF-8 bez BOM?

edytuj 1 proponowane rozwiązanie od dołu (dzięki!)

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)

To daje mi następujący błąd:

IOError: [Errno 9] Bad file descriptor

wiadomosci

W komentarzach powiedziano mi, że błąd polega na tym, że otwieram plik w trybie „rw” zamiast „r +” / „r + b”, więc powinienem w końcu ponownie edytować moje pytanie i usunąć rozwiązaną część.

timpone
źródło
2
Musisz otworzyć plik do odczytu i aktualizacji, tj r+. W trybie. Dodaj bteż, aby działał również w systemie Windows bez żadnych zabawnych zakończeń linii. Na koniec zechcesz wrócić do początku pliku i skrócić go na końcu - zobacz moją zaktualizowaną odpowiedź.
Martin Geisler

Odpowiedzi:

125

Po prostu użyj kodeka „utf-8-sig” :

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

To daje ci unicodeciąg bez BOM. Następnie możesz użyć

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

aby odzyskać normalny łańcuch zakodowany w UTF-8 s. Jeśli twoje pliki są duże, powinieneś unikać wczytywania ich wszystkich do pamięci. BOM ma po prostu trzy bajty na początku pliku, więc możesz użyć tego kodu, aby usunąć je z pliku:

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()

Otwiera plik, odczytuje fragment i zapisuje go do pliku 3 bajty wcześniej niż miejsce, w którym go odczytuje. Plik jest przepisywany w miejscu. Łatwiejszym rozwiązaniem jest zapisanie krótszego pliku do nowego pliku, takiego jak odpowiedź newtover . Byłoby to prostsze, ale przez krótki czas zajmowałoby dwukrotnie więcej miejsca na dysku.

Jeśli chodzi o zgadywanie kodowania, możesz po prostu zapętlić kodowanie od najbardziej do najmniej konkretnego:

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

Plik zakodowany w UTF-16 nie zostanie zdekodowany jako UTF-8, więc najpierw spróbujemy z UTF-8. Jeśli to się nie powiedzie, próbujemy z UTF-16. Na koniec używamy Latin-1 - to zawsze będzie działać, ponieważ wszystkie 256 bajtów to legalne wartości w Latin-1. W Nonetym przypadku możesz chcieć zwrócić zamiast tego, ponieważ jest to naprawdę rezerwa i Twój kod może chcieć obsługiwać to ostrożniej (jeśli to możliwe).

Martin Geisler
źródło
hmm, zaktualizowałem pytanie w edycji nr 1 przykładowym kodem, ale otrzymałem zły deskryptor pliku. dzięki za wszelką pomoc. Próbuję to rozgryźć.
timpone
64

W Pythonie 3 jest to całkiem proste: przeczytaj plik i przepisz go z utf-8kodowaniem:

s = open(bom_file, mode='r', encoding='utf-8-sig').read()
open(bom_file, mode='w', encoding='utf-8').write(s)
Geng Jiawen
źródło
3
Najlepsza odpowiedź w sieci na ten temat. Po prostu użyj 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
źródło
czy możesz wyjaśnić, jak działa ten kod? $ remove_bom.py <input.txt> output.txt Czy mam rację?
guneysus
@guneysus, tak, dokładnie
newtover
1
właśnie dodałemheader = header[3:] if header[0:3] == codecs.BOM_UTF8 else header
chinmayv
5

Oto moja implementacja do konwersji dowolnego rodzaju kodowania do UTF-8 bez BOM i zastąpienia powiększania okien przez format uniwersalny:

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
źródło
3

Znalazłem to pytanie, ponieważ mam problem z configparser.ConfigParser().read(fp)otwieraniem plików z nagłówkiem BOM UTF8.

Dla tych, którzy szukają rozwiązania w celu usunięcia nagłówka, aby ConfigPhaser mógł otworzyć plik konfiguracyjny zamiast zgłaszać błąd:, File contains no section headersproszę otworzyć plik w następujący sposób:

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

Może to zaoszczędzić mnóstwo wysiłku, sprawiając, że usunięcie nagłówka BOM pliku będzie niepotrzebne.

(Wiem, że to brzmi niepowiązane, ale mam nadzieję, że może to pomóc ludziom walczącym tak jak ja).

Alto.Clef
źródło
1
ponieważ pierwszy raz pracowałem z try - z wyjątkiem -> to również otwiera pliki zakodowane w UTF-8 "nie BOM" bez problemów
flipSTAR
2

Możesz używać kodeków.

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
źródło
w ogóle nieużywalny fragment kodu (uchwyt pliku? także kodeki, BOM_UTF8 zwraca błąd składni)
maks.