Jak wiarygodnie odgadnąć kodowanie między MacRoman, CP1252, Latin1, UTF-8 i ASCII

99

W pracy wydaje się, że żaden tydzień nie mija bez związanej z kodowaniem żałoby, nieszczęścia lub katastrofy. Problem zwykle pochodzi od programistów, którzy uważają, że mogą niezawodnie przetworzyć plik „tekstowy” bez określania kodowania. Ale nie możesz.

Postanowiono więc odtąd zabronić plikom kiedykolwiek nazw kończących się na *.txtlub *.text. Uważa się, że te rozszerzenia wprowadzają zwykłego programistę w tępe zadowolenie z kodowania, a to prowadzi do niewłaściwej obsługi. Prawie lepiej byłoby nie mieć żadnego rozszerzenia, ponieważ przynajmniej wtedy wiesz , że nie wiesz, co masz.

Jednak nie posuniemy się tak daleko. Zamiast tego oczekuje się, że użyjesz nazwy pliku, która kończy się kodowaniem. Więc dla plików tekstowych, na przykład, to byłoby coś README.ascii, README.latin1, README.utf8, itd.

W przypadku plików wymagających określonego rozszerzenia, jeśli można określić kodowanie w samym pliku, na przykład w Perlu lub Pythonie, należy to zrobić. W przypadku plików takich jak źródło Java, w przypadku których nie istnieje taka funkcja wewnątrz pliku, kodowanie należy umieścić przed rozszerzeniem, takim jak SomeClass-utf8.java.

W przypadku wyjścia zdecydowanie preferowany jest UTF-8 .

Ale aby wprowadzić dane, musimy dowiedzieć się, jak radzić sobie z tysiącami plików w naszej bazie kodu o nazwie *.txt. Chcemy zmienić ich nazwy, aby pasowały do ​​naszego nowego standardu. Ale nie możemy ich wszystkich przyjrzeć. Potrzebujemy więc biblioteki lub programu, który faktycznie działa.

Są różne w ASCII, ISO-8859-1, UTF-8, Microsoft CP1252 lub Apple MacRoman. Chociaż wiemy, że możemy stwierdzić, czy coś jest ASCII, i znosimy dobrą zmianę, wiedząc, czy coś jest prawdopodobnie UTF-8, jesteśmy zaskoczeni kodowaniem 8-bitowym. Ponieważ pracujemy w mieszanym środowisku Unix (Solaris, Linux, Darwin), gdzie większość komputerów stacjonarnych to komputery Mac, mamy sporo irytujących plików MacRoman. A to szczególnie stanowi problem.

Od jakiegoś czasu szukałem sposobu, aby programowo określić, który z nich

  1. ASCII
  2. ISO-8859-1
  3. CP1252
  4. MacRoman
  5. UTF-8

plik się znajduje, a nie znalazłem programu ani biblioteki, która mogłaby niezawodnie rozróżnić te trzy różne 8-bitowe kodowanie. Prawdopodobnie mamy ponad tysiąc samych plików MacRoman, więc niezależnie od używanego przez nas detektora znaków, musi być w stanie je wykryć. Nic, na co patrzyłem, nie poradzi sobie z tą sztuczką. Miałem duże nadzieje co do biblioteki do wykrywania zestawów znaków ICU , ale nie radzi sobie ona z MacRomanem. Przyjrzałem się także modułom, które wykonują to samo w Perlu i Pythonie, ale zawsze jest to ta sama historia: brak obsługi wykrywania MacRoman.

Dlatego szukam istniejącej biblioteki lub programu, który niezawodnie określa, w którym z tych pięciu kodowań znajduje się plik - a najlepiej więcej. W szczególności musi rozróżniać trzy 3-bitowe kodowanie, które zacytowałem, zwłaszcza MacRoman . Pliki zawierają ponad 99% tekstu w języku angielskim; jest kilka w innych językach, ale niewiele.

