Zanurkuj w Pythonie/Unikod

Z Wikibooks, biblioteki wolnych podręczników.

Unikod[edytuj]

Unikod jest systemem reprezentowania znaków ze wszystkich różnych języków świata. Kiedy Python parsuje dokument XML, wszystkie dane przechowuje w pamięci w postaci unikodu.

Powrócimy do Pythona za minutkę.

Notatka historyczna. Przed unikodem istniały oddzielne systemy kodowania znaków dla każdego języka. Każdy z nich korzystał z tych samych liczb (0-255) do reprezentowania znaków danego języka. Niektóre języki (jak rosyjski) miały wiele sprzecznych standardów reprezentowania tych samych znaków. Inne języki (np. japoński) posiadają tak wiele znaków, że wymagają wielu bajtów, aby zapisać cały zbiór ich znaków. Wymiana dokumentów pomiędzy tymi systemami była trudna, ponieważ komputer nie mógł stwierdzić, z którego systemu kodowania skorzystał autor. Komputer widział tylko liczby, a liczby mogą oznaczać różne rzeczy. Zaczęto się zastanawiać nad przechowywaniem tych dokumentów w tym samym miejscu (np. w tej samej tabeli bazy danych); trzeba było przechowywać informacje o kodowaniu każdego kawałku tekstu, a także trzeba było za każdym razem informować o kodowaniu przekazywanego tekstu. Wtedy też zaczęto myśleć o wielojęzycznych dokumentach, w których znaki z wielu języków znajdują się w tym samym dokumencie. (Wykorzystywały one zazwyczaj kod ucieczki, aby przełączyć tryb kodowania; ciach, jesteś w rosyjskim trybie, więc 241 znaczy to; ciach, jesteś w greckim trybie, więc 241 znaczy coś innego itd.) Unikod został zaprojektowany po to, aby rozwiązywać tego typu problemy.

Aby rozwiązać te problemy, unikod reprezentuje wszystkie znaki jako 2-bajtowe liczby, od 0 do 65535[1]. Każda 2-bajtowa liczba reprezentuje unikalny znak, który jest wykorzystywany w co najmniej jednym języku świata. (Znaki które są wykorzystywane w wielu językach świata, mają ten sam kod numeryczny.) Mamy dokładnie jedną liczbę na znak i dokładnie jeden znak na liczbę. Dane unikodu nigdy nie są dwuznaczne.

Oczywiście te stare systemy kodowania znaków ciągle istnieją. Na przykład 7-bitowy ASCII, który przechowuje wszystkie angielskie znaki za pomocą liczb z zakresu od 0 do 127. (65 jest wielką literą “A”, 97 jest małą literą “a” itd.) Język angielski posiada bardzo prosty alfabet, więc może się całkowicie zmieścić w 7-bitowym ASCII. Języki zachodniej Europy jak język francuski, hiszpański, czy też niemiecki, korzystają z systemu kodowania nazwanego ISO-8859-1 (inne określenie to “latin-1”), które korzysta z 7-bitowych znaków ASCII dla liczb od 0 do 127, ale rozszerza zakres 128-255 dla znaków typu "ñ" (241), czy ü (252). Także unikod wykorzystuje te same znaki jak 7-bitowy ASCII dla zakresu od 0 do 127, a także te same znaki jak ISO-8859-1 w zakresie od 128 do 255, a potem w zakresie od 256 do 65535 rozszerza znaki do innych języków.

Kiedy korzystamy z danych w postaci unikodu, może zajść potrzeba przekonwertowania danych na jakiś inny system kodowania np. gdy potrzebujemy współdziałać z innym komputerowym systemem, a który oczekuje danych w określonym 1-bajtowym systemie kodowania, czy też wysłać dane na terminal, który nie obsługuje unikodu, czy też do drukarki. Może zajść potrzeba przekonwertowania danych, gdy przechowujemy je w dokumencie XML z wyraźnie określonym systemem kodowania.

I po tej notatce, powróćmy do Pythona.

Python obsługuje unikod od wersji 2.0. Pakiet XML wykorzystuje unicode do przechowywania wszystkich przetworzonych danych, a Ty możesz używać unikodu gdziekolwiek chcesz.

Przykład 9.13. Wprowadzenie do unikodu

>>> s = u'Dive in'            #(1)
>>> s
u'Dive in'
>>> print s                   #(2)
Dive in
  1. Aby utworzyć napis w unikodzie zamiast w normalnym ASCII, dodajemy znak „u” przed napisem. Zauważ, że napis ten nie posiada żadnego znaku nie należącego do ASCII. To świetnie; unikod jest nadzbiorem ASCII (jest nawet bardzo dużym nadzbiorem), więc dowolny napis ASCII może być przechowywany jako unikod.
  2. Kiedy wypisujemy napis, Python będzie próbował przekonwertować go na domyślny system kodowania, zazwyczaj ASCII. (Więcej o tym powiemy za chwilkę.) Ponieważ ten unikodowy napis składa się ze znaków będących także znakami ASCII, wypisanie go da taki sam wynik, co wypisanie normalnego napisu ASCII. Konwersja jest nierozróżnialna i gdybyś nie wiedział, że to jest unikodowy napis, nigdy nie dostrzegłbyś różnicy.

