Her pasaportun fotoğraflı sayfasının altında pek dostane görünmeyen iki satır metin vardır: büyük harfler, rakamlar ve bol miktarda < işareti. Burası makine tarafından okunabilir bölgedir (MRZ) ve seyahat belgelerinin standardı olan ICAO Doc 9303 ile tanımlanır. Basılı sayfadaki temel bilgilerin aynısını (ad, belge numarası, uyruk, doğum tarihi, cinsiyet, geçerlilik bitiş tarihi) bir tarayıcının yazı tiplerini ya da sayfa düzenini tahmin etmeden okuyabileceği bir biçimde taşır.
Kendi hata tespitini de beraberinde getirir. Karakterlerin birkaçı kontrol hanesidir (check digit): her biri belirli bir alandan hesaplanır, son bir hane ise aynı anda birkaç alanı kapsar. Tek bir karakter yanlış okunursa, onu koruyan hane genellikle artık tutmaz. Bu da MRZ'yi kimlik belgesi işlemede kendi başınıza doğrulayabileceğiniz az sayıdaki şeyden biri yapar: yirmi satır kod yeter, kimsenin OCR'ına güvenmeniz gerekmez.
Bu yazıda algoritmayı, üç MRZ biçiminin her birinde hanelerin nerede durduğunu ve bir projeye doğrudan ekleyebileceğiniz Python ve JavaScript doğrulayıcısını ele alıyoruz. Tüm örneklerde ICAO'nun kendi hayali örnek belgesi kullanılıyor: "Utopia"dan Anna Maria Eriksson (UTO, yalnızca örnek belgelerde var olan bir ülke kodu). Hiçbir yerde gerçek bir belge yer almıyor.
Burası doc.cheap'in blogu. doc.cheap, MRZ'yi okuyan ve bu haneleri sunucuda yeniden kontrol eden bir belge tanıma API'sidir. Aşağıdakilerin hiçbiri ona ihtiyaç duymaz; kod çevrimdışı çalışır.
Alfabe
MRZ tam olarak 37 karakter kullanır: 0-9, A-Z ve dolgu karakteri <. Küçük harf, boşluk ya da noktalama işareti yoktur. Aksanlı ya da Latin dışı alfabelerdeki adlar transliterasyonla yazılır, alan içindeki boşluklar < olur. Dolgu karakteri ayrıca her alanı sabit genişliğine tamamlar; yani ERIKSSON<<ANNA<MARIA<<<<<<< "soyadı ERIKSSON, adları ANNA MARIA" demektir ve çift << soyadını adlardan ayırır.
Algoritma: 7, 3, 1 ağırlıkları
Kontrol hanesi, her biçimin her alanı için aynı şekilde hesaplanır:
- Her karakteri bir sayıya çevirin. Rakam kendi değerini alır. Harf, alfabedeki sırasının 9 fazlasıdır:
A= 10,B= 11, …Z= 35. Dolgu karakteri<0'dır. - Tekrarlanan bir ağırlıkla çarpın: alanın ilk karakterinden başlayarak 7, 3, 1, 7, 3, 1, …
- Çarpımları toplayın ve 10'a bölümden kalanı alın. Çıkan tek hane kontrol hanesidir.
Basılı kontrol hanesi 6 olan örnek pasaport numarası L898902C3 üzerinde adım adım:
character L 8 9 8 9 0 2 C 3
value 21 8 9 8 9 0 2 12 3
weight 7 3 1 7 3 1 7 3 1
product 147 24 9 56 27 0 14 36 3
sum = 316 316 mod 10 = 6 the zone prints 6
(Tablodaki satırlar: karakter, değer, ağırlık, çarpım; toplam 316, 316 mod 10 = 6 ve bölgede basılı olan da 6.)
Neden 7-3-1? Ağırlıklar, en sık karşılaşılan okuma hatalarının toplamı değiştireceği şekilde seçilmiştir: tek bir yanlış karakter ve yan yana iki karakterin yer değiştirmesinin birçok durumu. Bu kriptografik bir sağlama toplamı değildir. Herkes hesaplayabilir; dolayısıyla tutan bir hane, belgenin gerçek olduğunu değil, yalnızca bölgenin kendi içinde tutarlı olduğunu kanıtlar.
Üç biçim
ICAO 9303 üç MRZ düzeni tanımlar. Satır sayısı ve satır başına karakter sayısı bunları birbirinden ayırır:
| Biçim | Satır × karakter | Nerede karşınıza çıkar |
|---|---|---|
| TD1 | 3 × 30 | Kimlik kartları, oturma izinleri |
| TD2 | 2 × 36 | Eski kimlik kartları ve bazı seyahat belgeleri |
| TD3 | 2 × 44 | Pasaport defterleri |
Aşağıda kullanılan örnekler:
TD3 P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<
L898902C36UTO7408122F1204159ZE184226B<<<<<10
TD2 I<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<
D231458907UTO7408122F1204159<<<<<<<6
TD1 I<UTOD231458907<<<<<<<<<<<<<<<
7408122F1204159UTO<<<<<<<<<<<6
ERIKSSON<<ANNA<MARIA<<<<<<<<<<
TD3'ün ikinci satırını soldan sağa okuyun: L898902C3 belge numarası, 6 onun kontrol hanesi, UTO uyruk, 740812 doğum tarihi (YYAAGG), 2 onun kontrol hanesi, F cinsiyet, 120415 geçerlilik bitiş tarihi, 9 onun kontrol hanesi, ZE184226B<<<<< isteğe bağlı veri (çoğu zaman kişisel numara), 1 onun kontrol hanesi ve son olarak 0, bileşik kontrol hanesi.
MRZ ayrıştırıcısı üç biçimdeki her alanın ve kontrol hanesinin konumunu bir başvuru tablosu olarak verir; MRZ biçimleri sayfası ise her düzeni tek tek anlatır.
Her kontrol hanesi nerede durur
Konumlar 0'dan başlar, böylece doğrudan slice içine yazılabilir. Her alanın kontrol hanesi alanın hemen ardından gelir.
| Alan | TD3 (2. satır) | TD2 (2. satır) | TD1 |
|---|---|---|---|
| Belge numarası | 0–8, hane 9'da | 0–8, hane 9'da | 1. satır: 5–13, hane 14'te |
| Doğum tarihi | 13–18, hane 19'da | 13–18, hane 19'da | 2. satır: 0–5, hane 6'da |
| Geçerlilik bitiş tarihi | 21–26, hane 27'de | 21–26, hane 27'de | 2. satır: 8–13, hane 14'te |
| İsteğe bağlı veri | 28–41, hane 42'de | yok | yok |
| Bileşik | hane 43'te | hane 35'te | 2. satır: hane 29'da |
Evde yazılmış doğrulayıcıların çoğu bileşik hanede yanılır, çünkü bu hane satırın tamamını kapsamaz:
- TD3: 2. satırın 0–9, 13–19 ve 21–42 konumları. Uyruğu (10–12) ve cinsiyeti (20) atlar.
- TD2: 2. satırın 0–9, 13–19 ve 21–34 konumları. Aynı atlamalar.
- TD1: iki satıra yayılır: 1. satırın 5–29 konumları, ardından 2. satırın 0–6, 8–14 ve 18–28 konumları.
Her aralık, içinde kalan alan kontrol hanelerini de kapsar; bileşik hanenin bu hanelerin kendisindeki hataları da yakalamasını sağlayan budur.
Python'da bir doğrulayıcı
Bağımlılık yok. Biçimi şeklinden tanır, her alan hanesini ve bileşik haneyi kontrol eder, sonuçları bir sözlük olarak döndürür.
WEIGHTS = (7, 3, 1)
def char_value(c):
if c.isdigit():
return int(c)
if "A" <= c <= "Z":
return ord(c) - ord("A") + 10
if c == "<":
return 0
raise ValueError(f"not an MRZ character: {c!r}")
def check_digit(data):
return sum(char_value(c) * WEIGHTS[i % 3] for i, c in enumerate(data)) % 10
def digit_ok(data, printed):
# Yalnızca dolgu karakterinden oluşan bir alan, kontrol hanesi olarak "<" basabilir.
expected = 0 if printed == "<" else int(printed)
return check_digit(data) == expected
# Biçim başına (ad, satır indeksi, başlangıç, bitiş, kontrol hanesinin konumu)
LAYOUTS = {
"TD3": [("document number", 1, 0, 9, 9), ("birth date", 1, 13, 19, 19),
("expiry date", 1, 21, 27, 27), ("personal number", 1, 28, 42, 42)],
"TD2": [("document number", 1, 0, 9, 9), ("birth date", 1, 13, 19, 19),
("expiry date", 1, 21, 27, 27)],
"TD1": [("document number", 0, 5, 14, 14), ("birth date", 1, 0, 6, 6),
("expiry date", 1, 8, 14, 14)],
}
def composite(fmt, lines):
if fmt == "TD3":
l = lines[1]
return l[0:10] + l[13:20] + l[21:43], l[43]
if fmt == "TD2":
l = lines[1]
return l[0:10] + l[13:20] + l[21:35], l[35]
a, b = lines[0], lines[1]
return a[5:30] + b[0:7] + b[8:15] + b[18:29], b[29]
def detect(lines):
shape = (len(lines), len(lines[0]))
fmt = {(2, 44): "TD3", (2, 36): "TD2", (3, 30): "TD1"}.get(shape)
if fmt is None or any(len(l) != shape[1] for l in lines):
raise ValueError(f"unknown MRZ shape: {[len(l) for l in lines]}")
return fmt
def validate(lines):
fmt = detect(lines)
results = {}
for name, li, start, end, pos in LAYOUTS[fmt]:
results[name] = digit_ok(lines[li][start:end], lines[li][pos])
data, printed = composite(fmt, lines)
results["composite"] = digit_ok(data, printed)
return fmt, results
if __name__ == "__main__":
print(*validate(["P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<",
"L898902C36UTO7408122F1204159ZE184226B<<<<<10"]))
print(*validate(["I<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<",
"D231458907UTO7408122F1204159<<<<<<<6"]))
print(*validate(["I<UTOD231458907<<<<<<<<<<<<<<<",
"7408122F1204159UTO<<<<<<<<<<<6",
"ERIKSSON<<ANNA<MARIA<<<<<<<<<<"]))
# Yanlış okunmuş tek karakter: belge numarasındaki 3, 4 olarak okunmuş
print(*validate(["P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<",
"L898902C46UTO7408122F1204159ZE184226B<<<<<10"]))
Çıktı:
TD3 {'document number': True, 'birth date': True, 'expiry date': True, 'personal number': True, 'composite': True}
TD2 {'document number': True, 'birth date': True, 'expiry date': True, 'composite': True}
TD1 {'document number': True, 'birth date': True, 'expiry date': True, 'composite': True}
TD3 {'document number': False, 'birth date': True, 'expiry date': True, 'personal number': True, 'composite': False}
Bütün alıştırmanın amacı son satırdadır: tek bir karakter komşusu olarak yanlış okunmuş ve hem alan hanesi hem bileşik hane bunu işaretliyor.
Aynı doğrulayıcı JavaScript'te
Düz bir ES modülü; Node'da da tarayıcıda da çalışır.
const WEIGHTS = [7, 3, 1];
function charValue(c) {
if (c >= "0" && c <= "9") return c.charCodeAt(0) - 48;
if (c >= "A" && c <= "Z") return c.charCodeAt(0) - 55; // A = 10
if (c === "<") return 0;
throw new Error(`not an MRZ character: ${JSON.stringify(c)}`);
}
export function checkDigit(data) {
let sum = 0;
for (let i = 0; i < data.length; i++) sum += charValue(data[i]) * WEIGHTS[i % 3];
return sum % 10;
}
const digitOk = (data, printed) => checkDigit(data) === (printed === "<" ? 0 : Number(printed));
const LAYOUTS = {
TD3: [["document number", 1, 0, 9], ["birth date", 1, 13, 19], ["expiry date", 1, 21, 27], ["personal number", 1, 28, 42]],
TD2: [["document number", 1, 0, 9], ["birth date", 1, 13, 19], ["expiry date", 1, 21, 27]],
TD1: [["document number", 0, 5, 14], ["birth date", 1, 0, 6], ["expiry date", 1, 8, 14]],
};
function composite(fmt, [a, b]) {
if (fmt === "TD3") return [b.slice(0, 10) + b.slice(13, 20) + b.slice(21, 43), b[43]];
if (fmt === "TD2") return [b.slice(0, 10) + b.slice(13, 20) + b.slice(21, 35), b[35]];
return [a.slice(5, 30) + b.slice(0, 7) + b.slice(8, 15) + b.slice(18, 29), b[29]];
}
export function validate(lines) {
const fmt = { "2x44": "TD3", "2x36": "TD2", "3x30": "TD1" }[`${lines.length}x${lines[0].length}`];
if (!fmt || lines.some((l) => l.length !== lines[0].length)) throw new Error("unknown MRZ shape");
const results = {};
// Kontrol hanesi, koruduğu alanın hemen ardından gelir.
for (const [name, li, start, end] of LAYOUTS[fmt]) {
results[name] = digitOk(lines[li].slice(start, end), lines[li][end]);
}
const [data, printed] = composite(fmt, lines);
results.composite = digitOk(data, printed);
return { format: fmt, results };
}
console.log(validate([
"P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<",
"L898902C36UTO7408122F1204159ZE184226B<<<<<10",
]));
node mrz.mjs komutu format: 'TD3' ve beş kontrolün hepsi için true yazdırır.
Tuzaklar
Dolgu karakterlerini kırpmayın. < karakterleri, hanelerin hesaplandığı verinin parçasıdır. Bir satırın sonundaki < karakterlerini silerseniz, kusursuz bir belgede bileşik kontrol başarısız olur.
Bölgeyi ayrıştırılmış alanlardan yeniden kurmayın. MRZ'yi alanlara ayırıp bunları normalleştirir (tarihleri ISO'ya, adları boşluklu hale getirir), sonra haneleri kontrol etmek için yeniden serileştirirseniz, kendi serileştiricinizi kontrol etmiş olursunuz. Ham satırları okundukları haliyle kontrol edin.
OCR çıktısını doğrulamadan önce normalleştirin, ama dikkatle. OCR motorları küçük harf, boşluk ya da < yerine « döndürmeyi sever. Büyük harfe çevirmek ve boşlukları silmek güvenlidir. "Belge numaraları sayısaldır" diye O harfini 0 ile değiştirmek güvenli değildir: belge numaraları harf içerebilir ve L898902C3 tam olarak bunu gösterir.
Tutan bir hane, gerçek bir tarih demek değildir. 740812, 12 Ağustos 1974 makul olsun ya da olmasın kontrol hanesini geçer ve YYAAGG biçiminde yüzyıl bilgisi yoktur. Yüzyıla bağlamdan karar verin: doğum tarihi geçmiştedir, geçerlilik bitiş tarihi genellikle gelecekte.
TD1'de uzun belge numaraları. ICAO, dokuz karakterden uzun bir TD1 belge numarasının isteğe bağlı veri alanına taşmasına izin verir; bu durumda normal kontrol hanesi konumunda bir < bulunur ve kontrol hanesi numaranın son karakterinden sonra gelir. Yukarıdaki doğrulayıcı bu durumu ele almıyor. Bunu kullanan kurumların kimlik kartlarını işliyorsanız bir dal ekleyin; karşılaştırma yapacak bir şey isterseniz MRZ ayrıştırıcısı bunu destekliyor.
Kontrol haneleri özgünlük demek değildir. Bir görüntüyü düzenleyebilen herkes geçerli bir hane hesaplayabilir. MRZ size bölgenin doğru okunduğunu ve kendi içinde tutarlı olduğunu söyler, belgenin gerçek olduğunu değil. MRZ'yi basılı görsel bölgeyle karşılaştırmak daha güçlü bir sinyaldir, ama o bile bir sahtecilik kontrolü değildir.
Gerçek pasaport olmadan test verisi
Bu kodu test etmek için hiçbir zaman gerçek bir kişinin pasaportuna ihtiyacınız olmamalı. İki seçenek var:
- Yukarıdaki ICAO örnekleri; tam da bu amaçla yayımlanmışlardır.
- Kendiniz üretin: MRZ üreteci, yazdığınız değerlerden doğru kontrol haneleriyle sentetik bir TD3 bölgesini tarayıcıda oluşturur. Ardından tek bir karakteri değiştirin, elinizde başarısız bir test durumu olur.
Ters yön için herhangi bir bölgeyi (TD1, TD2 ya da TD3) MRZ ayrıştırıcısına yapıştırın: biçimi tanır, her alanı okur ve hesaplanan her kontrol hanesini basılı olanın yanında gösterir; hepsi tarayıcıda. Sizin uygulamanızla bir başkasınınki uyuşmadığında işe yarar.
Gerçek bir iş akışında yeri
MRZ'leri kendi OCR'ınızla okuyorsanız, bu kontrolleri her okumada çalıştırın ve bir başarısızlığı "kişiyi reddet" olarak değil, "yeniden fotoğrafla" olarak ele alın: bir karakterin üzerindeki parlama, aşınmış bir laminasyon ya da kırışmış bir sayfa, sahtecilikten çok daha yaygındır.
Bunun yerine barındırılan bir tanıma API'si kullanıyorsanız, sonuç para ya da erişim konusunda karar veriyorsa haneleri yine de kendiniz yeniden hesaplayın. Yanıtın, sağlayıcıya güvenmeden kontrol edebileceğiniz tek kısmı budur. Buna biz de dahiliz: doc.cheap yanıtı, bölgeyi kendi mrz.status kararının yanında mrz.lines ve mrz.text (satırların arada hiçbir şey olmadan birleştirilmiş hali) olarak birebir yayımlar; tam da yukarıdaki gibi bir fonksiyona verebilmeniz için. Belgelerdeki Check an MRZ rehberi (İngilizce) bu akışı anlatıyor.
Doğrulayıcının yanlış sonuç verdiği bir durum bulursanız admin@doc.cheap adresine yazın.
Her iki kod bloğu da çalıştırıldı ve çıktıları yazdırıldığı gibi aktarıldı; doc.cheap hakkındaki her ifade onun koduyla karşılaştırılarak kontrol edildi.