Zanurkuj w Pythonie/Kodowanie znaków

Z Wikibooks, biblioteki wolnych podręczników.

Kodowanie znaków[edytuj]

W komputerze pewnym znakom odpowiadają pewne liczby, a kodowanie znaków określa, która liczba odpowiada jakiej literze. W łańcuchu znaków każdy symbol zajmuje 8 bitów, co daje nam do dyspozycji tylko 256 różnych symboli. Podstawowym systemem kodowania jest ASCII. Przyporządkowuje on liczbom z zakresu od 0 do 127 znaki alfabetu angielskiego, cyfry i niektóre inne symbole. Pozostałe standardowe systemy kodowania rozszerzają standard ASCII, dlatego znaki z przedziału od 0 do 127 w każdym systemie kodowania są takie same.

Przykład. Znaki jako liczby i na odwrót
>>> ord('a')                             #(1)
97
>>> chr(99)                             #(2)
'c'
>>> ord('%')
37                             #(3)
>>> chr(115)
's'
>>> chr(261)
Traceback (most recent call last):      #(4)
  File "<stdin>", line 1, in ?
ValueError: chr() arg not in range(261)
>>> chr(188)
'\xbc'                          #(5)

  1. Funkcja ord zwraca liczbę, która odpowiada danemu symbolowi. W tym przypadku literze "a" odpowiada liczba 97.
  2. Za pomocą funkcji chr dowiadujemy się, jaki znak odpowiada danej liczbie. Liczbie 99 odpowiada znak "c".
  3. Procent ("%") odpowiada liczbie 37.
  4. Każdy symbol odpowiada liczbie z zakresu od 0 do 255. Liczba 261 nie mieści się w jednym bajcie, dlatego wyskoczył nam wyjątek.
  5. Co prawda liczba 188 mieści się w 8-bitach, ale nie mieści się w standardzie ASCII i dlatego tego symbolu Python nie może jednoznacznie zinterpretować. W systemie kodowania ISO 8859-2 liczba ta odpowiada znakowi "ź", ale w systemie kodowania Windows-1250 (znany też jako CP-1250) znakowi "Ľ".

Każdy edytor tekstu zapisuje tworzone przez nas programy korzystając z jakiegoś kodowania, choćby z samego ASCII. Dobrze jest korzystać z edytora, który daje nam możliwość ustawienia kodowania znaków. Kiedy wiemy, w jakim systemie kodowania został zapisany nasz skrypt, powinniśmy jeszcze o tym poinformować Pythona.

Informowanie Pythona o kodowaniu znaków[edytuj]

Wróćmy do odbchelper.py. Na samym początku dodaliśmy linię[1]:

#-*- coding: utf-8 -*-

W ten sposób ustawiamy kodowanie znaków danego pliku, a nie całego programu (program może się składać z wielu plików). Zresztą, jeśli nie zdefiniujemy kodowania znaków, Python nas o tym uprzedzi:

sys:1: DeprecationWarning: Non-ASCII character '\xc5' in file test.py on line 5
but no encoding declared; see http://www.python.org/peps/pep-0263.html for detils

Jeśli skorzystaliśmy z innego kodowania znaków zamiast utf-8 oczywiście napiszemy coś innego. Dodając polskie znaki z reguły korzysta się z kodowania UTF-8 (obsługiwane przez wszystkie komputery) lub ISO-8859-2, a czasami w przypadku systemu Windows z Windows-1250 (lokalnie, tylko dla polskich komputerów i komputerów z Centralnej Europy).

Ale co wtedy, gdy nie mamy możliwości ustawić kodowania znaków i nie wiemy z jakiego korzysta nasz edytor? Można to sprawdzić metodą prób i błędów:

#-*- coding: {tu wstawiamy utf-8, iso-8859-2 lub windows-1250} -*-

print "zażółć gęślą jaźń"

A może pora zmienić edytor?

Unikod jeszcze raz[edytuj]

Jak wiemy, unikod jest systemem reprezentowania różnych znaków ze wszystkich języków świata.

Zaraz powrócimy do Pythona.

Notatka historyczna. Przed powstaniem unikodu istniały oddzielne systemy kodowania znaków dla każdego języka, a co przed chwilą trochę omówiliśmy. 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 jego 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 kataloguje wszystkie znaki pod indeksami od 0 do 0x10FFFF, a kodowanie UTF-8 reprezentuje te indeksy jako zestawy od 1 do 4 bajtów[2] Każdy bajt lub jednoznaczna sekwencja 2, 3 albo 4 bajtów 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.

7-bitowy ASCII koduje 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 ma 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). Numery znaków unikodu pokrywają się z wartościami bajtów 7-bitowego ASCII dla zakresu od 0 do 127, oraz ISO-8859-1 w zakresie od 128 do 255. Zgodność numerów kolejnych nie oznacza jednak, że bajty użyte do zapisania czy transmisji będą takie same. Kodowanie UTF-8 na przykład zapisuje znaki z zakresu numerów 128-255 na dwóch bajtach.

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.

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

Przykład. Unikod w Pythonie
>>> ord(u"ą")
261                          #(1)
>>> print unichr(378)           #(2)
ź

  1. W unikodzie polski znak "ą" jednoznacznie odpowiada liczbie 261.
  2. Za pomocą funkcji unichr, dowiadujemy się jakiemu znakowi odpowiada dana liczba. Liczbie 378 odpowiada polska litera "ź". Python automatycznie zakoduje wypisywany napis unikodowy, aby został poprawnie wyświetlony na naszym systemie.