Przykład 9.14. Przechowywanie znaków nie należących do ASCII

>>> s = u'La Pe\xf1a'         #(1)
>>> print s                   #(2)
Traceback (innermost last):
  File "<interactive input>", line 1, in ?
UnicodeError: ASCII encoding error: ordinal not in range(128)
>>> print s.encode('latin-1') #(3)
La Peña
  1. Oczywiście prawdziwą zaletą jest możliwość przechowywania znaków, które nie należą do ASCII np. hiszpańskie “ñ” (n z tyldą nad sobą). Unikodowym kodem znaku dla n z tyldą jest 0xf1 w systemie szesnastkowym (241 w dziesiętnym), który możesz zapisać w ten sposób: \xf1.
  2. Wcześniej powiedzieliśmy, że funkcja print stara się przekonwertować napis unicodowy na ASCII, więc dlaczego napis nie został wypisany? Tutaj to nie działa, ponieważ nasz unikodowy napis zawiera znaki nie należące do ASCII, dlatego też Python zgłasza błąd UnicodeError.
  3. Tutaj przychodzi z pomocą konwersja z unikodu do innego systemu kodowania. s jest unikodowym napisem, lecz my możemy wyświetlić tylko zwykły napis. Aby rozwiązać ten problem, wywołujemy metodę encode, dostępną z każdego unikodowego napisu, w celu przekonwertowania go do zwykłego napisu. Przekazujemy przy tym jako parametr, system kodowania, na który chcemy zakodować napis. W tym przypadku korzystamy z latin-1 (nazywanego także iso-8859-1), który dołącza znak n z tyldą (natomiast domyślny system kodowania, ASCII, nie posiada tego znaku, ponieważ w ASCII występuje znaki numerowane od 0 do 127).

Pamiętasz, jak powiedzieliśmy, że Python zazwyczaj konwertuje unikod do ASCII, gdy tylko potrzebuje utworzyć zwykły napis z unikodowego? Ten domyślny system kodowania jest opcją, którą możemy ustawić.

Przykład 9.15. sitecustomize.py

 # sitecustomize.py                   #(1)
 # this file can be anywhere in your Python path,
 # but it usually goes in ${pythondir}/lib/site-packages/
 import sys
 sys.setdefaultencoding('iso-8859-1') #(2)
  1. sitecustomize.py jest specjalnym skryptem. Python będzie go próbował zaimportować zaraz przy starcie, dlatego dowolny kod zawarty w nim będzie uruchamiany automatycznie. Może się on znajdować gdziekolwiek (aby tylko instrukcja import mogła go znaleźć), ale zazwyczaj jest w katalogu site-packages, w katalogu lib instalacji Pythona.
  2. Funkcja setdefaultencoding ustawia domyślne kodowanie. Będzie to system kodowania, z którego będzie próbował korzystać Python, gdy zajdzie potrzeba automatycznego przekonwertowania napisu w unikodzie na zwykły napis.

Przykład 9.16. Rezultat ustawienia domyślnego kodowania

>>> import sys
>>> sys.getdefaultencoding() #(1)
'iso-8859-1'
>>> s = u'La Pe\xf1a'
>>> print s                  #(2)
La Peña
  1. Przykład ten zakłada, że wykonałeś zmiany przedstawione w poprzednim przykładzie, który odnosił się do pliku sitecustomize.py, a także zrestartowałeś Pythona. Jeśli Twoje domyślne kodowanie nadal mówi 'ascii', to nie ustawiłeś sobie poprawnie sitecustomize.py lub nie zrestartowałeś Pythona. Domyślne kodowanie może zostać zmienione tylko podczas startu Pythona, nie może zostać zmienione później.
  2. Teraz domyślny system kodowania dołącza wszystkie znaki, które wykorzystaliśmy w napisie. Python nie ma problemów z automatycznym zakodowaniem napisu i wyświetleniem go.

Przykład 9.17. Określanie kodowania w pliku .py

Jeśli zamierzasz przechowywać znaki nienależące do ASCII w kodzie Pythona, powinieneś określić system kodowania każdego pliku .py poprzez wstawienie deklaracji kodowania na górze każdego pliku. Ta deklaracja definiuje, że plik .py został zakodowany w UTF-8:

 #!/usr/bin/env python
 # -*- coding: UTF-8 -*-