Jeśli jest to kod biblioteki, preferujemy język Perl, C, Java lub Python i w tej kolejności. Jeśli jest to tylko program, to nie obchodzi nas, w jakim języku jest, o ile jest w pełnym kodzie źródłowym, działa w systemie Unix i jest całkowicie wolny od obciążeń.

Czy ktoś inny miał ten problem z milionami starszych plików tekstowych, które zostały losowo zakodowane? Jeśli tak, w jaki sposób próbowałeś go rozwiązać i jaki był sukces? To jest najważniejszy aspekt mojego pytania, ale interesuje mnie również, czy myślisz, że zachęcanie programistów do nadawania nazw (lub zmiany nazwy) swoich plików za pomocą faktycznego kodowania tych plików pomoże nam uniknąć problemu w przyszłości. Czy ktokolwiek próbował wyegzekwować to na zasadach instytucjonalnych, a jeśli tak, to czy to się udało, czy nie, i dlaczego?

I tak, w pełni rozumiem, dlaczego nie można zagwarantować jednoznacznej odpowiedzi, biorąc pod uwagę naturę problemu. Dotyczy to zwłaszcza małych plików, w przypadku których nie masz wystarczającej ilości danych, aby kontynuować. Na szczęście nasze pliki rzadko są małe. Oprócz READMEplików losowych większość ma rozmiar od 50 KB do 250 KB, a wiele z nich jest większych. Wszystko, co przekracza kilka K, na pewno będzie w języku angielskim.