Dlaczego warto korzystać z unikodu? Jest kilka powodów:

  • Unikod bardzo dobrze sobie radzi z różnymi międzynarodowymi znakami.
  • Reprezentacja unikodowa jest jednoznaczna; jednej liczbie odpowiada dokładnie jeden znak.
  • Nie musimy się zamartwiać szczegółami technicznymi np. czy dwa łańcuchy, które ze sobą łączymy są w takim samym systemie kodowania[3].
  • Python potrafi właściwie zinterpretować wszystkie znaki (np. co jest literą, co jest białym znakiem, a co jest cyfrą).
  • Korzystając z unikodu zapobiegamy wielu problemom.

Dlatego wszędzie, gdzie będziemy korzystali z polskich znaków, będziemy korzystali z unikodu.

Naprawianie polskich plików (pliki tekstowe, napisy...)[edytuj]

Aby naprawić plik zawierący tekst w formacie: «Wiêc wiedzia³em, ¿e ...» żeby naprawiony plik (UTF-8) wyglądał → «Więc wiedziałem, że ...» należy pisać w scripcie co następuje:

Python 2.x i Python 3.x
# -*- coding: utf-8 -*-
import codecs

try:
    # Python 2.x. Jesieli używamy Pythona > 2.x, bedzie exception.
    from tkFileDialog import askopenfilename
    from Tkinter import Tk, LabelFrame, Button, OptionMenu, StringVar
except ImportError:
    # Python 3.x
    from tkinter.filedialog import askopenfilename
    from tkinter import Tk, LabelFrame, Button, OptionMenu, StringVar

### Funkcja, która otwiera okienko po wybrania zestaw znaków
### Tylko w Python 3.x można ją nazywać wybierajZestawZnaków, więc zostawiamy wybierajZestawZnakow
def wybierajZestawZnakow(zestawieZnakow):
    gui = Tk()
    gui.resizable(0, 0)
    gui.title("")
    fra1 = LabelFrame(gui, text="Stary zestaw znaków")
    fra1.pack(padx=2, pady=2)
    var1 = StringVar()
    var1.set(zestawieZnakow[0])
    opt1 = OptionMenu(fra1, var1, *zestawieZnakow)
    opt1.pack(fill="x")
    but1 = Button(fra1, text="Otwieraj plik", command=lambda:gui.destroy())
    but1.pack(fill="x", padx=2, pady=2)
    gui.mainloop()
    return var1.get()

##Zaczyna się program

zestawieZnakow = ("windows-1250", "iso-8859-2", "windows-1252") # są inne kodowanie ...
stareKodowaniePliku = wybierajZestawZnakow(zestawieZnakow) #użytkownik wybiera kodowanie...

imiePlikuOryginalnego = askopenfilename() # użytkownik wybiera plik
plikOryginalny = codecs.open(imiePlikuOryginalnego, 'r', stareKodowaniePliku)

ostatkniaKropka = imiePlikuOryginalnego.rfind(".") #po ostatniej kropki zaczyna się rozszerzenie
imieNowegoPliku = imiePlikuOryginalnego[:ostatkniaKropka] + "_UTF-8"+imiePlikuOryginalnego[ostatkniaKropka:]

nowyPlik = codecs.open(imieNowegoPliku, 'w', 'utf-8')

for kreska in plikOryginalny.readlines():
    nowyPlik.write(kreska) # kreska "windows-1250 (albo inna)" --> do pliku UTF-8  => ąćęńłośżźĄĆĘŃŁÓŚŻŹ

plikOryginalny.close()
nowyPlik.close()
TYLKO Python 2.x (!!)
# -*- coding: utf-8 -*-
import codecs
import tkFileDialog
import sys

#1. Czytanie plik w kodowanie regionalnie (okienka TkFileDialog)
#2. Tworzenie nowegu pliku w uniwersalnym kodowanie (UTF-8)

stareKodowaniePliku = 'windows-1250' #regionalna; czasami może być 'iso-8859-2' (i są inne kodowanie dla innych regionów językowych)
reload(sys)
sys.setdefaultencoding( stareKodowaniePliku )

plikOryginalny = tkFileDialog.askopenfile(mode = 'r') # Plik z napisami (windows-1250) ¹æêñ³óœ¿Ÿ¥ÆÊÑ£ÓŒ¯

imieNowegoPliku = plikOryginalny.name[0:len(plikOryginalny.name)-4] + "_NOWY"+plikOryginalny.name[len(plikOryginalny.name)-4:]

nowyPlik = codecs.open(imieNowegoPliku, 'w', 'utf-8') # nowy plik z napisami jest UTF-8 (ąćęńłóśżźĄĆĘŃŁÓŚŻŹ)

for kreska in plikOryginalny.readlines():
    nowyPlik.write(kreska.encode(stareKodowaniePliku)) # kreska "windows-1250" --> do pliku UTF-8  (= ąćęńłóśżźĄĆĘŃŁÓŚŻŹ)

plikOryginalny.close()
nowyPlik.close()

Materiały dodatkowe[edytuj]

  • PEP 0263 wyjaśnia, w jaki sposób skonfigurować kodowanie kodu źródłowego.

Przypisy

  1. W tym podręczniku będziemy korzystać z kodowania UTF-8
  2. Istotne rozróżnienie: Unicode to katalog znaków, nadający im numery; UTF-8 to kodowanie tych numerów za pomocą jednoznacznych sekwencji bajtów
  3. W szczególności może się to zrobić niewygodne, kiedy korzystamy tylko ze standardowych łańcuchów znaków, a współpracujące ze sobą moduły korzystają z różnych systemów kodowania np. jedne z ISO 8859-2, a inne z UTF-8.