Kiedy pisze się aplikacje oparte na takich frameworkach jak web2py jest wiele rzeczy o które programista nie musi się martwić, szczególnie jeśli korzysta w pełni z wbudowanych mechanizmów, takich jak choćby smartgrid. To one odpowiadają na przykład za wczytanie danych z bazy i ich prezentację. Ostatnio jednak przyszło mi zrobić coś nieco ambitniejszego. Jeden z naszych klientów potrzebuje przesłać dane ze swojej bazy do systemu RaksSQL, a jednym z lepszych na to sposobów jest przygotowanie odpowiedniego pliku XML. I tu zaczynają się problemy z kodowaniem znaków w bazie i pliku jaki chcemy wyprodukować.
Dane wczytane za pomocą ORM w jaki wyposażony jest web2py należy następnie w formie stringów przekazać do struktury opartej na obiekcie ElementTree, bo tak się najłatwiej w Pythonie tworzy struktury XML, przynajmniej moim zdaniem. A nawet jakby ktoś chciał to zrobić w inny sposób kluczowe jest przekazanie danych z bazy w postaci stringów. O bibliotece ElementTree napisze innym razem, tymczasem zajmijmy się problemem kodowania.
Strukturę XML przygotowany w ten czy inny sposób przetwarza się do postaci stringu w wybranym kodowaniu i zapisuje w pliku XML, który ma być plikiem wsadowym, na przykład do wspomnianego to programu RaksSQL. I to własnie ten moment ujawnił pewne problemy z kodowaniem. Pojawił się znany dobrze wielu programistom komunikat o błędzie:
UnicodeDecodeError: 'utf8' codec can't decode byte 0xf1 in position 6: invalid continuation byte
Albo przy próbie użycia innego kodowania:
UnicodeEncodeError: 'ascii' codec can't encode characters in position XXX: ordinal not in range(128)
Pierwsze co można znaleźć w sieci to informacje, że domyślne kodowanie nie jest zgodne z tym co chcemy ponownie zakodować, i trzeba je wymusić:
reload(sys) sys.setdefaultencoding('utf8')
Niestety w tym wypadku na próżno.
Inną podpowiedzią było by wymusić odkodowanie danych z bazy funkcją unicode. I to jest dobry trop. Tylko aby to zrobić trzeba wiedzieć jak te dane są zakodowane. Można przypuszczać, że dane w bazie MSSQL z definicji są zakodowane za pomocą “jedynie słusznego” z punktu widzenia MS standardu cp1250, a jednak nie. Wykonanie prostego zapytania pomaga ustalić jak jest naprawdę:
SELECT data_type, character_set_catalog, character_set_schema, character_set_name, collation_catalog, collation_schema, collation_name, COUNT(*) count FROM information_schema.columns GROUP BY data_type, character_set_catalog, character_set_schema, character_set_name, collation_catalog, collation_schema, collation_name;
Kolumny typu varchar rzeczywiście domyślnie zakodowane są w cp1250, ale kolumny typu nvarchar to już UNICODE. Zatem, aby zrobić porządek w produkowanym pliku tekstowym należy dekodować jedne dane za komendą:
unicode(string_z_bazy, 'cp1250')
a inne komendą:
unicode(string_z_bazy, 'utf-16')
w zależności z jakiego typu kolumną mamy do czynienia.
Dopiero po takim porządkowaniu danych można je w pliku tekstowym zakodować za pomocą wybranego przez siebie kodowania.
Porządek w bazie (jak już się narobiła bałaganu i ma się w tabelach zarówno varchar jak i nvarchar) można zrobić również na poziomie bazy tworząc widoki i używając funkcji CONVERT() na wybranych kolumnach by ujednolicić kodowanie.