Ruby'de her metin (String) bir bayt dizisi ile bu baytların nasıl okunacağını söyleyen bir kodlamadan (Encoding) oluşur. Kaynak dosyalarının varsayılan kodlaması UTF-8'dir, bu yüzden kodunuza yazdığınız Türkçe metinler kendiliğinden UTF-8 olur. force_encoding baytlara dokunmadan yalnızca etiketi değiştirir, encode ise metni başka bir kodlamaya gerçekten dönüştürür. Türkçe büyük küçük harf dönüşümünde upcase(:turkic) ve downcase(:turkic) kullanmanız gerekir; aksi halde i harfi I olur.

Bu yazıda karakter ile bayt arasındaki farkı, bozuk baytları tespit etmeyi ve temizlemeyi, eski kodlamalarla (ör. Windows-1254) gelen dosyaları okumayı, uyumsuz kodlama hatalarını ve Türkçeye özgü İ/ı dönüşümlerini Ruby 4.0 belgelerine dayanarak ele alıyoruz. Metin metotlarının genel kullanımı için Ruby string metotları, genel yol haritası için Ruby öğrenme rehberi sayfasına bakabilirsiniz.

Kaynak kodlaması ve metnin kodlaması

Belgelere göre bir Ruby betiğinin varsayılan kodlaması UTF-8'dir ve bunu __ENCODING__ ile görebilirsiniz. Metin literalleri bu kodlamayı alır. Bir dosyanın ilk satırına (shebang varsa ikinci satırına) # encoding: Windows-1254 gibi bir sihirli yorum yazarak kaynak kodlamasını değiştirmek mümkündür, ancak yeni projelerde UTF-8 dışında bir kaynak kodlaması kullanmak için çoğu zaman bir neden yoktur.

ruby
s = "Çağrı"
p __ENCODING__
p s.encoding
p s.length
p s.bytesize
p s.chars
p s.bytes
# çıktı: #<Encoding:UTF-8>
# çıktı: #<Encoding:UTF-8>
# çıktı: 5
# çıktı: 8
# çıktı: ["Ç", "a", "ğ", "r", "ı"]
# çıktı: [195, 135, 97, 196, 159, 114, 196, 177]

Beş karakterlik bu metin sekiz bayt tutar: Ç, ğ ve ı UTF-8'de ikişer bayt kaplar, ASCII harfler ise birer bayt. length ve chars karakterlerle, bytesize ve bytes baytlarla çalışır. Veritabanı sütun uzunluğu veya ağ protokolü gibi bayt sınırı olan yerlerde bytesize, kullanıcıya gösterilecek uzunlukta ise length önemlidir.

Bayt düzeyinde kesmenin tehlikesi

byteslice gibi bayt tabanlı metotlarla bir karakteri ortasından bölerseniz, geçersiz bir UTF-8 dizisi elde edersiniz. valid_encoding? bunu yakalar. Karakter düzeyinde kesmek için [] kullanın.

ruby
parca = "Çağrı".byteslice(0, 1)
p parca
p parca.valid_encoding?
p "Çağrı"[0, 1]
# çıktı: "\xC3"
# çıktı: false
# çıktı: "Ç"

force_encoding ile encode farkı

force_encoding ile encode farkı: force_encoding, encode, valid_encoding?, scrub

Bu iki metot sık karıştırılır ama tamamen farklı işler yapar. force_encoding baytları olduğu gibi bırakır ve yalnızca "bu baytları şu kodlamaya göre oku" der; belgeler bunu içeriği değil yorumlama biçimini değiştirmek olarak tanımlar. encode ise her karakteri hedef kodlamadaki karşılığına çevirir; baytlar değişir, karakterler aynı kalır. Kural basit: baytlar zaten doğru ama etiket yanlışsa force_encoding, karakterleri başka bir kodlamaya taşımanız gerekiyorsa encode.

ruby
utf8 = "şeker"
win = utf8.encode("Windows-1254")
p win.encoding
p utf8.bytes
p win.bytes
p win.encode("UTF-8") == utf8

yanlis = win.dup.force_encoding("UTF-8")
p yanlis.valid_encoding?

ham = "şeker".b
p ham.encoding
p ham.force_encoding("UTF-8")
# çıktı: #<Encoding:Windows-1254>
# çıktı: [197, 159, 101, 107, 101, 114]
# çıktı: [254, 101, 107, 101, 114]
# çıktı: true
# çıktı: false
# çıktı: #<Encoding:BINARY (ASCII-8BIT)>
# çıktı: "şeker"

