środa, 1 kwietnia 2015

BŁĄD: znak sekwencją bajtów 0xe2 0x80 0x9e kodowany w "UTF8" nie ma równoważnego w kodowaniu "LATIN2"

Jeżeli ktoś kiedyś natknął się na taki błąd to oznacza, że jego aplikacja powstawała co najmniej 20 lat temu. Komunikat oznacza mniej więcej tyle, że łączy się ona z bazą używając kodowania jednobajtowego latin-2 czy też iso-8859-2, a baza danych używa kodowania utf-8 oraz zawiera tekst, którego nie da się zakodować w latin-2 (np. tekst zawierający długi myślnik). Być może rozwiązanie, które za chwilę przedstawię nikomu się już do niczego nie przyda ale przynajmniej zostanie na pamiątkę dla przyszłych pokoleń.

Nie da się tego problemu tak po prostu obejść. Tzn. jeżeli tylko wykonywane zapytanie na bazie danych przy połączeniu w kodowaniu latin-2 zwraca dane, których nie da się w tym kodowaniu zakodować to każde jedno się po prostu wywali. Zatem jedynym rozwiązaniem tego problemu jest poprawienie tych znaków. Jednakże wyszukiwanie ich ręcznie może być bardzo czasochłonne.

Pierwsza rzecz, którą możemy zrobić to połączenie się psqlem i ustawienie odpowiedniego kodowania dla tego połączenia:

set client_encoding = latin2;

Teraz możemy próbować szukać w jakiej tabeli występuje dany znak. Wystarczy po prostu wykonać:

select * from tabela;

i jeżeli mamy szczęście to otrzymamy od razu komunikat:

BŁĄD: znak sekwencją bajtów 0xe2 0x80 0x9e kodowany w "UTF8" nie ma równoważnego w kodowaniu "LATIN2"

Pozostaje nam teraz zlokalizować wiersz i kolumnę, gdzie interesujący nas znak występuje. Mam na to dwie metody.


Pierwszy sposób

Pierwsza wykorzystuje fakt, że w komunikacie o błędzie występuje zapis szesnastkowy danego znaku. Możemy spróbować zatem go zlokalizować w taki oto sposób:

select id from tabela where encode(kolumna::bytea, 'hex') ilike '%e2809e%';

Zapytanie wykorzystuje postgresową funkcję encode, która potrafi zamienić tekst na reprezentację szesnastkową i w tej reprezentacji poszukujemy ciągu znaków zwróconego nam przez komunikat błędu. Pomijamy tylko 0x występujące w komunikacie. Metoda ta ma dwie wady:

  1. musimy znać kolumnę, w której dany znak może wystąpić (albo podać warunki na każdą kolumnę z użyciem or: ... where encode(kolumna1::bytea, 'hex') ilike '%..%' or encode(kolumna2::bytea, 'hex') ilike '%..%' or ..).
  2. komunikat o błędzie kodowania znaku zwraca nam informację tylko o pierwszym takim przypadku. Jeżeli jest ich więcej to będziemy poprawiać po jednym i czekać aż postgres zwróci nam informację o kolejnym.

Drugi sposób

Druga metoda znajduje wszystkie rekordy, które zawierają "błędne" znaki w tabeli. Niestety wykonanie go trwa znacznie dłużej.

select id
from tabela
where regexp_replace(encode(translate(kolumna, 'ęóąśłżźćńĘÓĄŚŁŻŹĆŃ', '')::bytea, 'hex'), '(.{2})', '\1 ', 'g') ~ '^[8-9a-f]| [8-9a-f]';

Jak już wspomniałem zapytanie wyłapie wszystkie rekordy w tabeli z "niepoprawnymi" znakami w podanej kolumnie. Zapytanie jest dość skomplikowane więc omówię je krok po kroku:

  1. zacznijmy od najbardziej zagnieżdżonego wywołania: translate(kolumna, 'ęóąśłżźćńĘÓĄŚŁŻŹĆŃ', '') Funkcja translate zamienia znaki występujące w jej drugim argumencie na znaki występujące w jej trzecim argumencie. Czyli w tym przypadku po prostu je usuwa. Robimy tak dlatego, że polskie znaczki są jedynymi dwubajtowymi znakami w kodowaniu utf-8, które posiadają swój odpowiednik w kodowaniu latin-2. Takie nas nie interesują. Chcemy wyłapać wszystkie wielobajtowe znaki, których nie ma w latin-2.
  2. encode(..::bytea, 'hex') - zamieniamy znaki na ich szesnastkową reprezentację. Każdy bajt jest teraz zapisany jako dwa znaki.
  3. regexp_replace(.., '.{2}', '\1 ', 'g') - zamieniamy każde dwa znaki jako x i spację. Po prostu rozdzielamy każdy bajt spacją.
  4. ~ '^[8-9a-f]| [8-9a-f]' - operator ~ sprawdza czy podany string odpowiada wyrażeniu regularnemu. Z kolei to wyrażenie sprawdza czy w ciągu znaków występuje 8, 9, a, b, c, d, e lub f, które jest albo na początku stringa albo jest poprzedzone spacją. Chodzi o to, że chcemy wyłapać czy istnieje bajt większy od 127 a taki będzie się zaczynał od tych znaków w reprezentacji szesnastkowej. Jeżeli istnieje to mamy nasz rekord.

Przy tym rozwiązaniu znów musimy testować sobie wszystkie tekstowe kolumny ale przynajmniej wyłapiemy od razu wszystkie rekordy. Możemy oczywiście napisać sobie zapytanie, które wszystkie niedozwolone znaki usunie lub zamieni na inne. Pozostawiam to już w kwestii czytelnika.

1 komentarz: