UTF-8 vs UTF-16 vs Unicode — Geliştiriciler İçin Eksiksiz Kodlama Rehberi
utf-8 unicode encoding aramalarının gerçekte sorduğu kısa cevap şu: Unicode ile UTF-8 aynı şey değildir. Unicode, her karaktere bir kod noktası (U+1F600 gibi bir sayı) atayan devasa numaralandırılmış tablodur. UTF-8, UTF-16 ve UTF-32 ise kodlamalardır — kod noktalarını baytlara çevirmenin üç farklı yolu. UTF-8 neredeyse her zaman istediğiniz seçimdir: İngilizce metin için ASCII’yle bayt düzeyinde özdeştir, her emoji için dört bayta uzar ve JSON, HTML5 ile çoğu modern protokol tarafından zorunlu kılınır.
Bu rehber, ısırılmış geliştirici içindir: bir 😀 üzerinde MySQL Incorrect string value hatası, JavaScript’in "😀".length === 2 sürprizi, cat ile düzgün açılan ama Excel’de bozuk görünen CSV. Kod noktalarından başlayıp UTF-8 bayt mekaniğine, vekil çiftlere, BOM’lara, dokuz dilin varsayılan davranışına ve sekiz üretim tuzağına kadar yürüyeceğiz; ardından bir karar matrisi ve SSS ile kapatacağız.
Okurken bir bayt dizisini doğrulamak ister misiniz? Herhangi bir karakter dizisini Base64 Kodla ve Çöz aracına yapıştırın — çözülen yük, bu makalenin açıkladığı UTF-8 bayt akışının ta kendisidir.
2026’da Kodlama Sizi Hâlâ Neden Isırıyor?
Üç senaryo, hepsi son on iki ayın gerçek hata izleyicilerinden:
- MySQL bir emojiyi reddediyor. Bir kullanıcı
Hello 😀gönderiyor ve sunucuIncorrect string value: '\xF0\x9F\x98\x80'dönüyor. Tabloutf8, geliştirici “bu zaten UTF-8, sorun ne?” diye düşünüyor; cevap MySQL tarihinin derinliklerine gömülü (7. bölümde ele alınıyor). - Bir karakter sayacı bozuk gönderiliyor. 280 karakterlik bir tweet doğrulayıcı
text.lengthkullanıyor, emoji dolu bir mesajı kabul ediyor ve API reddediyor. Tersi de oluyor: geçerli bir gönderi ön uç tarafından reddediliyor. Belirti 4. bölümde teşhis ediliyor. - Yerel HTML “䏿–‡” hâline geliyor. Bir geliştirici dosyayı Windows-1252 ile kaydediyor, tarayıcı UTF-8 tahmin ediyor ve Mojibake çıkıyor. Bu, 5. bölümdeki BOM / charset bildirim hikâyesi; aynı bayt-karakter uyumsuzluğunun sorgu dizelerini bozduğu URL Kodlama ve Çözme Rehberi ile paralellik taşır.
Bu rehberin vaadi: son sayfaya geldiğinizde (a) Unicode’u UTF-8’den tek cümleyle ayırt edeceksiniz, (b) herhangi bir yeni proje için UTF-8, UTF-16 ve UTF-32 arasında seçim yapabileceksiniz, (c) her büyük dilde emojiyi doğru sayan kod yazacaksınız ve (d) yalnızca bayt akışından her türlü charset hatasını ayıklayabileceksiniz. Karakter kodlama tavşan deliği derin ama pratik yüzeyi küçüktür.
Unicode Nedir? Kod Noktaları, Karakterler ve Glifler
Unicode, her karaktere benzersiz bir sayı — U+1F600 gibi bir kod noktası — atayan bir karakter tablosudur. UTF-8, UTF-16 ve UTF-32 ise bu kod noktalarını baytlara çeviren kodlamalardır. Unicode’un kendisi hiçbir bayt depolamaz; yalnızca soyut karakterden tamsayıya eşlemeyi tanımlar.
Üç terim daha sohbeti bulandırır çünkü çoğunlukla aynı görünür işarete atıfta bulunur:
Ayırmanız gereken üç katman
- Kod noktası (
U+0041,U+1F600): Unicode’un atadığı tamsayı. UzayU+0000ileU+10FFFFarasında uzanır, kabaca 1,1 milyon yuva, bunlardan yaklaşık 150.000 tanesi şu an atanmış durumdadır. - Karakter (veya soyut karakter): anlamsal kimlik — Latin büyük A, gülen yüz emojisi.
- Glif: bir yazı tipinin oluşturduğu görsel şekil. Bir karakterin birçok glifi vardır: serif A, italik A, elle çizilmiş bir A. Unicode glifleri umursamaz.
- Grafem kümesi: kullanıcının tek bir “karakter” olarak algıladığı şey. Çoğunlukla bir kod noktası, bazen birkaçı. á harfi tek kod noktası
U+00E1olabilir veya iki kod noktasıa + U+0301(birleşen keskin aksan) olabilir; Karakter ve Sözcük Limitleri 2026 — Twitter, SMS, SEO, Instagram Twitter, SMS ve SEO’nun bu sınırı nasıl farklı çizdiğini inceler.
Başka hiçbir şey hatırlamayacaksanız, şunu hatırlayın: kod noktası → kodlama → baytlar → görüntüleme. Her ok bağımsız olarak kırılabilir.
Kod noktası gösterimi — U+XXXX ve \uXXXX
Kod noktalarının birkaç biçimde yazıldığını göreceksiniz. U+0041 kanonik Unicode gösterimidir: dört ila altı onaltılık basamak, U+ öneki. Kaynak kodda:
- JavaScript / JSON:
"A"(dört onaltılık basamak, yalnızca BMP) ve"\u{1F600}"(ES6 küme parantezi, herhangi bir kod noktası). - Python:
"A"(4 basamak),"\U00000041"(8 basamak, büyük U),"\N{LATIN CAPITAL LETTER A}"(ada göre). - Shell / git log / sed çıktısı: çoğu zaman
éiçin\xc3\xa9gibi ham UTF-8 baytları görürsünüz; bu bir kod noktası değil, kodlanmış biçimdir ve bizi 3. bölüme götürür.
17 düzlem — BMP ve ötesi
Unicode, kod noktası uzayını her biri 65.536 kod noktası içeren 17 düzleme böler — 17 × 2^16 = 1.114.112.
- Düzlem 0, Temel Çok Dilli Düzlem (BMP, Basic Multilingual Plane):
U+0000ileU+FFFFarası. Latin, CJK ideogramları, Kiril, Arapça, Yunanca — eski metinlerde karşılaştığınız hemen her yazı sistemi burada yaşar. - Düzlem 1-16, tamamlayıcı düzlemler:
U+10000ileU+10FFFFarası. Çoğu emoji (U+1F600ve arkadaşları), nadir CJK karakterleri, tarihî yazı sistemleri (Mısır hiyeroglifleri, çiviyazısı), müzik notası.
U+FFFF adresindeki BMP / tamamlayıcı sınırı, bu makaledeki en önemli tek sayıdır. UTF-16’nın karakter başına tek kod birimi olmaktan çıktığı, UTF-8’in üç bayttan dörde sıçradığı ve MySQL’in yanlış adlandırılmış utf8 harmanlamasının pes ettiği yerdir.
Emoji ile hızlı sağlamlık kontrolü
"a" → 1 kod noktası U+0061 → 1 grafem
"é" (NFC) → 1 kod noktası U+00E9 → 1 grafem
"é" (NFD) → 2 kod noktası U+0065 U+0301 → 1 grafem
"😀" → 1 kod noktası U+1F600 (Düzlem 1) → 1 grafem
"👨👩👧" → 5 kod noktası (3 kişi + 2 ZWJ U+200D) → 1 grafem
Son satır asıl vuruştur. Aile emojisi kullanıcı tarafından tek bir karakter olarak algılanır, ama beş kod noktasının Sıfır Genişlikli Birleştiriciler ile birleştirilmesinden oluşur. Yığının her katmanı bunu farklı sayabilir ve 7. bölümdeki 6 numaralı tuzak, bu anlaşmazlığın doldurduğu hata raporudur.
UTF-8 Kodlama Mekaniği — 1-4 Bayt Nasıl Çalışır?
UTF-8, Unicode kod noktalarını 1 ila 4 bayt ile kodlar. ASCII (U+0000–U+007F) 1 bayt kullanır ve ASCII ile bayt düzeyinde özdeştir. Daha yüksek kod noktaları, ilk baytın toplam uzunluğu bildirdiği ve her devam baytının 10xxxxxx bit deseniyle başladığı çok baytlı diziler kullanır. UTF-8’in kodlama savaşlarını kazanmasının nedeni bu kendini tanımlayan düzendir.
Bayt deseni tablosu — tek diyagramda UTF-8
| Kod noktası aralığı | UTF-8 baytları | Bayt deseni |
|---|---|---|
U+0000 – U+007F | 1 bayt | 0xxxxxxx |
U+0080 – U+07FF | 2 bayt | 110xxxxx 10xxxxxx |
U+0800 – U+FFFF | 3 bayt | 1110xxxx 10xxxxxx 10xxxxxx |
U+10000 – U+10FFFF | 4 bayt | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
Her x, kod noktasının ikili gösteriminden alınmış bir veri bitidir. Baştaki 0 / 110 / 1110 / 11110 çözücüye toplam kaç bayt olduğunu söyler; baştaki 10 ise her devam baytını işaretler. Bu artıklık UTF-8’i kendi kendini senkronize hâle getirir: bir baytı düşürürseniz akış aşağısında her şeyi bozmak yerine bir sonraki başlangıç baytında devam edebilirsiniz.
Çalışılmış örnek — 中 (U+4E2D) kodlaması
0x4E2D kod noktası U+0800–U+FFFF aralığına düşer, dolayısıyla 3 baytlık şablonu kullanırız.
- İkili:
0x4E2D=0100 1110 0010 1101(16 bit). xyuvalarına sığdırmak için 4-6-6 olarak ayır:0100 / 111000 / 101101.1110xxxx 10xxxxxx 10xxxxxxiçine yerleştir:11100100 10111000 10101101.- Onaltılık:
0xE4 0xB8 0xAD.
URL kodlamasından sonra 中 karakterinin tam olarak %E4%B8%AD hâline gelmesinin nedeni budur: yüzde kodlaması her UTF-8 baytını %XX ile sarar, kod noktasını doğrudan kodlamaz. 7. bölümdeki 3. tuzak zinciri ayrıntılarıyla anlatır.
Çalışılmış örnek — 😀 (U+1F600) kodlaması
0x1F600 kod noktası BMP’yi aşar, bu yüzden 4 baytlık şablonu kullanırız.
- İkili:
0x1F600=0 0001 1111 0110 0000 0000(21 bit, dolgulu). - 3-6-6-6 olarak ayır:
000 / 011111 / 011000 / 000000. 11110xxx 10xxxxxx 10xxxxxx 10xxxxxxiçine yerleştir:11110000 10011111 10011000 10000000.- Onaltılık:
0xF0 0x9F 0x98 0x80.
MySQL’in utf8 harmanlamasının boğulduğu dört bayt işte bunlardır: karakter başına en fazla üç bayt ayırır. 7. bölümdeki 1. tuzakta çözüm var.
UTF-8 neden kazandı — üç güçlü yönü
- ASCII uyumluluğu. Saf ASCII metin dosyası, UTF-8 kodlamasıyla bayt düzeyinde özdeştir. Unicode’dan önce gelen onlarca yıllık araçlar —
grep,awk, klasik shell boruları — bu altküme için çalışmaya devam eder. - Kendi kendini senkronize eder. Devam baytları her zaman
10ile başlar ve bu hiçbir başlangıç baytıyla çakışmaz. Ağ aktarımında bir baytı kaybedin, çöp yığını yerine bir sonraki karakter sınırında yeniden senkronize olursunuz. - Bayt sırası yok. UTF-8, 16-bit veya 32-bit birimler değil, bayt akışıdır; bu yüzden endianlık önemsizdir. UTF-16 ve UTF-32, hangi ucun önce geldiğini bildirmek için bir Byte Order Mark (BOM) gerektirir; UTF-8 gerektirmez (ve genellikle gerektirmemelidir — bkz. 5. bölüm).
Geçersiz UTF-8 — şartnamenin yasakladıkları
Katı bir çözücü şu bayt dizilerini reddeder:
- 5 veya 6 baytlık diziler. Erken RFC’ler bunlara izin veriyordu; RFC 3629 (2003), 21-bitlik Unicode uzayına uyacak şekilde UTF-8’i 4 bayta sınırladı.
- Aşırı uzun kodlamalar.
/karakterini tek bayt0x2Fyerine üç bayt0xE0 0x80 0xAFolarak kodlamak. Bir zamanlar temizledikten sonra çözen yol doğrulayıcılarında dizin geçiş açıklarının verimli bir kaynağıydı. - Yalnız vekil kod noktaları (
U+D800–U+DFFF). Bunlar UTF-16 için ayrılmıştır ve UTF-8’de asla görünmemelidir. - Kesilmiş diziler. 3 baytlık bir başlangıç baytını yalnızca tek bir devam baytının takip etmesi; kullanıcı girişi çok baytlı bir karakterin ortasında bayt sınırında kesildiğinde sık görülür.
Bunlardan herhangi birini somut olarak görmek için bir karakter dizisini Base64 Kodla ve Çöz aracına atın, kodlayın ve ardından baytlar olarak yeniden çözün — kodlayıcı ile çözücü arasındaki bayt dizisi, bu bölümün anlattığı UTF-8 akışıdır.
UTF-16 ve Vekil Çiftler — JavaScript length Neden Yalan Söyler?
utf-8 vs utf-16 etrafında yapılan en yaygın arama aslında “kodumda neden "😀".length 2’ye eşit?” sorusudur. Cevap vekil çiftlerdir ve JavaScript, Java, C# ile Windows’un hep birlikte miras aldığı 1990’lardan kalma bir karardır.
Tek paragrafta UTF-16
UTF-16, Unicode’u 16-bitlik kod birimleri kullanarak temsil eder. BMP içindeki karakterler (U+0000–U+FFFF) tam olarak bir kod birimi alır. Tamamlayıcı düzlemlerdeki karakterler (U+10000–U+10FFFF) iki kod birimi alır ve buna vekil çift denir: U+D800–U+DBFF arasında bir yüksek vekil ve onu izleyen U+DC00–U+DFFF arasında bir alçak vekil. U+D800–U+DFFF bloğu Unicode’da kalıcı olarak ayrılmıştır, dolayısıyla orada gerçek bir karakter yaşamaz. UTF-16, JavaScript, Java, C# (.NET), Windows çekirdek API’leri, Objective-C NSString ve Qt için dahili karakter dizisi biçimidir; hepsi 65.536 karakterin yeterli göründüğü dönemde tasarlanmıştır.
String.length tuzağı
"a".length // 1 — BMP, tek kod birimi
"é".length // 1 — BMP (U+00E9), tek kod birimi
"中".length // 1 — BMP (U+4E2D), tek kod birimi
"😀".length // 2 — tamamlayıcı düzlem (U+1F600), vekil çifti!
"a😀".length // 3 — bir BMP + iki vekil birimi
String.prototype.length, karakter sayısını değil, UTF-16 kod birimlerinin sayısını bildirir. Tamamlayıcı düzlemden herhangi bir şey 2 olarak okunur. Aynı tuzak Java’nın String.length() ve C#‘ın string.Length özelliğinde de vardır.
JS’de kod noktalarını doğru sayma
[..."😀"].length // 1 — yayma yineleyici kod noktalarını dolaşır
Array.from("😀").length // 1 — Array.from de kod noktalarını dolaşır
"😀".match(/./gu).length // 1 — /u bayrağı = unicode-bilinçli düzenli ifade
// "😀".charAt(0) yalnız yüksek vekil döndürür (görsel olarak bozuk)
"😀".codePointAt(0) // 128512 — tam kod noktası U+1F600
Yayma operatörü ve Array.from, dil şartnamesinin kod noktalarını dolaşmak olarak tanımladığı yineleyici protokolünü kullanır. Düz indeks erişimi (str[0], charAt) hâlâ kod birimleri döndürür ve emoji üzerinde size yarım bir vekil çifti verir.
Python — len() zaten doğru olanı yapıyor (neredeyse)
len("😀") # 1 — Python 3 karakter dizileri kod noktası indekslidir
len("👨👩👧") # 5 — kod noktaları (3 insan + 2 ZWJ), grafem değil
# Python 2 varsayılan olarak bayt indeksliydi — len("😀") 4 döndürürdü
Python 3, karakter dizilerini esnek bir 1, 2 veya 4 baytlık gösterimde (PEP 393) saklar ve kod noktasına göre indeksler. len("😀") 1’dir, ama yine de grafem sayısı değildir: aile emojisi hâlâ 5 olarak okunur. Kullanıcı tarafından algılanan karakterleri saymak için bir grafem kütüphanesi gerekir: JavaScript’te Intl.Segmenter (Node 22+, tüm güncel tarayıcılar), Python’da grapheme veya regex, ya da String.count özelliği varsayılan olarak grafem sayan tek ana akım dil olan Swift.
UTF-16 vs UCS-2 — sessiz göç
1996’dan önce Unicode 16 bite sığacağına söz vermişti ve buna karşılık gelen kodlama UCS-2 idi: sabit 2 baytlık bir eşleme. Unicode 2.0, tamamlayıcı düzlemleri ekleyerek bu sözü bozdu. UTF-16, vekil çiftleri kullanan yamalı sürümdür. JavaScript şartnamesi hâlâ yer yer eski UCS-2 sözcük dağarcığını alıntılar; bu nedenle dil, yasak olması gereken yalnız vekilleri tolere eder — “WTF-16” şakaları gerçektir. Web platformu API’leri (DOM, fetch, TextEncoder) yalnız vekilleri reddeder çünkü geçerli UTF-8’e kodlanamazlar.
UTF-32, BOM ve Bayt Sırası Sorusu
UTF-32 — basit, savurgan olan
UTF-32, kod noktası başına sabit 4 bayt kullanır. U+0041, 0x00000041 olarak; U+1F600, 0x0001F600 olarak saklanır. Avantajı sabit zamanlı rastgele erişimdir: n’inci kod noktası 4n bayt ofsetinde durur. Dezavantajı boyuttur: saf ASCII metin UTF-8 ayakizinin dört katına şişer ve CJK metin bile ikiye katlanır. Hemen hiçbir sistem UTF-32’yi diskte saklamaz. Dahili olarak Python 3, karakter dizisi başına en yüksek kod noktasına göre 1, 2 veya 4 bayt seçer; Linux fontconfig yığını, bellek içi glif tabloları için UTF-32 kullanır.
Bayt sırası — endianlık UTF-16 / UTF-32 için neden önemlidir?
UTF-8 tek baytlık bir akıştır, dolayısıyla endianlık geçerli değildir. UTF-16 ve UTF-32, çok baytlı birimler üzerinde çalışır ve farklı CPU’lar bir sayının hangi ucunun önce geldiği konusunda anlaşamaz.
UTF-16 BE'de U+0041 ('A') → 00 41
UTF-16 LE'de U+0041 ('A') → 41 00
x86 ve ARM CPU’lar küçük endian’dır; eski PowerPC ve “ağ bayt sırası” büyük endian’dır. Bir UTF-16 dosyası yazarken birine bağlanmalı ve okuyucuya hangisi olduğunu söylemelisiniz; BOM tam da bunun içindir.
BOM — nedir, ne zaman kullanılır?
Byte Order Mark, dosyanın başına yerleştirilen U+FEFF değeridir. Kodlandığında hem kodlamayı hem (UTF-16 / UTF-32 için) bayt sırasını bildirir.
| Kodlama | BOM baytları |
|---|---|
| UTF-8 | EF BB BF |
| UTF-16 BE | FE FF |
| UTF-16 LE | FF FE |
| UTF-32 BE | 00 00 FE FF |
| UTF-32 LE | FF FE 00 00 |
utf-8 BOM mevcut, ama hiçbir bayt sırası bilgisi taşımıyor çünkü UTF-8’in bayt sırası yok. Tek görevi “bu dosya UTF-8’dir” demektir: başka sinyali olmayan araçlar için faydalı, dosyanın sihirli bir sayı veya yönerge ile başlamasını bekleyen araçlar için zararlıdır.
BOM karar matrisi — eklemeli miyim?
| Biçim | UTF-8 BOM | UTF-16 BOM | UTF-32 BOM |
|---|---|---|---|
| HTML | Hayır (eski ayrıştırıcılarda <!doctype> algılamasını bozar) | — | — |
| JSON | Hayır (RFC 8259 yasaklar) | — | — |
| JavaScript / CSS kaynağı | Kaçının (eski Node ve IE boğulur) | — | — |
| Excel’de açılan CSV | Evet (Excel BOM’suz UTF-8’i ANSI olarak okur ve CJK’yi bozar) | — | — |
| XML | İsteğe bağlı (XML bildirimi zaten kodlamayı belirtir) | Gerekli | Gerekli |
Düz metin .txt | İsteğe bağlı (Windows Notepad varsayılan olarak ekler) | Gerekli | Gerekli |
Kısa kural: webde sunulan her şeyden UTF-8 BOM’u kaldırın; Excel’in açmasını istediğiniz CSV’lere ekleyin; diğer her şeyi okuyucuya bırakın.
9 Dil Yan Yana — Varsayılan Kodlama Davranışı
Diller arası çalışma, bu bilginin karşılığını verdiği yerdir. Aynı "a😀é" karakter dizisi, Bash betiğinizden çağırdığınız her çalışma zamanında farklı bir uzunluk üretir.
Diller arası davranış tablosu
| Dil | Kaynak dosya kodlaması | Karakter dizisi depolaması | length / len neyi sayar | Varsayılan I/O kodlaması | 4 baytlık emoji güvenli mi? |
|---|---|---|---|---|---|
| JavaScript (V8 / SpiderMonkey) | UTF-8 | UTF-16 | UTF-16 kod birimleri | UTF-8 (Node, Web) | Evet, ama .length === 2 |
| Python 3 | UTF-8 (PEP 3120) | dinamik 1 / 2 / 4 bayt (PEP 393) | kod noktaları | UTF-8 (3.7’den beri PEP 540) | Evet, len === 1 |
| Java | UTF-8 (javac varsayılanı) | UTF-16 | UTF-16 kod birimleri | platform charset → UTF-8 (JEP 400, JDK 18+) | Evet, ama .length() === 2 |
| Go | UTF-8 | UTF-8 baytları | baytlar (kod noktaları için utf8.RuneCountInString) | UTF-8 | Evet, len(s) baytları döndürür |
| Rust | UTF-8 | UTF-8 baytları (String değişmezi) | .len() baytlar, .chars().count() kod noktaları | UTF-8 | Evet, açık biçimde |
| C# (.NET) | UTF-8 (.NET Core 3.0’dan beri varsayılan) | UTF-16 | UTF-16 kod birimleri | UTF-8 (.NET 5’ten beri Encoding.Default) | Evet, ama .Length === 2 |
| Ruby | UTF-8 (2.0’dan beri) | karakter dizisi başına kodlama etiketi | kod noktaları (.length) | UTF-8 | Evet, length === 1 |
| PHP | (kaynak kodlaması yok) | bayt karakter dizisi | baytlar (strlen); kod noktaları için mb_strlen | default_charset değerine bağlı | Evet, mb_* ailesi ile |
| MySQL | — | sütun charset’i | baytlar (LENGTH), karakterler (CHAR_LENGTH) | character_set_* sistem değişkenleri | Yalnızca utf8mb4 ile |
Tablo aslında size ne söylüyor?
Üç felsefe, üç hata kümesi:
- Dahili UTF-8 (Go, Rust, Ruby). Yerel karakter dizisi baytlardır;
lengthiyi tanımlıdır ama saydığı şeyi sayar. Yalnızca bir UI veya doğrulama sınırını geçtiğinizde kod noktalarına veya grafemlere dönüştürün. - Dahili UTF-16 (JavaScript, Java, C#). 1990’lı yılların varsayımlarından miras kalmıştır;
lengthkod birimleridir, vekil çifti 2 sayar. Kullanıcıya bakan herhangi bir sayım için kod noktası bilincine sahip yineleme kullanın. - Kod noktası indeksli (Python 3).
lenkod noktalarını verir, bu da ZWJ emojisiyle tanışana kadar doğru görünür; sonrasında yine bir grafem kütüphanesine ihtiyacınız olur.
PHP özel bir durumdur. Yerleşik str* işlevlerinin tümü baytlar üzerinde çalışır, UTF-8 dizilerini opak bloklar olarak ele alır. ASCII dışı her proje mb_* (çok baytlı) ailesini kullanmak zorundadır ve yıldan yıla biriken hata raporları bunun ne sıklıkla atlandığını gösterir.
Pratik yönlendirme: UTF-8’i her yerde tel formatı olarak tutun — dosyalar, HTTP gövdeleri, veritabanı sütunları — ve sınırda çalışma zamanınızın yerel karakter dizisi türüne dönüştürün. Bu, 8. bölümde geri döneceğimiz “UTF-8 sandviçi”dir.
Gerçek Dünyadan 8 Mühendislik Tuzağı
Aşağıdaki örüntüler küreselleşmiş bir kod tabanındaki her kod incelemesinde karşımıza çıkar.
Tuzak 1: MySQL utf8 3 baytlık bir yalandır — utf8mb4’e geçin
Belirti. INSERT INTO users (bio) VALUES ('Hello 😀'); ifadesi Incorrect string value: '\xF0\x9F\x98\x80' for column 'bio' döndürür.
Kök neden. MySQL’in tarihsel utf8 aslında utf8mb3’ün takma adıdır: karakter başına üç baytla sınırlı bir UTF-8 varyantı. U+FFFF üzerindeki herhangi bir kod noktası (her emoji, birkaç bin nadir CJK karakteri, tüm tarihî yazı sistemleri) dört UTF-8 baytı gerektirir ve reddedilir.
Çözüm.
ALTER DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
SET NAMES utf8mb4; -- client connection
# my.cnf
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
MySQL 8.0 hâlâ utf8’i utf8mb3 takma adı olarak gönderiyor. utf8mb3 kullanımdan kaldırılmış ama henüz tamamen çıkarılmamış. Her yeni sütun, her yeni veritabanı, her yeni bağlantı için utf8mb4’ü kullanın; eski varyantın hiçbir avantajı yok.
Tuzak 2: Windows-1252 geri dönüşü — soru işareti gizemi
Belirti. Windows’taki bir meslektaşınızın Notepad’inden dışa aktarılan bir .txt dosyası, kendi makinesinde "akıllı tırnaklar" ve em-tire gösterir. Sunucunuzda ? veya U+FFFD (yedek karakter) hâline gelir.
Kök neden. Eski Notepad, eğri tırnak " işaretini 0x93 olarak kodlayan Windows-1252 (CP-1252) biçimini varsayar. Bir UTF-8 çözücü 0x93’ü kendisinden önce başlangıç baytı olmayan bir başıboş devam baytı (yüksek bit 10) olarak görür ve yedek karakterle ikame eder.
Çözüm. Kaynak kodlamayı tespit edin (Unix’te file, Python’da chardet / charset-normalizer, Node’da jschardet), doğru codec ile çözün ve ardından kaydetmeden önce UTF-8 olarak yeniden kodlayın. Giriş aşamasında UTF-8’i standartlaştırmak tekrarı ortadan kaldırır.
Tuzak 3: URL yüzde kodlaması ≠ UTF-8 (ama onun üzerine inşa edilir)
Belirti. fetch("/search?q=中文") bir arka uç çerçevesinden 404 döndürür ve diğerinde çalışır.
Kök neden. Yüzde kodlaması kod noktaları üzerinde değil baytlar üzerinde çalışır. 中 tek bir kod noktasıdır ama üç UTF-8 baytıdır (E4 B8 AD); her biri ayrı ayrı %E4%B8%AD olarak yüzde kodlanır — URL’de dokuz ASCII karakteri. URL’yi UTF-8 yerine Latin-1 olarak çözen bir çerçeve, işleyiciye üç tek baytlık karakter olarak yorumlanan üç bozuk baytı verecektir.
Çözüm. İstemcide encodeURIComponent("中文") kullanın (tarayıcılar UTF-8 + yüzde kodlamasını tek adımda yapar) ve sunucu çerçevesinin URL’leri UTF-8 olarak çözdüğünü doğrulayın (tüm modern çerçeveler varsayılan olarak öyle yapar). Görsel onay için 中文 karakterlerini URL Kodlayıcı ve Çözücü — URL’leri Kodla, Çöz, Ayrıştır aracına yapıştırın ve %E4%B8%AD%E6%96%87 hâline gelmesini izleyin. Zincirin tamamı URL Kodlama ve Çözme: Yüzde Kodlaması Rehberi yazısında ele alınıyor.
Tuzak 4: Base64 girdisi baytlar, ama siz bir karakter dizisi yazdınız
Belirti. btoa("你好") şunu fırlatır: InvalidCharacterError: The string contains characters outside the Latin1 range.
Kök neden. btoa, ASCII / Latin-1 döneminde tasarlandı. Her giriş karakterinin tek bir bayta (0-255 kod noktaları) sığmasını bekler. 你好, JS motorunda UTF-16 olarak U+4F60 U+597D kod noktalarındadır ve her ikisi de 255’in çok üzerindedir.
Çözüm. Önce UTF-8 baytlarına kodlayın, ardından bu baytları Base64 ile kodlayın.
// Yanlış:
btoa("你好"); // hata fırlatır
// Doğru:
const bytes = new TextEncoder().encode("你好");
// Uint8Array(6) [228, 189, 160, 229, 165, 189]
const b64 = btoa(String.fromCharCode(...bytes));
// "5L2g5aW9"
Uzun hikâye Base64 Kodlama Nedir? Yeni Başlayanlar İçin Rehber ile İleri Düzey Base64: MIME, Data URL, Performans ve Güvenlik yazılarında; Base64 Kodla ve Çöz aracı dönüşümü tek adımda yapar ve ara bayt akışını gösterir.
Tuzak 5: Doğrulama için String.length (Twitter / SMS limitleri)
Belirti. 280 karakterlik bir oluşturucu istemci tarafında doğrulanır, ardından API 422 döndürür. Ya da tersi: kusursuz bir gönderi istemci tarafından reddedilir.
Kök neden. JavaScript’in .length’i UTF-16 kod birimlerini sayar; tek bir emoji 2 olarak sayılır. Twitter ise kod noktalarını sayar (emoji = 1). Karakter sayısı hangi API’ye güvendiğinize bağlı olarak zıt yönlerde yanlıştır.
Çözüm. Kod noktası sayısı için [...text].length kullanın veya gerçek grafem sayısı için Intl.Segmenter (Bluesky / iMessage yaklaşımı). Platform bazında sayılar ile SMS GSM-7 ve UCS-2 sınırları Karakter ve Sözcük Limitleri 2026 — Twitter, SMS, SEO, Instagram yazısında kataloglanıyor.
Tuzak 6: ZWJ emoji aileleri N kod noktası, 1 grafem olarak sayılır
Belirti. "👨👩👧".length === 8. Kod noktalarını saymak 5 verir. Kullanıcıya göre tek bir görüntüdür.
Kök neden. Sıfır Genişlikli Birleştirici (U+200D), birden fazla emoji kod noktasını tek bir oluşturulan kümeye yapıştırır: üç kişi emojisi artı iki ZWJ, beş kod noktasına, sekiz UTF-16 kod birimine, bir grafeme eşittir.
Çözüm.
const seg = new Intl.Segmenter('en', { granularity: 'grapheme' });
[...seg.segment("👨👩👧")].length; // 1
Intl.Segmenter, Node 22+ ve her güncel tarayıcıda mevcuttur. Daha eski çalışma zamanları için grapheme-splitter paketi UAX #29’u uygular.
Tuzak 7: JSON \uXXXX kaçışı — U+FFFF üzerindeki kod noktaları vekil çifti gerektirir
Belirti. Bir JSON yükü "😀" içerir ve alıcı çözücü ya bunu doğru biçimde 😀 olarak oluşturur ya da JSON’daki vekil çiftleri anlayıp anlamamasına bağlı olarak iki kutu karakteri gösterir.
Kök neden. JSON’un \uXXXX kaçışı yalnızca tam olarak dört onaltılık basamak kabul eder, yani tek bir UTF-16 kod birimi. 😀 (U+1F600) kodlaması 😀 vekil çiftini gerektirir. JSON’da \u{...} küme parantezi sözdizimi yoktur.
Çözüm. Ya vekil çiftini kabul edin (her şartnameyle uyumlu ayrıştırıcı bunu işler) ya da emojiyi düz biçimde yazın — JSON, kaçış sözdizimi dışında herhangi bir UTF-8 karakterine izin verir ve çoğu modern ayrıştırıcı bu biçimi tercih eder.
Tuzak 8: HTTP Content-Type: charset= varsayılanları sandığınız gibi değil
Belirti. Bir UTF-8 HTML sayfası bir tarayıcıda Mojibake olarak, diğerinde doğru biçimde oluşturulur.
Kök neden. RFC 2616 başlangıçta açık bir charset belirtilmeyen text/* yanıtları için varsayılan olarak ISO-8859-1’i zorunlu kılıyordu. RFC 7231 (2014) bu varsayılanı kaldırdı ve her tarayıcının tahmin etmesine bıraktı. Bazıları içeriği koklar, bazıları UTF-8’e döner, bazıları sistem yereline varsayılır.
Çözüm. Sunucudan her zaman Content-Type: text/html; charset=utf-8 gönderin ve belge başlığında <meta charset="utf-8"> bulundurun. Her biri tek başına çalışır; ikisi birden başlıkları çıkaran eski vekiller için emniyet kemeri görevi görür.
Bu tuzakları bayt seviyesinde canlı izlemek için en hızlı mikroskop Base64 Kodla ve Çöz aracıdır: bir karakter dizisini yapıştırın, Base64’e kodlayın ve çözülen yük UTF-8 akışıdır.
Doğru Kodlamayı Seçmek — Karar Matrisi
utf-8 vs utf-16 sorusu için cevap neredeyse her zaman UTF-8’dir. Aşağıdaki tablo uç durumları kapsıyor.
Karar matrisi
| Senaryo | Seçim | Neden |
|---|---|---|
| Web sayfaları, API JSON, kaynak dosyaları | UTF-8 (BOM yok) | ASCII uyumlu, bayt sırası yok, Latin metin için en küçük, RFC 8259 JSON için UTF-8’i zorunlu kılar |
| Yoğun CJK depolama (Çin DB, Japon oyun verisi) | UTF-8 (utf8mb4) | UTF-8 CJK karakteri başına 3 bayt kullanır, UTF-16 ise 2 bayt; ancak işaretleme ve JSON anahtarlarından gelen ASCII yükü pratikte UTF-8’i önde bırakır — ve çevredeki ekosistem zaten UTF-8’dir |
| Windows yerel API, eski Java / C# kodu | UTF-16 | Platform varsayılanı; her API çağrısında dönüştürmek hatalara davetiye çıkarır |
| Bellek içi indeks yoğun metin işleme | UTF-32 | Sabit zamanlı kod noktası erişimi; yalnızca ayrıştırıcı kritik kod yollarında değer |
| Windows’ta Excel ile açılan CSV | BOM’lu UTF-8 | Excel, BOM’suz UTF-8’i ANSI olarak okur ve CJK başlıklarını bozar |
| Yeni proje, kısıt yok | UTF-8 (BOM yok) | Kodlama savaşları kesin biçimde sona erdi |
İki temel kural
- Bir platform aksini zorlamadıkça her yerde varsayılan olarak UTF-8 kullanın. W3C, IETF ve Unicode Consortium hepsi hemfikir.
- Sınırda dönüştürün, ortada değil. Girişte baytları dilinizin yerel karakter dizisi türüne çözün. İş mantığında baytlar üzerinde değil, karakter dizileri üzerinde çalışın. Çıkışta UTF-8’e geri kodlayın. Bu “UTF-8 sandviçi” tüm boru hattı ortası mojibake hatalarını ortadan kaldırır.
Sıkça Sorulan Sorular
UTF-8 her zaman ASCII ile geriye dönük uyumlu mudur?
Evet. Geçerli her ASCII dosyası, UTF-8 gösterimiyle bit düzeyinde özdeştir. İlk 128 kod noktası (U+0000–U+007F), yüksek biti sıfırlanmış tek bir bayt olarak kodlanır. Yalnızca ASCII destekleyen eski araçlar — erken grep, sed, klasik shell boruları — saf ASCII UTF-8 dosyalarını değiştirmeden işler. Sıkıntı yalnızca ASCII dışı baytlar (yüksek bit ayarlı) akışa girdiğinde başlar.
Dosyalarımda UTF-8 BOM kullanmalı mıyım?
Varsayılan olarak hayır. HTML, JSON, JavaScript ve CSS dosyaları, başlangıçta bir BOM göründüğünde bazı ayrıştırıcılarda kırılır veya uyarı verir. Standart istisna, Windows’ta Excel ile açılması istenen CSV’dir: BOM olmadan Excel ANSI’yi tahmin eder ve Çince, Japonca veya Korece başlıkları bozar. 5. bölümdeki BOM karar matrisine bakın.
JavaScript’te "😀".length neden 2’ye eşit?
JavaScript karakter dizileri UTF-16 olarak saklanır ve .length karakter sayısını değil, kod birimlerinin sayısını döndürür. 😀 (U+1F600) tamamlayıcı düzlemde yaşar ve vekil çifti gerektirir (iki 16-bit kod birimi), bu yüzden .length 2’dir. Gerçek sayım için [..."😀"].length, Array.from("😀").length ya da Intl.Segmenter kullanın.
Unicode ile UTF-8 arasındaki fark nedir?
Unicode, her karaktere bir kod noktası (U+1F600 gibi bir sayı) atayan karakter tablosudur. UTF-8, bu kod noktalarını baytlara çeviren birkaç kodlamadan biridir (kod noktası başına 1 ila 4 bayt). Unicode bir karakterin ne olduğunu tanımlar; UTF-8 onun bir dosya veya ağ üzerinden nasıl seyahat ettiğini tanımlar. UTF-16 ve UTF-32 ise aynı Unicode tablosunun alternatif kodlamalarıdır.
MySQL’de utf8mb4 her zaman utf8’den daha güvenli midir?
Yeni projeler için evet. MySQL’in utf8’i yanlış adlandırılmış 3 bayt sınırlı utf8mb3 varyantıdır ve U+FFFF üzerindeki hiçbir karakteri saklayamaz — her emoji, birçok nadir CJK karakteri, tüm tarihî yazı sistemleri. utf8mb4 tam 4 baytlık UTF-8’dir. Tek uyarı indeks uzunluğudur: her utf8mb4 karakteri 4 bayta kadar yer kaplayabilir, bu yüzden 767 baytlık eski InnoDB indeks sınırı benzersiz indeksleri 191 karakterle sınırlar (MySQL 5.7+‘da innodb_large_prefix ile çözülmüş ve 8.0’da varsayılan hâle gelmiştir).
Bilinmeyen bir dosyanın kodlamasını nasıl tespit ederim?
Unix’te file, Python’da chardet veya charset-normalizer, Node’da jschardet kullanın. Hiçbiri kusursuz değildir; bayt dağılımından istatistiksel olarak tahmin ederler. UTF-8 algılaması, devam baytı deseni sayesinde son derece güvenilirdir. Windows-1252, ISO-8859-1 ve diğer tek baytlık eski kodlamalar birbirinden neredeyse ayırt edilemez, bu yüzden algılama çoğunlukla dil sezgilerine iner.
UTF-16 her Unicode karakterini temsil edebilir mi?
Evet. UTF-16, 1.114.112 kod noktasının tamamını kapsar. BMP karakterleri (U+0000–U+FFFF) bir 16-bit kod birimi (2 bayt) kullanır ve tamamlayıcı düzlem karakterleri (U+10000–U+10FFFF) vekil çiftleri (4 bayt) kullanır. Kapsama UTF-8 ve UTF-32 ile özdeştir; yalnızca bayt düzeni ve işleme anlam bilgisi farklıdır. Aralarındaki seçim, yetenekle değil ekosistem uyumuyla ilgilidir.