Bu bölüme gelinceye kadar Python programlama dilindeki karakter dizisi, liste ve dosya adlı veri tiplerine ilişkin epey söz söyledik. Artık bu veri tiplerine dair hemen hemen bütün ayrıntıları biliyoruz.
Ancak henüz öğrenmediğimiz, ama programcılık maceramız açısından mutlaka öğrenmemiz gereken çok önemli bir konu daha var. Bu önemli konunun adı, karakter kodlama.
Hatırlarsanız önceki derslerimizde karakter dizilerinin encode()
adlı bir metodu olduğundan söz etmiştik.
İşte bu bölümde, o zaman henüz bilgimiz yetersiz olduğu için ertelediğimiz bu encoding konusunu bütün ayrıntılarıyla ele alacağız ve yazdığımız programlarda Türkçe karakterleri kullanırken neden sorunlarla karşılaştığımızı, bu sorunun temelinde neyin yattığını anlamaya çalışacağız.
Giriş
Önceki bölümlerde sık sık tekrar ettiğimiz gibi, bilgisayar dediğimiz şey, üzerinden elektrik geçen devrelerden oluşmuş bir sistemdir. Eğer bir devrede elektrik yoksa o devrenin değeri 0 volt iken, o devreden elektrik geçtiğinde devrenin değeri yaklaşık +5 volttur.
Peki bir bilgisayar yalnızca elektrik sinyallerinden anlıyorsa, biz mesela bilgisayarları nasıl oluyor da metin girişi için kullanabiliyoruz?
Bu sorunun cevabı aslında çok açık: Birtakım elektrik sinyallerini, birtakım aritmetik işlemleri gerçekleştirebilmek amacıyla nasıl birtakım sayılar halinde kodlayabiliyorsak; birtakım sayıları da, birtakım metin işlemlerini gerçekleştirebilmek amacıyla birtakım karakterler halinde kodlayabiliriz.
Peki ama nasıl?
Bilgisayarlarda da hangi elektrik sinyalinin hangi sayıya; hangi sayının da hangi karaktere karşılık geleceğini belirleyebiliriz. Daha doğrusu, bilgisayarların gördüğü bu elektrik sinyallerini sayılara ve karakterlere dönüştürebiliriz.
Dışarıdan girilen karakterleri de, bilgisayarların anlayabilmesi için tam aksi istikamette sayıya, oradan da elektrik sinyallerine çevirebiliriz. İşte bu dönüştürme işlemine karakter kodlama (character encoding) adı verilir.
1960’lı yılların başında IBM şirketinde çalışan Bob Bemer adlı bir bilim adamı, herkes tarafından benimsenecek ortak bir karakter kodlama sistemi üzerinde ilk çalışmaları başlattı. İşte ASCII (‘aski’ okunur) böylece hayatımıza girmiş oldu.
Peki bu ‘ASCII’ denen şey tam olarak ne anlama geliyor? Gelin bu sorunun cevabını, en baştan başlayarak ve olabildiğince ayrıntılı bir şekilde vermeye çalışalım.
ASCII
Bob Bemer ve ekibi hangi sayıların hangi karakterlere karşılık geleceğini belli bir standarda bağlayan bir tablo oluşturdu. Bu standarda ise American Standard Code for Information Interchange, yani ‘Bilgi Alışverişi için Standart Amerikan Kodu’ veya kısaca ‘ASCII’ adı verildi.
7 Bitlik bir Sistem
ASCII adı verilen sistem, birtakım sayıların birtakım karakterlerle eşleştirildiği basit bir tablodan ibarettir. Bu tabloyu http://www.asciitable.com/ adresinde görebilirsiniz:
İsterseniz bu tabloyu Python yardımıyla kendiniz de oluşturabilirsiniz:
for i in range(128):
if i % 4 == 0:
print("\n")
print("{:<3}{:>8}\t".format(i, repr(chr(i))), sep="", end="")
Bu kodlarda repr()
fonksiyonu dışında bilmediğiniz ve anlayamayacağınız hiçbir şey yok. Biraz sonra repr()
fonksiyonundan da bahsedeceğiz.
ASCII tablosunda toplam 128 karakterin sayılarla eşleştirilmiş durumda olduğunu görüyorsunuz. 128 adet sayı 7 bite karşılık gelir (2**7=128
). Yani 7 bit ile gösterilebilecek son sayı 127’dir. Dolayısıyla ASCII 7 bitlik bir sistemdir.
ASCII tablosunu şöyle bir incelediğimizde ilk 32 öğenin göze ilk başta anlamsız görünen birtakım karakterlerden oluştuğunu görüyoruz:
sayı | karakter | sayı | karakter | sayı | karakter | sayı | karakter |
---|---|---|---|---|---|---|---|
0 | ‘\x00’ | 1 | ‘\x01’ | 2 | ‘\x02’ | 3 | ‘\x03’ |
4 | ‘\x04’ | 5 | ‘\x05’ | 6 | ‘\x06’ | 7 | ‘\x07’ |
8 | ‘\x08’ | 9 | ‘\t’ | 10 | ‘\n’ | 11 | ‘\x0b’ |
12 | ‘\x0c’ | 13 | ‘\r’ | 14 | ‘\x0e’ | 15 | ‘\x0f’ |
16 | ‘\x10’ | 17 | ‘\x11’ | 18 | ‘\x12’ | 19 | ‘\x13’ |
20 | ‘\x14’ | 21 | ‘\x15’ | 22 | ‘\x16’ | 23 | ‘\x17’ |
24 | ‘\x18’ | 25 | ‘\x19’ | 26 | ‘\x1a’ | 27 | ‘\x1b’ |
28 | ‘\x1c’ | 29 | ‘\x1d’ | 30 | ‘\x1e’ | 31 | ‘\x1f’ |
Mesela ekranda görüntülenebilen ‘a’, ‘b’, ‘c’, ‘!’, ‘?’, ‘=’ gibi karakterlerden farklı olarak bu ilk 32 karakter ekranda görünmez. Bunlara aynı zamanda ‘kontrol karakterleri’ (control characters) adı da verilir.
Çünkü bu karakterler ekranda görüntülenmek yerine, metnin akışını kontrol eder. Bu karakterlerin ne işe yaradığını şu tabloyla tek tek gösterebiliriz
(tablo http://tr.wikipedia.org/wiki/ASCII adresinden alıntıdır):
Sayı | Karakter | Sayı | Karakter |
---|---|---|---|
0 | boş | 16 | veri bağlantısından çık |
1 | başlık başlangıcı | 17 | aygıt denetimi 1 |
2 | metin başlangıcı | 18 | aygıt denetimi 2 |
3 | metin sonu | 19 | aygıt denetimi 3 |
4 | aktarım sonu | 20 | aygıt denetimi 4 |
5 | sorgu | 21 | olumsuz bildirim |
6 | bildirim | 22 | zaman uyumlu boşta kalma |
7 | zil | 23 | aktarım bloğu sonu |
8 | geri al | 24 | iptal |
9 | yatay sekme | 25 | ortam sonu |
10 | satır besleme/yeni satır | 26 | değiştir |
11 | dikey sekme | 27 | çık |
12 | form besleme/yeni sayfa | 28 | dosya ayırıcısı |
13 | satır başı | 29 | grup ayırıcısı |
14 | dışarı kaydır | 30 | kayıt ayırıcısı |
15 | içeri kaydır | 31 | birim ayırıcısı |
Gördüğünüz gibi, bunlar birer harf, sayı veya noktalama işareti değil. O yüzden bu karakterler ekranda görünmez. Ama bir metindeki veri, satır ve paragraf düzeninin nasıl olacağını, metnin nerede başlayıp nerede biteceğini ve nasıl görüneceğini kontrol ettikleri için önemlidirler.
Geri kalan sayılar ise doğrudan karakterlere, sayılara ve noktalama işaretlerine tahsis edilmiştir:
sayı | karakter | sayı | karakter | sayı | karakter | sayı | karakter |
---|---|---|---|---|---|---|---|
32 | ‘ ‘ | 33 | ‘!’ | 34 | ‘”’ | 35 | ‘#’ |
36 | ‘$’ | 37 | ‘%’ | 38 | ‘&’ | 39 | “’” |
40 | ‘(‘ | 41 | ‘)’ | 42 | ‘*’ | 43 | ‘+’ |
44 | ‘,’ | 45 | ‘-‘ | 46 | ‘.’ | 47 | ‘/’ |
48 | ‘0’ | 49 | ‘1’ | 50 | ‘2’ | 51 | ‘3’ |
52 | ‘4’ | 53 | ‘5’ | 54 | ‘6’ | 55 | ‘7’ |
56 | ‘8’ | 57 | ‘9’ | 58 | ‘:’ | 59 | ‘;’ |
60 | ‘<’ | 61 | ‘=’ | 62 | ‘>’ | 63 | ‘?’ |
64 | ‘@’ | 65 | ‘A’ | 66 | ‘B’ | 67 | ‘C’ |
68 | ‘D’ | 69 | ‘E’ | 70 | ‘F’ | 71 | ‘G’ |
72 | ‘H’ | 73 | ‘I’ | 74 | ‘J’ | 75 | ‘K’ |
76 | ‘L’ | 77 | ‘M’ | 78 | ‘N’ | 79 | ‘O’ |
80 | ‘P’ | 81 | ‘Q’ | 82 | ‘R’ | 83 | ‘S’ |
84 | ‘T’ | 85 | ‘U’ | 86 | ‘V’ | 87 | ‘W’ |
88 | ‘X’ | 89 | ‘Y’ | 90 | ‘Z’ | 91 | ‘[‘ |
92 | ‘\’ | 93 | ‘]’ | 94 | ‘^’ | 95 | ‘_’ |
96 | ‘’’ | 97 | ‘a’ | 98 | ‘b’ | 99 | ‘c’ |
100 | ‘d’ | 101 | ‘e’ | 102 | ‘f’ | 103 | ‘g’ |
104 | ‘h’ | 105 | ‘i’ | 106 | ‘j’ | 107 | ‘k’ |
108 | ‘l’ | 109 | ‘m’ | 110 | ‘n’ | 111 | ‘o’ |
112 | ‘p’ | 113 | ‘q’ | 114 | ‘r’ | 115 | ‘s’ |
116 | ‘t’ | 117 | ‘u’ | 118 | ‘v’ | 119 | ‘w’ |
120 | ‘x’ | 121 | ‘y’ | 122 | ‘z’ | 123 | ‘{‘ |
124 | ‘|’ | 125 | ‘}’ | 126 | ‘~’ | 127 | ‘x7f’ |
Adından da anlaşılacağı gibi, ASCII bir Amerikan standardıdır. Dolayısıyla hazırlanışında İngilizce temel alınmıştır. Zaten ASCII tablosunu incelediğinizde, bu tabloda Türkçeye özgü harflerin bulunmadığını göreceksiniz.
Bu sebepten, bu standart ile mesela Türkçeye özgü karakterleri gösteremeyiz. Çünkü ASCII standardında ‘ş’, ‘ç’, ‘ğ’ gibi harfler kodlanmamıştır. Özellikle Python’ın 2.x serisini kullanmış olanlar, ASCII’nin bu yetersizliğinin nelere sebep olduğunu gayet iyi bilir. Python’ın 2.x serisinde mesela doğrudan şöyle bir kod yazamayız:
print("Merhaba Şirin Baba!")
“Merhaba Şirin Baba! adlı karakter dizisinde geçen ‘Ş’ harfi ASCII dışı bir karakterdir. Yani bu harf ASCII ile temsil edilemez. O yüzden böyle bir kod yazıp bu kodu çalıştırdığımızda Python bize şöyle bir hata mesajı gösterecektir:
File "deneme.py", line 1
SyntaxError: Non-ASCII character '\xde' in file deneme.py on line 1, but no
encoding declared; see http://www.python.org/peps/pep-0263.html for details
Sözün özü, eğer yazdığınız veya kendiniz yazmamış da olsanız herhangi bir sebeple kullanmakta olduğunuz bir programda Türkçe karakterlere ilişkin bir hata alıyorsanız, bu durumun en muhtemel sebebi, kullandığınız programın veya sistemin, doğrudan ASCII’yi veya ASCII’ye benzer başka bir sistemi temel alarak çalışıyor olmasıdır.
Genişletilmiş ASCII
Dediğimiz gibi, ASCII 7 bitlik bir karakter kümesidir. Bu standardın ilk çıktığı dönemde 8. bitin hata kontrolü için kullanıldığını söylemiştik. Sonraki yıllarda 8. bitin hata kontrolü için kullanılmasından vazgeçildi. Böylece 8. bit yine boşa düşmüş oldu.
Microsoft şirketinin Türkiye’ye gönderdiği bilgisayarlarda tanımlı ‘cp857’ adlı kod sayfasında 128 ile 256 aralığında Türkçe karakterlere de yer verilmişti (bkz. http://msdn.microsoft.com/en-us/library/cc195068.aspx)
Bu tabloya baktığınızda baştan 128’e kadar olan karakterlerin standart ASCII tablosu ile aynı olduğunu göreceksiniz. 128. karakterden itibaren ise Türkçeye özgü harfler tanımlanır. Mesela bu tabloda 128. karakter Türkçedeki büyük ‘ç’ harfi iken, 159. karakter küçük ‘ş’ harfidir. Bu durumu şu Python kodları ile de teyit edebilirsiniz:
>>> "Ç".encode("cp857")
b'\x80'
>>> "ş".encode("cp857")
b'\x9f'
Bu arada bu sayıların onaltılı sayma düzenine göre olduğunu biliyorsunuz. Onlu düzende bunların karşılığı sırasıyla şudur:
>>> int("80", 16)
128
>>> int("9f", 16)
159
cp857 numaralı kod sayfasında ‘Ç’ ve ‘ş’ harfleri yer aldığı için, biz bu harfleri o kod sayfasına göre kodlayabiliyoruz. Ama mesela ASCII kodlama sisteminde bu harfler bulunmaz. O yüzden bu harfleri ASCII sistemine göre kodlayamayız:
>>> "Ç".encode("ascii")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode character '\xc7' in position
0: ordinal not in range(128)
Tıpkı hata mesajında da söylendiği gibi:
Unicode Kodlama Hatası: 'ascii' kod çözücüsü, 0 konumundaki '\xc7' adlı
karakteri kodlayamıyor. Sayı 0-128 aralığında değil.
Bütün bu anlattıklarımızdan şu sonucu çıkarıyoruz: ASCII bilgisayarlar arasında güvenli bir şekilde veri aktarımını sağlamak için atılmış en önemli ve en başarılı adımlardan bir tanesidir. Bu güçlü standart sayesinde uzun yıllar bilgisayarlar arası temel iletişim başarıyla sağlandı. Ancak bu standardın zayıf kaldığı nokta 7 bitlik olması ve boşta kalan 8. bitin tek başına dünyadaki bütün dilleri temsil etmeye yeterli olmamasıdır.
1 Karakter == 1 Bayt
ASCII standardı, her karakterin 1 bayt ile temsil edilebileceği varsayımı üzerine kurulmuştur. Bildiğiniz gibi, 1 bayt (geleneksel olarak) 8 bit’e karşılık gelir. Peki 1 bayt’ın 8 bit’e karşılık gelmesinin nedeni nedir?
Aslında bunun özel bir nedeni yok. 1 destede neden 10 öğe, 1 düzinede de 12 öğe varsa, 1 bayt’ta da 8 bit vardır…
Dediğimiz gibi ASCII standardı 7 bitlik bir sistemdir. Yani bu standartta en büyük sayı olan 127 yalnızca 7 bit ile gösterilebilir:
>>> bin(127)[2:]
'1111111'
127 sayısı 7 bit ile gösterilebilecek son sayıdır:
>>> (127).bit_length()
7
>>> (128).bit_length()
8
8 bitlik bir sistem olan Genişletilmiş ASCII ise 0 ile 255 arası sayıları temsil edebilir:
>>> bin(255)[2:]
'11111111'
255 sayısı 8 bit ile gösterilebilecek son sayıdır:
>>> (255).bit_length()
8
>>> (256).bit_length()
9
Dolayısıyla ASCII’de ve Genişletilmiş ASCII’de 1 baytlık alana toplam 256 karakter sığdırılabilir. Eğer daha fazla karakteri temsil etmek isterseniz 1 bayttan fazla bir alana ihtiyaç duyarsınız.
UNICODE
Türkiye’den gönderilen bir metin (örneğin bir e.posta) Almanya’daki bilgisayarlarda düzgün görüntülenemeyebiliyordu.
Örneğin Windows-1254 (cp1254) numaralı kod sayfası ile kodlanmış Türkçe bir metin, Almanya’da Windows-1250 numaralı kod sayfasının tanımlı olduğu bir bilgisayarda, aynı sayıların her iki kod sayfasında farklı karakterlere karşılık gelmesi nedeniyle düzgün görüntülenemez.
Windows-1254 adlı kod sayfası için http://en.wikipedia.org/wiki/Windows-1254 adresine;
Windows-1250 adlı kod sayfası için ise http://en.wikipedia.org/wiki/Windows-1250 adresine bakabilirsiniz.
İşte nasıl 1960’lı yılların başında Bob Bemer ve arkadaşları bilgisayarlar arasında sağlıklı bir veri iletişimi sağlamak için kolları sıvayıp ASCII gibi bir çözüm ürettiyse, ASCII ve Genişletilmiş ASCII ile kodlanamayan karakterleri de kodlayıp, uluslar arasında çok geniş çaplı veri alışverişine izin verebilmek amacıyla Xerox şirketinden Joe Becker, Apple şirketinden ise Lee Collins ve Mark Davis UNICODE adlı bir çözüm üzerinde ilk çalışmaları başlattı.
Peki tam olarak nedir bu UNICODE denen şey?
Aslında Unicode da tıpkı ASCII gibi bir standarttır. Unicode’un bir proje olarak ortaya çıkışı 1987 yılına dayanır. Projenin amacı, dünyadaki bütün dillerde yer alan karakterlerin tek, benzersiz ve doğru bir biçimde temsil edilebilmesidir. Yani bu projenin ortaya çıkış gayesi, ASCII’nin yetersiz kaldığı noktaları telafi etmektir.
Sınırsız Bitlik bir Sistem
Unicode standardı ile ilgili olarak bilmemiz gereken ilk şey bu standardın ASCII’yi tamamen görmezden gelmiyor olmasıdır. Üstelik ASCII standardı yaygın olarak kullanılmaya da devam etmektedir.
Bu sebeple ASCII ile halihazırda kodlanmış karakterler UNICODE standardında da aynı şekilde kodlanmıştır. Dolayısıyla ASCII UNICODE sisteminin bir alt kümesi olduğu için, ASCII ile uyumlu olan bütün sistemler otomatik olarak UNICODE ile de uyumludur.
UNICODE’un ASCII’den en önemli farkı, UNICODE’un ASCII’ye kıyasla çok daha büyük miktarda karakterin kodlanmasına izin vermesidir. ASCII yalnızca 128 karakterin kodlanmasına izin verirken UNICODE 1.000.000’dan fazla karakterin kodlanmasına izin verir.
Unicode sisteminde her karakter tek ve benzersiz bir ‘kod konumuna’ (code point) karşılık gelir. Kod konumları şu formüle göre gösterilir:
U+sayının_onaltılı_değeri
Örneğin ‘a’ harfinin kod konumu şudur:
u+0061
Buradaki 0061 sayısı onaltılı bir sayıdır. Bunu onlu sayı sistemine çevirebilirsiniz:
>>> int("61", 16)
97
Hatırlarsanız ‘a’ harfinin ASCII tablosundaki karşılığı da 97 idi.
Unicode standardına http://www.unicode.org/versions/Unicode6.2.0/UnicodeStandard-6.2.pdf adresinden ulaşabilirsiniz.
UTF-8 Kod Çözücüsü
Dediğimiz gibi UNICODE devasa bir tablodan ibarettir. Bu tabloda karakterlere ilişkin birtakım bilgiler bulunur ve bu sistemde her karakter, kod konumları ile ifade edilir. UNICODE kendi başına karakterleri kodlamaz. Bu sistemde tanımlanan karakterleri kodlama işi kod çözücülerin görevidir.
UNICODE sistemi içinde UTF-1, UTF-7, UTF-8, UTF-16 ve UTF-32 adlı kod çözücüler bulunur. UTF-8, UNICODE sistemi içindeki en yaygın, en bilinen ve en kullanışlı kod çözücüdür.
UTF-8 adlı kod çözücünün kodlayabildiği karakterlerin listesine http://www.fileformat.info/info/charset/UTF-8/list.htm adresinden ulaşabilirsiniz.
1 Karakter != 1 Bayt
ASCII sisteminde her karakterin 1 bayt’a karşılık geldiğini söylemiştik. Ancak 1 bayt dünyadaki bütün karakterleri kodlamaya yetmez. Geri kalan karakterleri de kodlayabilmek için 1 bayttan fazlasına ihtiyacımız var. Mesela karakter kodlama için:
1 bayt kullanırsak toplam 2**8 = 256
2 bayt kullanırsak toplam 2**16 = 65,536
3 bayt kullanırsak toplam 2**24 = 16,777,216
4 bayt kullanırsak toplam 2**32 = 4,294,967,296
karakter kodlayabiliriz. Bu durumu şu Python kodları ile de gösterebiliriz:
>>> for i in range(1, 5):
... print("{} bayt kullanırsak toplam 2**{:<2} = {:,}".format(i, i*8, (2**(i*8))))
UNICODE sistemi içindeki UTF-8 adlı kod çözücü, karakterleri değişken sayıda baytlar halinde kodlayabilir. UTF-8, UNICODE sistemi içinde tanımlanmış karakterleri kodlayabilmek için 1 ile 4 bayt arası değerleri kullanır. Böylece de bu kod çözücü UNICODE sistemi içinde tanımlanmış bütün karakterleri temsil edebilir.
Bu durumu bir örnek üzerinden göstermeye çalışalım:
harfler = "abcçdefgğhıijklmnoöprsştuüvyz"
for s in harfler:
print("{:<5}{:<15}{:<15}".format(s,
str(s.encode("utf-8")),
len(s.encode("utf-8"))))
Buradan şuna benzer bir çıktı alıyoruz:
a b'a' 1
b b'b' 1
c b'c' 1
ç b'\xc3\xa7' 2
d b'd' 1
e b'e' 1
f b'f' 1
g b'g' 1
ğ b'\xc4\x9f' 2
h b'h' 1
ı b'\xc4\xb1' 2
i b'i' 1
j b'j' 1
k b'k' 1
l b'l' 1
m b'm' 1
n b'n' 1
o b'o' 1
ö b'\xc3\xb6' 2
p b'p' 1
r b'r' 1
s b's' 1
ş b'\xc5\x9f' 2
t b't' 1
u b'u' 1
ü b'\xc3\xbc' 2
v b'v' 1
y b'y' 1
z b'z' 1
Burada, s.encode("utf-8")
komutunun ‘baytlar’ (bytes) türünden bir veri tipi verdiğine dikkat edin (baytlar veri tipini bir sonraki bölümde ayrıntılı olarak inceleyeceğiz). Karakter dizilerinin aksine baytların format()
adlı bir metodu bulunmaz.
Bu yüzden, bu veri tipini format()
metoduna göndermeden önce str()
fonksiyonu yardımıyla karakter dizisine dönüştürmemiz gerekiyor. Bu dönüştürme işlevini, alternatif olarak şu şekilde de yapabilirdik:
print("{:<5}{!s:<15}{:<15}".format(s,
s.encode("utf-8"),
len(s.encode("utf-8"))))
Yukarıdaki tabloda ilk sütun Türk alfabesindeki tek tek harfleri gösteriyor. İkinci sütun ise bu harflerin UTF-8 ile kodlandığında nasıl göründüğünü. Son sütunda ise UTF-8 ile kodlanan Türk harflerinin kaç baytlık yer kapladığını görüyoruz.
Bu tabloyu daha iyi anlayabilmek için mesela buradaki ‘ç’ harfini ele alalım:
>>> 'ç'.encode('utf-8')
b'\xc3\xa7'
Burada Python’ın kendi yerleştirdiği karakterleri çıkarırsak (‘b’ ve ‘\x’ gibi) elimizde şu onaltılı sayı kalır:
c3a7
Bu onaltılı sayının onlu sistemdeki karşılığı şudur:
>>> int('c3a7', 16)
50087
50087 sayısının ikili sayma sistemindeki karşılığı ise şudur:
>>> bin(50087)
'0b1100001110100111'
Gördüğünüz gibi, bu sayı 16 bitlik, yani 2 baytlık bir sayıdır. Bunu nasıl teyit edeceğinizi biliyorsunuz:
>>> (50087).bit_length()
16
http://www.fileformat.info/info/charset/UTF-8/list.htm adresine gittiğinizde de UTF-8 tablosunda ‘ç’ harfinin ‘c3a7’ sayısıyla eşleştirildiğini göreceksiniz.
Mesela ‘a’ harfi hem ASCII’de, hem UTF-8’de 97 sayısı ile temsil edilir. Bu sayı 256’dan küçük olduğu için yalnızca 1 bayt ile temsil edilir. Ancak standart ASCII dışında kalan karakterler, farklı kod çözücüler tarafından farklı sayılarla eşleştirilecektir. Bununla ilgili şöyle bir çalışma yapabiliriz:
kod_çözücüler = ['UTF-8', 'cp1254', 'latin-1', 'ASCII']
harf = 'İ'
for kç in kod_çözücüler:
try:
print("'{}' karakteri {} ile {} olarak "
"ve {} sayısıyla temsil edilir.".format(harf, kç,
harf.encode(kç),
ord(harf)))
except UnicodeEncodeError:
print("'{}' karakteri {} ile temsil edilemez!".format(harf, kç))
Bu programı çalıştırdığımızda şuna benzer bir çıktı alırız:
'İ' karakteri UTF-8 ile b'\xc4\xb0' olarak ve 304 sayısıyla temsil edilir
'İ' karakteri cp1254 ile b'\xdd' olarak ve 304 sayısıyla temsil edilir.
'İ' karakteri latin-1 ile temsil edilemez!
'İ' karakteri ASCII ile temsil edilemez!
Bu ufak programı kullanarak hangi karakterin hangi kod çözücü ile nasıl temsil edildiğini (veya temsil edilip edilemediğini) görebilirsiniz.
Eksik Karakterler ve encode Metodu
İnternette dolaşırken mutlaka anlamsız karakterlerle dolu web sayfalarıyla karşılaşmışsınızdır. Bu durumun sebebi, ilgili sayfanın dil kodlamasının (encoding) düzgün belirtilmemiş olmasıdır. Yani sayfanın HTML kodları arasında meta charset etiketi ya hiç yazılmamış ya da yanlış yazılmıştır.
Bir karakter kümesinde herhangi bir karakter bulunamadığında, bulunamayan bu karakterin yerine neyin geleceği, tamamen aradaki yazılıma bağlıdır. Örneğin söz konusu olan bir Python programıysa, ilgili karakter bulunamadığında öntanımlı olarak bu karakterin yerine hiçbir şey koyulmaz. Onun yerine program çökmeye bırakılır… Ancak böyle bir durumda ne yapılacağını isterseniz kendiniz de belirleyebilirsiniz.
Bunun için karakter dizilerinin encode()
metodunun errors adlı parametresinden yararlanacağız. Bu parametre dört farklı değer alabilir:
Parametre | Anlamı |
---|---|
‘strict’ | Karakter temsil edilemiyorsa hata verilir |
‘ignore’ | Temsil edilemeyen karakter görmezden gelinir |
‘replace’ | Temsil edilemeyen karakterin yerine bir ‘?’ işareti koyulur |
‘xmlcharrefreplace’ | Temsil edilemeyen karakter yerine XML karşılığı koyulur |
Bu parametreleri şöyle kullanıyoruz:
>>> "bu Türkçe bir cümledir.".encode("ascii", errors="strict")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode character '\xfc' in
position 4: ordinal not in range(128)
‘strict’ zaten öntanımlı değerdir. Dolayısıyla eğer errors parametresine herhangi bir değer vermezsek Python sanki ‘strict’ değerini vermişiz gibi davranacak ve ilgili karakter kodlaması ile temsil edilemeyen bir karakter ile karşılaşıldığında hata verecektir:
>>> "bu Türkçe bir cümledir.".encode("ascii")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode character '\xfc' in
position 4: ordinal not in range(128)
Gelelim öteki değerlerin ne yaptığına:
>>> "bu Türkçe bir cümledir.".encode("ascii", errors="ignore")
b'bu Trke bir cmledir.'
Gördüğünüz gibi, errors parametresine ‘ignore’ değerini verdiğimizde, temsil edilemeyen karakterler görmezden geliniyor:
>>> "bu Türkçe bir cümledir.".encode("ascii", errors="replace")
b'bu T?rk?e bir c?mledir.'
Burada ise ‘replace’ değerini kullandık. Böylece temsil edilemeyen karakterlerin yerine birer ? işareti koyuldu:
>>> "bu Türkçe bir cümledir.".encode("ascii", errors="xmlcharrefreplace")
b'bu Türkçe bir cümledir.'
Son olarak ise ‘xmlcharrefreplace’ değerinin ne yaptığını görüyoruz. Eğer errors parametresine ‘xmlcharrefreplace’ değerini verecek olursak, temsil edilemeyen her bir harf yerine o harfin XML karşılığı yerleştirilir. Bu değer, programınızdan alacağınız çıktıyı bir XML dosyasında kullanacağınız durumlarda işinize yarayabilir.
Dosyalar ve Karakter Kodlama
Dosyalar konusunu anlatırken, Python’da bir dosyanın open()
fonksiyonu ile açılacağını söylemiştik. Bildiğiniz gibi open()
fonksiyonunu şu şekilde kullanıyoruz:
>>> f = open(dosya_adı, dosya_açma_kipi)
Burada biz open()
fonksiyonunu iki farklı parametre ile birlikte kullandık. Ancak aslında belirtmemiz gereken önemli bir parametresi daha var bu fonksiyonun. İşte bu parametrenin adı encoding’dir.
Gelin şimdi bu parametrenin ne olduğuna ve nasıl kullanıldığına bakalım:
encoding
Tahmin edebileceğiniz gibi, encoding parametresi bir dosyanın hangi kod çözücü ile açılacağını belirtmemizi sağlar. Python’da dosyalar öntanımlı olarak locale
adlı bir modülün getpreferredencoding()
adlı fonksiyonunun gösterdiği kod çözücü ile açılır. Siz de dosyalarınızın varsayılan olarak hangi kod çözücü ile açılacağını öğrenmek için şu komutları yazabilirsiniz:
>>> import locale
>>> locale.getpreferredencoding()
Dosyalarınızın hangi kod çözücü ile kodlanmış olduğunu open()
fonksiyonuna vereceğiniz encoding parametresi aracılığıyla her zaman belirtmelisiniz:
>>> f = open(dosya, encoding='utf-8')
Örneğin cp1254 ile kodlanmış bir belgeyi UTF-8 ile açmaya kalkışırsanız veya siz hiçbir kod çözücü belirtmediğiniz halde kullandığınız işletim sistemi öntanımlı olarak dosyaları açmak için cp1254 harici bir kod çözücüyü kullanıyorsa, dosyayı okuma esnasında şuna benzer bir hata alırsınız:
>>> f = open("belge.txt", encoding="utf-8")
>>> f.read(50)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "C:\Python33\lib\codecs.py", line 300, in decode
(result, consumed) = self._buffer_decode(data, self.errors, final)
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xde in position 79: invalid
continuation byte
Hatırlarsanız bu tür hatalara karşı ne tepki verileceğini belirleyebilmek için encode()
metodunda errors adlı bir parametreyi kullanabiliyorduk. İşte open()
fonksiyonunda da aynı errors parametresi bulunur.
errors
Bu parametreyi encoding()
metodundan hatırlıyorsunuz. Bu parametre orada nasıl kullanılıyorsa, open()
fonksiyonunda da aynı şekilde kullanılır. Dikkatlice bakın:
>>> f = open(dosya_adı, encoding='utf-8', errors='strict')
Bu zaten errors parametresinin öntanımlı değeridir. Dolayısıyla ‘strict’ değerini belirtmeseniz de öntanımlı olarak bu değeri belirtmişsiniz gibi davranılacaktır.
>>> f = open(dosya_adı, encoding='utf-8', errors='ignore')
Burada ise ‘ignore’ değerini kullanarak, Python’ın kodlanamayan karakterleri görmezden gelmesini sağlıyoruz.
>>> f = open(dosya_adı, encoding='utf-8', errors='replace')
‘replace’ değeri ise kodlanamayan karakterlerin yerine \ufffd karakterini yerleştirecektir. Bu karakter işlev bakımından, encode()
metodunu anlatırken gördüğümüz ‘?’ işaretine benzer.
Bu karaktere teknik olarak ‘UNICODE Değiştirme Karakteri’ (UNICODE Replacement Character) adı verilir. Bazı yerlerde bu karakteri elmas şeklinde siyah bir küp içine yerleştirilmiş soru işareti şeklinde görebilirsiniz.
Konu ile ilgili Fonksiyonlar
Bu bölümde, karakter kodlama işlemleri esnasında işimize yarayacak bazı fonksiyonları ele alacağız.
repr()
Dilerseniz repr()
fonksiyonunu anlatmaya bir örnek ile başlayalım.
Şimdi Python’ın etkileşimli kabuğunu açarak şu kodu yazın:
>>> "Python programlama dili"
Dikkat ettiyseniz, yukarıdaki kodların çıktısında karakter dizisi tırnak işaretleri içinde gösteriliyor. Eğer bu karakter dizisini print()
fonksiyonu içine yazarsanız o tırnak işaretleri kaybolacaktır:
>>> print("Python programlama dili")
Python programlama dili
Peki bu iki farklı çıktının sebebi ne?
Python programlama dilinde nesneler iki farklı şekilde temsil edilir:
- Python’ın göreceği şekilde
- Kullanıcının göreceği şekilde
Daha önce de dediğimiz gibi, başında print()
olmayan ifadeler, bir dosyaya yazılıp çalıştırıldığında çıktıda görünmez. İşte burada yardımımıza repr()
adlı bir fonksiyon yetişecek. Bu fonksiyonu şöyle kullanıyoruz:
print(repr("karakter dizisi\n"))
Bu kodu bir dosyaya yazıp kaydettiğimizde şöyle bir çıktı alıyoruz:
'karakter dizisi\n'
Gördüğünüz gibi hem tırnak işaretleri, hem de satır başı karakteri çıktıda görünüyor. Eğer repr()
fonksiyonunu kullanmasaydık şöyle bir çıktı alacaktık:
karakter dizisi
Yukarıda biz bu fonksiyonun nasıl kullanıldığına dair ayrıntıları verdik. Ancak bu fonksiyonun, yine yukarıdaki işleviyle bağlantılı olmakla birlikte biraz daha farklı görünen bir işlevi daha bulunur.
Hatırlarsanız, ilk derslerimizde r adlı bir kaçış dizisinden söz etmiştik. Bu kaçış dizisini şöyle kullanıyorduk:
print(r"\n")
Bildiğiniz gibi, \n kaçış dizisi bir alt satıra geçmemizi sağlıyor. İşte r adlı kaçış dizisi \n kaçış dizisinin bu işlevini baskılayarak, bizim \n kaçış dizisinin kendisini çıktı olarak verebilmemizi sağlıyor.
Acaba bir değişkene atanmış kaçış dizilerinin işlevini nasıl baskılayabiliriz? Yani mesela elimizde şöyle bir değişken bulunuyor olsun:
yeni_satır = "\n"
Biz bu değişkenin değerini nasıl ekrana yazdıracağız?
İşte bu tür durumlar için de repr()
fonksiyonundan yararlanabilirsiniz:
print(repr('\n'))
Böylece satır başı karakterinin işlevi baskılanacak ve biz çıktıda bu karakterin kendisini göreceğiz.
Hatırlarsanız ASCII konusunu anlatırken şöyle bir örnek vermiştik:
for i in range(128):
if i % 4 == 0:
print("\n")
print("{:<3}{:>8}\t".format(i, repr(chr(i))), sep="", end="")
İşte burada, repr()
fonksiyonunun yukarıda sözünü ettiğimiz işlevinden yararlanıyoruz. Eğer bu kodlarda repr()
fonksiyonunu kullanmazsak, ASCII tablosunu oluşturan karakterler arasındaki \n, \a, \t gibi kaçış dizileri ekranda görünmeyecek, bunun yerine bu kaçış dizileri doğrudan işlevlerini yerine getirecek, bu da bizim istediğimiz ASCII tablosunu üretmemize engel olacaktır.
ascii()
ascii()
fonksiyonu biraz önce öğrendiğimiz repr()
fonksiyonuna çok benzer. Örneğin:
>>> repr("asds")
"'asds'"
>>> ascii("asds")
"'asds'"
Bu iki fonksiyon, ASCII tablosunda yer almayan karakterlere karşı tutumları yönünden birbirlerinden ayrılır. Örneğin:
>>> repr("İ")
"'İ'"
>>> ascii("İ")
"'\\u0130'"
Gördüğünüz gibi, repr()
fonksiyonu ASCII tablosunda yer almayan karakterleri de göründükleri gibi temsil ediyor. ascii()
fonksiyonu ise bu karakterlerin UNICODE kod konumlarını (code points) gösteriyor.
Bir örnek daha verelim:
>>> repr("€")
"'€'"
>>> ascii("€")
"'\\u20ac'"
ascii()
fonksiyonunun UNICODE kod konumlarını gösterme özelliğinin bir benzerini daha önce öğrendiğimiz encode()
metodu yardımıyla da elde edebilirsiniz:
>>> "€".encode("unicode_escape")
b'\\u20ac'
Ancak ascii()
fonksiyonunun str tipinde, encode()
metodunun ise bytes tipinde bir çıktı verdiğine dikkat edin.
ord()
Bu fonksiyon, bir karakterin sayı karşılığını verir:
>>> ord("\n")
10
>>> ord("€")
8364
chr()
Bu fonksiyon, bir sayının karakter karşılığını verir:
>>> chr(10)
'\n'
>>> chr(8364)
'€'
Bu eğitim seti Kaynak tarafından oluşturulmuştur.