Domeną problemu jest eksploracja tekstu biomedycznego, więc czasami mamy do czynienia z rozległymi i bardzo dużymi korpusami, takimi jak wszystkie repozytorium Open Access PubMedCentral. Dość dużym plikiem jest BioThesaurus 6.0 o rozmiarze 5,7 gigabajta. Ten plik jest szczególnie denerwujący, ponieważ prawie cały jest UTF-8. Jednak jakiś numbskull poszedł i umieścił w nim kilka wierszy, które są w 8-bitowym kodowaniu - chyba Microsoft CP1252. Trwa trochę czasu, zanim się na niego potkniesz. :(

tchrist
źródło
Zobacz stackoverflow.com/questions/4255305/ ... aby znaleźć rozwiązanie
mpenkov

Odpowiedzi:

86

Po pierwsze, proste przypadki:

ASCII

Jeśli twoje dane nie zawierają bajtów powyżej 0x7F, to jest to ASCII. (Lub 7-bitowe kodowanie ISO646, ale są one bardzo przestarzałe).

UTF-8

Jeśli Twoje dane sprawdzają się jako UTF-8, możesz bezpiecznie założyć, że jest to UTF-8. Ze względu na ścisłe zasady walidacji UTF-8 fałszywe alarmy są niezwykle rzadkie.

ISO-8859-1 a Windows-1252

Jedyną różnicą między tymi dwoma kodowaniami jest to, że ISO-8859-1 zawiera znaki sterujące C1, a windows-1252 - znaki drukowalne € ‚ƒ„… † ‡ ˆ ‰ Š ‹ŒŽ ''„ ”• –—˜ ™ š› œžŸ. Widziałem wiele plików, które używają cudzysłowów lub myślników, ale żaden nie używa znaków sterujących C1. Więc nawet nie przejmuj się nimi lub ISO-8859-1, po prostu wykryj zamiast tego windows-1252.

To teraz pozostawia tylko jedno pytanie.

Jak odróżniasz MacRoman od cp1252?

To jest o wiele trudniejsze.

Niezdefiniowane znaki

Bajty 0x81, 0x8D, 0x8F, 0x90, 0x9D nie są używane w systemie Windows 1252. Jeśli wystąpią, załóżmy, że dane to MacRoman.

Identyczne postacie

Bajty 0xA2 (¢), 0xA3 (£), 0xA9 (©), 0xB1 (±), 0xB5 (µ) są takie same w obu kodowaniach. Jeśli są to jedyne bajty spoza ASCII, nie ma znaczenia, czy wybierzesz MacRoman, czy cp1252.

Podejście statystyczne

Policz częstotliwości znaków (NIE bajtów!) W danych, o których wiesz, że są to UTF-8. Określ najczęściej występujące znaki. Następnie użyj tych danych, aby określić, czy znaki cp1252 czy MacRoman są bardziej powszechne.

Na przykład podczas wyszukiwania, które właśnie przeprowadziłem na 100 losowych artykułach z angielskiej Wikipedii, najczęściej występującymi znakami spoza ASCII są ·•–é°®’èö—. W oparciu o ten fakt

  • Bajty 0x92, 0x95, 0x96, 0x97, 0xAE, 0xB0, 0xB7, 0xE8, 0xE9 lub 0xF6 sugerują windows-1252.
  • Bajty 0x8E, 0x8F, 0x9A, 0xA1, 0xA5, 0xA8, 0xD0, 0xD1, 0xD5 lub 0xE1 sugerują MacRoman.

Policz bajty sugerujące cp1252 i bajty sugerujące MacRoman i idź z tym, który jest największy.

dan04
źródło
6
Przyjąłem twoją odpowiedź, ponieważ nie przedstawił się żaden lepszy i wykonałeś dobrą robotę spisując te same kwestie, nad którymi majstrowałem. Rzeczywiście mam programy do wyłapywania tych bajtów, chociaż masz około dwa razy więcej niż sam wymyśliłem.
tchrist
10
W końcu zabrałem się za wdrożenie tego. Okazuje się, że Wikipedia nie przedstawia dobrych danych treningowych. Z 1 tys. Losowych artykułów w en.wikipedii, nie licząc sekcji JĘZYKI, otrzymałem 50 tys. Punktów kodowych unASCII, ale dystrybucja nie jest wiarygodna: środkowa kropka i punktor są zbyt wysokie, & c & c & c. Więc użyłem korpusu PubMed w całości opartego na UTF8 Open Access, wydobywając + 14M punktów kodowych unASCII. Używam ich do zbudowania modelu częstotliwości względnej wszystkich 8-bitowych kodowań, bardziej wyszukanego niż twoje, ale opartego na tym pomyśle. Dowodzi to wysoce przewidywalnego kodowania tekstów biomedycznych, domeny docelowej. Powinienem to opublikować. Dzięki!
tchrist
5
Nadal nie mam żadnych plików MacRoman, ale nie użyłbym CR jako separatorów linii, które zapewniłyby przydatny test. To działałoby w przypadku starszych wersji systemu Mac OS, chociaż nie znam systemu OS9.
Milliways
10

Mozilla nsUniversalDetector (powiązania Perla: Encode :: Detect / Encode :: Detect :: Detector ) jest milion razy sprawdzona.

daxim
źródło
Więcej dokumentacji można znaleźć tutaj: mozilla.org/projects/intl/detectorsrc.html , stamtąd sugeruje się, że jeśli zagłębisz się w dokumentację, możesz znaleźć obsługiwane zestawy znaków
Joel Berger
@Joel: Wkopałem się w źródło. To było pytanie retoryczne. x-mac-cyrillicjest obsługiwany, x-mac-hebrewjest szczegółowo omawiany w komentarzach, x-mac-anything-elsenie ma wzmianki.
John Machin
@John Machin: dziwne, że cyrylica i hebrajski kiwają głową, ale nic więcej. Wrzucałem tylko inne źródło dokumentacji, nie czytałem dalej, dzięki, że to robisz!
Joel Berger
7

Moja próba zastosowania takiej heurystyki (zakładając, że wykluczyłeś ASCII i UTF-8):

  • Jeśli 0x7f do 0x9f w ogóle się nie pojawia, to prawdopodobnie ISO-8859-1, ponieważ są to bardzo rzadko używane kody kontrolne.
  • Jeśli w partii pojawia się 0x91 do 0x94, prawdopodobnie jest to Windows-1252, ponieważ są to „inteligentne cudzysłowy”, zdecydowanie najbardziej prawdopodobne znaki z tego zakresu używane w tekście angielskim. Aby być bardziej pewnym, możesz poszukać par.
  • W przeciwnym razie jest to MacRoman, zwłaszcza jeśli widzisz dużo od 0xd2 do 0xd5 (tam są cudzysłowy typograficzne w MacRoman).

Dygresja:

W przypadku plików takich jak źródło Java, w przypadku których nie istnieje taka funkcja wewnątrz pliku, kodowanie należy umieścić przed rozszerzeniem, na przykład SomeClass-utf8.java

Nie rób tego!!

Kompilator Java oczekuje, że nazwy plików będą zgodne z nazwami klas, więc zmiana nazw plików spowoduje, że kod źródłowy będzie niekompilowalny. Prawidłową rzeczą byłoby odgadnięcie kodowania, a następnie użycie native2asciinarzędzia do konwersji wszystkich znaków spoza ASCII na sekwencje ucieczki Unicode .

Michael Borgwardt
źródło
7
Stoopid kompilor! Nie, nie możemy powiedzieć ludziom, że mogą używać tylko ASCII; to już nie są lata 60. Nie byłoby problemu, gdyby istniała adnotacja @encoding, aby fakt, że źródło jest w określonym kodowaniu, nie był zmuszony do przechowywania poza kodem źródłowym, naprawdę idiotyczna wada Javy, na którą ani Perl, ani Python nie cierpią . Powinien znajdować się w źródle. To nie jest jednak nasz główny problem; to tysiące *.textplików.
tchrist
3
@tchrist: Właściwie nie byłoby tak trudno napisać własny procesor adnotacji, który obsługuje taką adnotację. Wciąż żenujące przeoczenie, że nie ma go w standardowym API.
Michael Borgwardt
Nawet jeśli Java obsługuje @encoding, nie zapewniłoby to poprawności deklaracji kodowania .
dan04
4
@ dan04: To samo możesz powiedzieć o deklaracji kodowania w XML, HTML lub gdziekolwiek indziej. Ale tak jak w przypadku tych przykładów, gdyby został zdefiniowany w standardowym API, większość narzędzi współpracujących z kodem źródłowym (zwłaszcza edytory i IDE) będzie go obsługiwać, co całkiem niezawodnie zapobiegnie przypadkowemu tworzeniu plików, których kodowanie zawartości nie pasuje Deklaracja.
Michael Borgwardt
4
„Kompilator Java oczekuje, że nazwy plików będą zgodne z nazwami klas”. Ta reguła ma zastosowanie tylko wtedy, gdy plik definiuje klasę publiczną najwyższego poziomu.
Matthew Flaschen
6

„Perl, C, Java lub Python i w tej kolejności”: ciekawe podejście :-)