Örnekte ş UTF-8'de iki bayt (197, 159), Windows-1254'te tek bayttır (254). Windows-1254 baytlarına force_encoding("UTF-8") uygulamak metni düzeltmez, geçersiz bir UTF-8 metni üretir. Buna karşılık b ile ikili (BINARY) etiketlenmiş ama baytları aslında UTF-8 olan bir metin, force_encoding ile doğru biçimde geri kazanılır. Ağdan veya dosyadan ham bayt olarak okunan veriler sıklıkla bu durumdadır.

MetotBaytları değiştirir miNe zaman kullanılır
force_encodingHayırBaytlar doğru, kodlama etiketi yanlış
encodeEvetMetni başka bir kodlamaya dönüştürmek
valid_encoding?Hayır (sorgu)Baytlar etikete uygun mu diye kontrol
scrubEvet (yeni metin)Geçersiz bayt dizilerini değiştirmek
bHayır (yeni metin)Metnin BINARY etiketli kopyasını almak
unicode_normalizeEvet (yeni metin)Birleşik ve ayrık karakterleri tek biçime getirmek

Bozuk baytları bulmak ve temizlemek

Dışarıdan gelen bir metnin geçerli olup olmadığını valid_encoding? ile kontrol edebilirsiniz. Geçersiz bayt dizilerini atmak ya da bir yer tutucuyla değiştirmek için scrub kullanılır; argüman vermezseniz Unicode kodlamalarında yer tutucu olarak U+FFFD karakteri kullanılır. Bozuk baytlı bir metinde split veya gsub gibi düzenli ifade kullanan metotlar ise ArgumentError fırlatır.

ruby
bozuk = "Merhaba \xFF dünya"
p bozuk.valid_encoding?
p bozuk.scrub
p bozuk.scrub("?")
p bozuk.scrub("").valid_encoding?
begin
  bozuk.split(" ")
rescue ArgumentError => e
  puts e.message
end
# çıktı: false
# çıktı: "Merhaba � dünya"
# çıktı: "Merhaba ? dünya"
# çıktı: true
# çıktı: invalid byte sequence in UTF-8

encode seçenekleri ve dönüşüm hataları

Hedef kodlamada karşılığı olmayan bir karakterle karşılaşan encode, varsayılan olarak Encoding::UndefinedConversionError fırlatır; kaynakta geçersiz bayt varsa Encoding::InvalidByteSequenceError oluşur. Belgelerdeki seçeneklerle bu davranışı değiştirebilirsiniz: undef: :replace tanımsız karakterleri, invalid: :replace geçersiz baytları değiştirir; replace: ise yer tutucu metni belirler.

ruby
metin = "Fiyat: 10 €"
begin
  metin.encode("ISO-8859-9")
rescue => e
  puts e.class
  puts e.message
end
p metin.encode("ISO-8859-9", undef: :replace)
p metin.encode("ISO-8859-9", undef: :replace, replace: "EUR")
p metin.encode("Windows-1254").bytes.last
# çıktı: Encoding::UndefinedConversionError
# çıktı: U+20AC from UTF-8 to ISO-8859-9
# çıktı: "Fiyat: 10 ?"
# çıktı: "Fiyat: 10 EUR"
# çıktı: 128

Türkçe karakterleri kapsayan iki tek baytlık kodlama olan ISO-8859-9 ile Windows-1254 bu konuda farklıdır: örnekteki avro işareti ISO-8859-9'da yoktur ama Windows-1254'te 128 numaralı bayt olarak bulunur. Hangi kodlamayla çalıştığınızı bilmeden dönüşüm yapmak bu yüzden veri kaybına yol açabilir.

Uyumsuz kodlamaları birleştirmek

Farklı kodlamadaki iki metni birleştirmek her zaman mümkün değildir. Belgelere göre Encoding.compatible? iki nesnenin birleştirilip birleştirilemeyeceğini söyler ve mümkünse sonucun kodlamasını, değilse nil döndürür. Uyumsuz metinleri birleştirmeye çalışırsanız Encoding::CompatibilityError alırsınız. Çözüm, metinleri önce ortak bir kodlamaya (genellikle UTF-8) encode etmektir.

ruby
a = "şeker"
b = "çay".encode("Windows-1254")
begin
  a + b
rescue Encoding::CompatibilityError => e
  puts e.message
end
p Encoding.compatible?(a, b)
p Encoding.compatible?(a, "ascii")
p a + b.encode("UTF-8")
# çıktı: incompatible character encodings: UTF-8 and Windows-1254
# çıktı: nil
# çıktı: #<Encoding:UTF-8>
# çıktı: "şekerçay"

Farklı kodlamadaki dosyaları okumak

Farklı kodlamadaki dosyaları okumak