A jak z XML-em? Każdy dokument XML jest zapisany w określonym systemie kodowania. Ponownie, ISO-8859-1 jest popularnym systemem kodowania danych dla języków zachodniej Europy. KOI8-R jest popularny dla tekstów rosyjskich. System kodowania, jeśli jest określony, znajduje się w nagłówku dokumentu XML.

Przykład 9.18. russiansample.xml

 <?xml version="1.0" encoding="koi8-r"?>       #(1)
 <preface>
 <title>Предисловие</title>                    #(2)
 </preface>
  1. Jest to prosty wyciąg z prawdziwego rosyjskiego dokumentu XML. Jest to część rosyjskiego tłumaczenia tej oto książki. Dodajmy, że system kodowania, koi8-r, jest określony w nagłówku.
  2. Te znaki cyrylicy przetłumaczymy z rosyjskiego na „Przedmowa”. Jeśli otworzysz ten plik w zwykłym edytorze tekstu, znaki te najprawdopodobniej będą wyglądały jak śmietnik, ponieważ zostały one zakodowane jako koi8-r, ale są wyświetlane, jakby były zakodowane w systemie iso-8859-1.

Przykład 9.19. Parsowanie russiansample.xml

>>> from xml.dom import minidom
>>> xmldoc = minidom.parse('russiansample.xml') #(1)
>>> title = xmldoc.getElementsByTagName('title')[0].firstChild.data
>>> title                                       #(2)
u'\u041f\u0440\u0435\u0434\u0438\u0441\u043b\u043e\u0432\u0438\u0435'
>>> print title                                 #(3)
Traceback (innermost last):
  File "<interactive input>", line 1, in ?
UnicodeError: ASCII encoding error: ordinal not in range(128)
>>> convertedtitle = title.encode('koi8-r')     #(4)
>>> convertedtitle
'\xf0\xd2\xc5\xc4\xc9\xd3\xcc\xcf\xd7\xc9\xc5'
>>> print convertedtitle                        #(5)
Предисловие
  1. Zakładamy, że zapisałeś poprzedni przykład w bieżącym katalogu jako russiansample.xml. Zakładamy też, że zmieniłeś swoje domyślne kodowanie z powrotem na 'ascii' poprzez usunięcie swojego pliku sitecustomize.py lub co najmniej zakomentowałeś linie z setdefaultencoding.
  2. Zauważmy, że tekst danych zawartych w znaczniku title (a teraz też w zmiennej title, dzięki wywołaniu długiego ciągu funkcji Pythona, które niestety omówimy w następnej sekcji), czyli tekst zawarty wewnątrz dokumentu XML, jest przechowywany w unikodzie.
  3. Wypisanie zmiennej title nie jest możliwe, ponieważ ten unicodowy napis posiada znaki nienależące do ASCII, dlatego też Python nie może przekonwertować ich do ASCII, bo nie miałoby to sensu.
  4. Jakkolwiek można wyraźnie przekonwertować go do koi8-r, dzięki któremu otrzymamy (zwykły, nieunikodowy) napis składający się ze jednobajtowych znaków (f0, d2, c5 itd.), a które są odpowiednikiem znaków unikodowych w systemie kodowania koi8-r.
  5. Wypisując napis zakodowany jako koi8-r, prawdopodobnie zobaczymy śmieci na swoim ekranie, ponieważ Python IDE prawdopodobnie będzie interpretował te dane jako iso-8859-1, a nie koi8-r. Ale przynajmniej zostanie coś wypisane. (Gdy dobrze się przyglądniesz, te same śmieci zobaczysz, jeśli otworzysz oryginalny dokument XML w edytorze tekstu nieobsługującym unikod. Python podczas parsowania przekonwertował ten dokument z systemu koi8-r na unikod, a my z powrotem go przekonwertowaliśmy do koi8-r.)

Podsumowując, jeśli nie widziałeś wcześniej unikod, to wydaje się trochę straszny. Jednak dane przechowywane w postaci unikodu naprawdę łatwo się obsługuje w Pythonie. Gdyby wszystkie znaki dokumentu XML należały do 7-bitowego ASCII (jak przykłady w tym rozdziale), nigdy byś nie myślał o unikodzie. Python konwertuje dane w systemie ASCII podczas parsowania dokumentu XML na unikode, a także automatycznie zakodowuje z powrotem do ASCII, jeśli zajdzie taka potrzeba, a Ty nawet nie musisz w żaden sposób go instruować. Jednak, jeśli potrzebujesz się zajmować innymi językami, Python jest dobrze przygotowany.

Przypisy

  1. Niestety ciągle jest to nadmiernym uproszczeniem. Unikod został rozszerzony, aby obsługiwać teksty w starożytnych odmianach chińskiego, koreańskiego, czy japońskiego, ale one mają tak dużo znaków, że 2-bajtowy system nie może reprezentować ich wszystkich.