„Mamy dobrą zmianę, wiedząc, czy coś jest prawdopodobnie UTF-8”: W rzeczywistości szansa, że ​​plik zawierający znaczący tekst zakodowany w innym zestawie znaków, który używa bajtów o dużej wartości bitowej, zostanie pomyślnie zdekodowany, ponieważ UTF-8 jest znikomo mały.

Strategie UTF-8 (w najmniej preferowanym języku):

# 100% Unicode-standard-compliant UTF-8
def utf8_strict(text):
    try:
        text.decode('utf8')
        return True
    except UnicodeDecodeError:
        return False

# looking for almost all UTF-8 with some junk
def utf8_replace(text):
    utext = text.decode('utf8', 'replace')
    dodgy_count = utext.count(u'\uFFFD') 
    return dodgy_count, utext
    # further action depends on how large dodgy_count / float(len(utext)) is

# checking for UTF-8 structure but non-compliant
# e.g. encoded surrogates, not minimal length, more than 4 bytes:
# Can be done with a regex, if you need it

Kiedy już zdecydujesz, że nie jest to ani ASCII, ani UTF-8:

Wykrywacze zestawów znaków pochodzenia Mozilli, o których wiem, nie obsługują MacRoman, aw każdym razie nie radzą sobie dobrze z 8-bitowymi zestawami znaków, szczególnie w języku angielskim, ponieważ AFAICT polegają na sprawdzeniu, czy dekodowanie ma sens w danym język, ignorując znaki interpunkcyjne i oparty na szerokiej gamie dokumentów w tym języku.