Dosya ve IO nesnelerinin iki kodlaması vardır: dosyanın içindeki baytların kodlaması olan dış kodlama (external) ve okunan metnin dönüştürüleceği iç kodlama (internal). Belgelere göre iç kodlamanın varsayılanı nil, yani dönüşüm yapılmaz. Eski bir sistemden gelen Windows-1254 kodlu bir dosyayı UTF-8 metin olarak almak için iki kodlamayı da açıkça verebilirsiniz:

ruby
File.write("eski.txt", "Güle güle".encode("Windows-1254"))
p File.binread("eski.txt")

metin = File.read("eski.txt", external_encoding: "Windows-1254", internal_encoding: "UTF-8")
p metin
p metin.encoding
File.delete("eski.txt")
# çıktı: "G\xFCle g\xFCle"
# çıktı: "Güle güle"
# çıktı: #<Encoding:UTF-8>

Varsayılan dış kodlama Encoding.default_external ile okunur ve komut satırında -E seçeneğiyle ayarlanabilir. Belgeler, bu değerleri program çalışırken Encoding.default_external= ile değiştirmenin, değişiklikten önce ve sonra oluşan metinlerin farklı kodlamalara sahip olmasına yol açabileceği için sorun çıkarabileceğini belirtir. Dosya işlemlerinin ayrıntısı için Ruby dosya ve IO işlemleri, CSV dosyaları için CSV formatı ve JSON dönüşümü yazısına bakabilirsiniz.

bash
# Varsayılan dış ve iç kodlamayı UTF-8 yaparak bir betiği çalıştırmak
ruby -E UTF-8 betik.rb

# Yalnızca dış kodlamayı Windows-1254, iç kodlamayı UTF-8 yapmak
ruby -E Windows-1254:UTF-8 betik.rb

Türkçe büyük küçük harf: upcase(:turkic)

Ruby'nin büyük küçük harf metotları varsayılan olarak tam Unicode eşlemesini kullanır; bu çoğu dil için doğrudur ama Türkçe ve Azerice için değil. Belgeler, noktalı ve noktasız I ayrımı yapan diller için :turkic seçeneğini tanımlar. Bu seçenek upcase, downcase, capitalize, swapcase ve ünlemli eşlerinde kullanılabilir; casecmp ve casecmp? ise bu argümanı almaz.

ruby
p "istanbul".upcase
p "istanbul".upcase(:turkic)
p "ISPARTA".downcase
p "ISPARTA".downcase(:turkic)
p "istanbul".capitalize(:turkic)

izmir = "İzmir".downcase
p izmir
p izmir.length
p "İzmir".downcase(:turkic)
# çıktı: "ISTANBUL"
# çıktı: "İSTANBUL"
# çıktı: "isparta"
# çıktı: "ısparta"
# çıktı: "İstanbul"
# çıktı: "i̇zmir"
# çıktı: 6
# çıktı: "izmir"

En sinsi sonuç "İzmir".downcase satırında: varsayılan Unicode eşlemesi büyük İ harfini i ve ardından gelen bir birleştirici nokta (U+0307) olarak küçültür. Ekranda neredeyse aynı görünen metin altı karakter uzunluğundadır ve "izmir" ile eşit değildir. Türkçe metinleri küçültüp karşılaştırırken veya arama anahtarı üretirken her zaman :turkic kullanın.

ruby
p "İSTANBUL".casecmp?("istanbul")
p "İSTANBUL".downcase(:turkic) == "istanbul"
# çıktı: false
# çıktı: true

Unicode normalizasyonu

Aynı görünen bir harf Unicode'da iki farklı biçimde yazılabilir: tek bir hazır karakter olarak (ö) ya da temel harf ile birleştirici işaretin yan yana gelmesiyle (o + iki nokta). Bu iki metin ekranda aynıdır ama == ile eşit değildir. unicode_normalize metni seçtiğiniz biçime getirir; varsayılan :nfc birleşik biçimdir. Kopyala yapıştırla veya farklı işletim sistemlerinden gelen dosya adlarında bu fark sık görülür.

ruby
a = "ö"
b = "ö"
p a == b
p [a.length, b.length]
p a == b.unicode_normalize
p b.unicode_normalize.length
# çıktı: false
# çıktı: [1, 2]
# çıktı: true
# çıktı: 1

Türkçe alfabetik sıralama

String#<=> metinleri içeriklerine göre karşılaştırır ve sort bu karşılaştırmayı kullanır; UTF-8 metinlerde sonuç bayt sırasıdır. Bu yüzden Ç, İ, Ş gibi harfler Z'den sonraya düşer. Ruby çekirdeğinde Türkçe alfabe sırasına göre sıralayan hazır bir metot yoktur; basit durumlarda kendi alfabe sıranızı tanımlayıp sort_by ile kullanabilirsiniz:

ruby
sehirler = ["Zonguldak", "Çorum", "Ankara", "İzmir", "Ordu", "Iğdır", "Şanlıurfa"]
p sehirler.sort

ALFABE = "AaBbCcÇçDdEeFfGgĞğHhIıİiJjKkLlMmNnOoÖöPpRrSsŞşTtUuÜüVvYyZz"
p sehirler.sort_by { |s| s.chars.map { |c| ALFABE.index(c) || ALFABE.size } }
# çıktı: ["Ankara", "Iğdır", "Ordu", "Zonguldak", "Çorum", "İzmir", "Şanlıurfa"]
# çıktı: ["Ankara", "Çorum", "Iğdır", "İzmir", "Ordu", "Şanlıurfa", "Zonguldak"]

Bu yaklaşım alfabede olmayan karakterleri en sona atar ve büyük küçük harfi yan yana sıralar; tam dilbilimsel sıralama gerekiyorsa bu iş için hazırlanmış bir kütüphaneye bakmanız gerekir. sort_by ve diğer koleksiyon metotları için Ruby Enumerable: map, select, reduce yazısına bakabilirsiniz.

Ruby 4.0 notu

Ruby 4.0 NEWS dosyasına göre String ve Regexp için Unicode sürümü 17.0.0, emoji sürümü 17.0 olarak güncellendi. Kodlama sisteminin temel davranışında bu sürümde listelenen başka bir değişiklik yok. Düzenli ifadelerde Türkçe karakterlerin nasıl ele alındığı için Ruby Regexp yazısındaki \w bölümüne göz atın.

Sık yapılan hatalar

Sık yapılan hatalar: Kodlamayı düzeltmek için force_encoding kullanmak, length ile bytesize karıştırmak, :turkic olmadan küçültmek, Bozuk girdiyi kontrol etmemek
  • Kodlamayı düzeltmek için force_encoding kullanmak: Baytlar başka bir kodlamadaysa etiketi değiştirmek metni düzeltmez; encode gerekir.
  • length ile bytesize karıştırmak: Türkçe karakterler UTF-8'de iki bayt olduğu için bayt sınırlı alanlarda length yanıltıcıdır.
  • :turkic olmadan küçültmek: "İ".downcase birleştirici noktalı bir i üretir, "I".downcase ise Türkçede yanlış olan i sonucunu verir.
  • Bozuk girdiyi kontrol etmemek: Dışarıdan gelen metni işlemeden önce valid_encoding? ile doğrulamak, ileride çıkacak ArgumentError veya kodlama hatalarını önler.
  • Kodlamayı tahmin etmek: Bir dosyanın kodlamasını bilmeden encode etmek, sessiz veri bozulmasına yol açabilir. Kaynağı belgeleyin veya sorun.
  • Normalizasyonu atlamak: Dosya adı veya kullanıcı adı karşılaştırırken iki tarafı da unicode_normalize ile aynı biçime getirin.

Sık Sorulan Sorular

Ruby'de varsayılan kodlama nedir?

Kaynak dosyaları ve metin literalleri için varsayılan UTF-8'dir. Dosya okurken kullanılan varsayılan dış kodlama Encoding.default_external ile belirlenir; belgelere göre metin akışları için bu UTF-8, ikili akışlar için ASCII-8BIT'tir. İç kodlamanın varsayılanı nil olduğundan okuma sırasında kendiliğinden dönüşüm yapılmaz.

"invalid byte sequence in UTF-8" hatasını nasıl çözerim?

Bu, metnin UTF-8 olarak etiketlendiği ama baytlarının geçerli UTF-8 olmadığı anlamına gelir. Metnin gerçek kodlamasını biliyorsanız önce force_encoding ile doğru etiketi verip ardından encode("UTF-8") ile dönüştürün. Bilmiyorsanız ve kayıp kabul edilebilirse scrub ile bozuk baytları temizleyebilirsiniz.

Türkçe karakterleri büyük harfe çevirmek için ne kullanmalıyım?

upcase(:turkic). Bu seçenek i harfini İ, ı harfini I yapar. Küçültmek için de downcase(:turkic) kullanın.

Bir metnin yalnızca ASCII karakterlerden oluşup oluşmadığını nasıl anlarım?

ascii_only? metodu bunu söyler. Örneğin "abc".ascii_only? true, "çay".ascii_only? false döndürür.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Ruby öğrenme rehberi · Ruby string metotları · Ruby Regexp · Ruby dosya ve IO · Ruby frozen string · CSV ve JSON dönüşümü