Jak zauważyli inni, tak naprawdę masz dostępne tylko znaki interpunkcyjne o wysokim ustawieniu bitów, aby odróżnić cp1252 od macroman. Sugerowałbym wytrenowanie modelu typu Mozilla na własnych dokumentach, a nie na Szekspirze, Hansardzie czy Biblii KJV i wzięcie pod uwagę wszystkich 256 bajtów. Przypuszczam, że twoje pliki nie zawierają żadnych znaczników (HTML, XML itp.) - to mogłoby zniekształcić prawdopodobieństwo coś szokującego.

Wspomniałeś o plikach, które w większości są w formacie UTF-8, ale nie można ich zdekodować. Powinieneś także być bardzo podejrzliwy wobec:

(1) pliki, które są rzekomo zakodowane w ISO-8859-1, ale zawierają „znaki kontrolne” z zakresu od 0x80 do 0x9F włącznie ... jest to tak powszechne, że projekt standardu HTML5 nakazuje dekodować WSZYSTKIE strumienie HTML zadeklarowane jako ISO-8859 -1 przy użyciu cp1252.

(2) pliki, które dekodują OK jako UTF-8, ale wynikowy Unicode zawiera „znaki sterujące” z zakresu od U + 0080 do U + 009F włącznie ... może to wynikać z transkodowania cp1252 / cp850 (widać, że to się stało!) / Itd. pliki od „ISO-8859-1” do UTF-8.

Kontekst: Mam projekt na mokre niedzielne popołudnie, aby stworzyć oparty na Pythonie detektor zestawu znaków, który jest zorientowany na pliki (zamiast zorientowanego na sieć) i działa dobrze z 8-bitowymi zestawami znaków, w tym legacy ** ntakimi jak cp850 i cp437. Jeszcze nie nadeszła pora największej oglądalności. Interesują mnie pliki szkoleniowe; Czy Twoje pliki ISO-8859-1 / cp1252 / MacRoman są równie „nieobciążone”, jak oczekujesz od czyjegoś rozwiązania kodu?

John Machin
źródło
1
powodem uporządkowania języka jest środowisko. Większość naszych głównych aplikacji znajduje się zwykle w Javie, a pomniejsze narzędzia, a niektóre aplikacje są w Perlu. Mamy tu i tam trochę kodu w Pythonie. Jestem głównie programistą C i Perl, przynajmniej z pierwszego wyboru, więc szukałem rozwiązania java do podłączenia do naszej biblioteki aplikacji lub biblioteki Perl do tego samego. Jeśli C, mógłbym zbudować warstwę kleju XS, aby połączyć ją z interfejsem Perla, ale nigdy wcześniej tego nie robiłem w Pythonie.
tchrist
3

Jak odkryłeś, nie ma idealnego sposobu rozwiązania tego problemu, ponieważ bez ukrytej wiedzy o tym, jakiego kodowania używa plik, wszystkie 8-bitowe kodowania są dokładnie takie same: zbiór bajtów. Wszystkie bajty są ważne dla wszystkich 8-bitowych kodowań.

Najlepsze, na co możesz liczyć, to jakiś algorytm, który analizuje bajty i na podstawie prawdopodobieństwa użycia określonego bajtu w określonym języku z określonym kodowaniem zgadnie, jakiego kodowania używa pliki. Ale to musi wiedzieć, jakiego języka używa plik, i staje się całkowicie bezużyteczne, gdy masz pliki z mieszanym kodowaniem.

Z drugiej strony, jeśli wiesz, że tekst w pliku jest napisany po angielsku, prawdopodobnie nie zauważysz żadnej różnicy niezależnie od tego, które kodowanie zdecydujesz się użyć dla tego pliku, ponieważ różnice między wszystkimi wspomnianymi kodowaniami są zlokalizowane w części kodowania, które określają znaki, które nie są zwykle używane w języku angielskim. Możesz mieć pewne problemy, gdy tekst używa specjalnego formatowania lub specjalnych wersji znaków interpunkcyjnych (na przykład CP1252 ma kilka wersji cudzysłowów), ale jeśli chodzi o istotę tekstu, prawdopodobnie nie będzie żadnych problemów.

Epcylon
źródło
1

Jeśli potrafisz wykryć każde kodowanie Z WYJĄTKIEM makromany, logiczne byłoby założenie, że te, których nie można odszyfrować, są w makromie. Innymi słowy, po prostu zrób listę plików, których nie można przetworzyć i postępuj z nimi tak, jakby były makromą.

Innym sposobem sortowania tych plików byłoby utworzenie programu opartego na serwerze, który pozwala użytkownikom zdecydować, które kodowanie nie jest zniekształcone. Oczywiście byłoby to w firmie, ale przy 100 pracownikach wykonujących po kilku każdego dnia będziesz mieć tysiące plików zrobionych w mgnieniu oka.

Wreszcie, czy nie byłoby lepiej po prostu przekonwertować wszystkie istniejące pliki do jednego formatu i wymagać, aby nowe pliki były w tym formacie.

Eric Pauley
źródło
5
Zabawny! Kiedy po raz pierwszy przeczytałem ten komentarz po 30 minutowej przerwie, przeczytałem „macroman” jako „makro man” i nie nawiązałem połączenia z MacRomanem, dopóki nie wyszukałem tego ciągu, aby sprawdzić, czy OP o nim wspomniał
Adrian Pronk
+1 ta odpowiedź jest dość interesująca. nie wiem, czy to dobry, czy zły pomysł. Czy ktoś może pomyśleć o istniejącym kodowaniu, które również może pozostać niewykryte? czy jest prawdopodobne, że będzie taki w przyszłości?
nazwa użytkownika
1

Czy ktoś inny miał ten problem z milionami starszych plików tekstowych, które zostały losowo zakodowane? Jeśli tak, w jaki sposób próbowałeś go rozwiązać i jaki był sukces?

Obecnie piszę program, który tłumaczy pliki na XML. Musi automatycznie wykrywać typ każdego pliku, co jest nadzbiorem problemu określenia kodowania pliku tekstowego. Do określenia kodowania używam metody bayesowskiej. Oznacza to, że mój kod klasyfikacyjny oblicza prawdopodobieństwo (prawdopodobieństwo), że plik tekstowy ma określone kodowanie dla wszystkich kodowań, które rozumie. Następnie program wybiera najbardziej prawdopodobny dekoder. Podejście bayesowskie działa w ten sposób dla każdego kodowania.

  1. Ustaw początkowe ( wcześniejsze ) prawdopodobieństwo, że plik jest zakodowany, na podstawie częstotliwości każdego kodowania.
  2. Sprawdź po kolei każdy bajt w pliku. Wyszukaj wartość bajtu, aby określić korelację między obecną wartością bajtu a plikiem faktycznie znajdującym się w tym kodowaniu. Użyj tej korelacji, aby obliczyć nowe ( późniejsze ) prawdopodobieństwo, że plik jest zakodowany. Jeśli masz więcej bajtów do zbadania, podczas sprawdzania następnego bajtu użyj późniejszego prawdopodobieństwa tego bajtu jako prawdopodobieństwa poprzedniego.
  3. Kiedy dojdziesz do końca pliku (właściwie patrzę tylko na pierwsze 1024 bajty), wydajność, którą masz, to prawdopodobieństwo, że plik jest zakodowany.

Okazuje się, że Bayesa Twierdzenie staje się bardzo łatwe do zrobienia, jeśli zamiast obliczania prawdopodobieństw, obliczyć zawartość informacyjną , która jest logarytm z kursem : info = log(p / (1.0 - p)).

Będziesz musiał obliczyć prawdopodobieństwo initail priori i korelacje, badając korpus plików, które zostały ręcznie sklasyfikowane.

Raedwald
źródło