Python'da bir pasaport okuyucu ilk seferde yaklaşık yirmi satırdır, üretimde ayakta kalması gerektiğinde ise yaklaşık yüz satır. Aradaki seksen satırın OCR ile ilgisi yoktur. Her ücretli API'nin doğurduğu üç soruyla ilgilidir: fotoğraf kötüyse ne olur, bir istek zaman aşımına uğrar ve onu yeniden gönderirseniz ne olur, ve kendi kayıtlarınız hangi çağrıların para tuttuğunu nereden bilir.
Bu eğitim o betiği yalnızca requests ile kuruyor. doc.cheap adlı bir pasaport ve kimlik OCR API'si kullanıyor ve bu yazı doc.cheap'in kendi blogunda yayımlanıyor; ürün tercihlerini buna göre tartın. Kalıplar (her görüntü için bir idempotency anahtarı, sabit bir hata koduna göre dallanma, çağrı başına bir maliyet bayrağını saklama) Python'dan çağırdığınız her ücretli API'ye aynen taşınır.
Sandbox anahtarıyla tek istek
Belgeler herkese açık bir sandbox anahtarı veriyor: sk_sandbox_public. Ücretli anahtarla aynı tanımayı çalıştırır ve hesap gerektirmez: IP adresi başına toplam 10 ücretsiz tanınan belge ve yanıt ne olursa olsun saatte en fazla 10 istek. Ücretsiz pasaport OCR API'si sayfasında aynı ilk çağrı tek bir curl komutu olarak yer alıyor. Daha sonra açacağınız bir hesap, kart gerektirmeden her ay 100 ücretsiz belge ekler.
import base64
import requests
with open("specimen.jpg", "rb") as f:
image = base64.b64encode(f.read()).decode("ascii")
response = requests.post(
"https://api.doc.cheap/v1/scans",
headers={"Authorization": "Bearer sk_sandbox_public"},
json={"image": image},
timeout=60,
)
scan = response.json()
print(response.status_code, scan["meta"]["status"], scan["meta"]["billed"])
json= sizin yerinize Content-Type: application/json başlığını ayarlar. Görüntü gövdenin içinde base64 olarak gider, JPEG ya da PNG. Çağrı eşzamanlıdır: alanlar bu yanıtla geri gelir; sorgulanacak bir iş kimliği ya da barındırılacak bir webhook yoktur.
Kendi pasaportunuzla değil, sentetik bir örnekle test edin. ICAO'nun kurgusal "Utopia" belgeleri ve birçok belge düzenleyicinin yayımladığı örnek sayfalar tam da bunun için vardır.
Önce fotoğrafı küçültün. Bir telefon fotoğrafı, base64 üçte bir eklemeden önce bile birkaç megabayt olabilir. Pasaport kılavuzu uzun kenarda yaklaşık 1600 piksel ve 85 JPEG kalitesi öneriyor; sınırı aşan bir gövde, hiçbir şey çalışmadan payload_too_large ile reddedilir.
Yanıtı okumak
Yanıtın her anahtarı her zaman vardır; bilinmeyen bir değer json() sonrasında None olur, asla eksik bir anahtar değildir. Kodunuzun bundan sonra ne yapacağını dört bölüm belirler:
meta.statusbelgenin okunup okunmadığını söyler. Beş dizeden biridir:recognized,no_document_found,unreadable,unsupported_document,rejected. Yalnızca ilki veri taşır.documentveholderdüzenlenmiş değerleri tutar:document.kind(passport, kimlik kartı vb.), ISO 3166-1 alpha-3 biçiminde ülke, numara, ISOYYYY-MM-DDbiçiminde tarihler;holder.surname,holder.given_names,holder.birth_date. Tarama bir grup için hiçbir şey üretmediğinde o grubun tamamıNoneolur.mrz.statuspassed,failedya daabsentdeğerini alır: makinece okunabilir bölgenin bulunup bulunmadığı ve kontrol basamaklarının tutup tutmadığı.mrz.textbölgenin okunduğu hâlidir; kontrol basamaklarını kendiniz de hesaplayabilirsiniz.meta.billedbu çağrının bakiyeden düşülüp düşülmediğini söyler.
Tüm istemciyi şekillendiren nokta şu: okunamayan bir fotoğraf hata değil, HTTP 200 döner. Bu yüzden kod iki kez dallanır: retler için HTTP hata koduna, sonuçlar için meta.status değerine göre. no_document_found durumunda istisna fırlatırsanız yeniden deneme döngünüz asla okunmayacak bir fotoğrafı tekrar tekrar gönderir. Başarı sayarsanız alanları olmayan bir belge kaydedersiniz.
billed bayrağı
Canlı bir anahtarda billed yalnızca bir belge gerçekten tanındığında True olur. Hiçbir şey bulunamaması, okunamayan bir görüntü, desteklenmeyen bir tür, hizmet tarafında bir arıza: bunların hiçbiri ücretlendirilmez. Ücretlendirilen bir belge her hacimde sabit $0.01 tutar; pasaport OCR API karşılaştırması bunu diğer hizmetlerin yayımladığı fiyatların yanına koyuyor.
Sandbox hiçbir şey ücretlendirmez. sk_sandbox_public üzerinde billed yine de aynı taramanın canlı bir anahtarda ücretlendirilip ücretlendirilmeyeceğini söyler ve sandbox'ı test için değerli kılan da budur. Bayrağı her sonucun yanında saklayın: bir ay boyunca kendi billed satırlarınızı toplamak, faturayla mutabakat gerektirmez.
Çift ücret alamayan yeniden denemeler
Riskli yeniden deneme, zaman aşımından sonrakidir. İlk isteğin sunucuya ulaşıp ulaşmadığını bilmezsiniz ve ücretli bir API'de körlemesine bir yeniden deneme tek bir görüntü için iki kez ödeme yapabilir.
Çözüm, POST /v1/scans üzerinde 1 ile 255 karakter arası bir Idempotency-Key başlığıdır. Onu her görüntü için bir kez üretin ve her denemede aynı değeri gönderin. Canlı bir anahtarda, aynı anahtar ve aynı gövdeyle yapılan bir yeniden deneme ikinci bir tanıma yerine ilk sonucu geri alır ve bu tekrar oynatma hiçbir şeye mal olmaz. Başvuru belgesindeki üç kural kodu şekillendirir:
- Anahtar gövdeye bağlıdır. Aynı anahtar farklı bir görüntü ya da farklı seçeneklerle
409 idempotency_conflictdöner. Gövdeyi döngünün dışında bir kez oluşturun. - İkinci deneme, ilki hâlâ çalışırken gelebilir. Bu
409 idempotency_in_progressdemektir: bekleyin ve aynı anahtarla yeniden deneyin. - Saklanan sonuç yoksa tekrar oynatma da yoktur.
retain_hours: 0ile hiçbir şey saklanmaz; bu yüzden 24 saat içinde aynı anahtarla yapılan bir yeniden deneme ikinci bir çalıştırma yerine409 idempotency_replay_unavailablealır. Hiçbir şey saklamamak ile bir sonucu tekrar oynatmak bir arada olmaz; kullanım durumuna göre seçin.
Sandbox anahtarları başlığı kabul eder ama orada hiçbir şey ücretlendirilmediği için başlık bir şey belirlemez. Bu kod yolunu test etmeye yine de değer.
Hangi hatalar yeniden denemeye değer. Her hata gövdesi aynı biçimdedir (code, message, docs_url, request_id, event_id) ve dallanılacak olan koddur, çünkü tek bir HTTP durumu birbirinin tersi işlem gerektiren kodlar taşıyabilir. Hataları ele alma kılavuzu onları gruplara ayırıyor:
| Kodlar | Ne yapmalı |
|---|---|
rate_limited, document_repeated, internal_error, engine_unavailable, service_unavailable, maintenance |
Bekleyin (Retry-After değerine uyun), sonra yeniden deneyin |
idempotency_in_progress |
Bekleyin, aynı anahtarla yeniden deneyin |
validation_failed, invalid_request, payload_too_large, unsupported_media_type |
İsteği düzeltin; yeniden deneme yine başarısız olur |
unauthorized, registration_required, insufficient_credits |
Anahtarı ya da hesabı düzeltin; beklemek hiçbir şeyi değiştirmez |
Retry-After tam saniye cinsindendir. Sandbox'ın saatlik sınırı neredeyse bir saatlik bekleme isteyebilir ve hiçbir çağıran tek bir fonksiyon çağrısının o kadar uyumasını istemez; bu yüzden aşağıdaki istemci bekleme bir dakikayı aştığında vazgeçer.
İstemcinin tamamı
# scan.py
import base64
import os
import time
import uuid
import requests
API = "https://api.doc.cheap/v1/scans"
KEY = os.environ.get("DOC_CHEAP_API_KEY", "sk_sandbox_public")
RETRY = {
"rate_limited", "document_repeated", "internal_error", "engine_unavailable",
"service_unavailable", "maintenance", "idempotency_in_progress",
}
MAX_WAIT = 60 # saniye; daha uzun bir Retry-After beklenmez, bildirilir
class ScanError(Exception):
def __init__(self, status, error):
super().__init__(f"{error['code']} ({status}): {error['message']}")
self.code = error["code"]
self.docs_url = error["docs_url"]
self.request_id = error["request_id"]
def retry_after(response, attempt):
try:
seconds = int(response.headers.get("Retry-After", ""))
except ValueError:
seconds = 0
return seconds if seconds > 0 else 2 ** attempt
def scan(path, reference=None, attempts=4, session=None):
session = session or requests.Session()
with open(path, "rb") as f:
image = base64.b64encode(f.read()).decode("ascii")
body = {"image": image, "reference": reference, "options": {"return_portrait": False}}
headers = {
"Authorization": f"Bearer {KEY}",
"Idempotency-Key": str(uuid.uuid4()), # görüntü başına bir anahtar, her denemede yeniden kullanılır
}
for attempt in range(1, attempts + 1):
last = attempt == attempts
try:
response = session.post(API, headers=headers, json=body, timeout=60)
payload = response.json()
except (requests.RequestException, ValueError):
# Zaman aşımı, kopan bağlantı ya da HTML ile yanıt veren bir proxy.
if last:
raise
time.sleep(2 ** attempt)
continue
if response.ok:
return payload
error = payload["error"]
if error["code"] not in RETRY or last:
raise ScanError(response.status_code, error)
wait = retry_after(response, attempt)
if wait > MAX_WAIT:
raise ScanError(response.status_code, error)
time.sleep(wait)
def summarise(scan):
meta, document, holder, mrz = scan["meta"], scan["document"], scan["holder"], scan["mrz"]
if meta["status"] != "recognized":
return {"ok": False, "status": meta["status"], "billed": meta["billed"]}
document, holder = document or {}, holder or {}
return {
"ok": True,
"billed": meta["billed"],
"kind": document.get("kind"),
"country": document.get("country"),
"expiry_date": document.get("expiry_date"),
"surname": holder.get("surname"),
"birth_date": holder.get("birth_date"),
"mrz": mrz["status"],
}
if __name__ == "__main__":
import sys
try:
print(summarise(scan(sys.argv[1], reference="demo-1")))
except ScanError as err:
print(err, err.docs_url, err.request_id, file=sys.stderr)
sys.exit(1)
python scan.py specimen.jpg komutuyla çalıştırın. Açıklamaya değer birkaç tercih:
requestsbir 4xx ya da 5xx yanıtında istisna fırlatmaz, sizraise_for_status()çağırmadıkça. İstemci JSON gövdesini her durumda okur, çünkü kod hata gövdesindedir;raise_for_status()onu çöpe atardı.- Gövde ve anahtar döngüden önce bir kez oluşturulur. Her denemeyi sunucunun gözünde aynı istek yapan budur.
- Bir
Sessionbağlantıyı yeniden denemeler ve bir görüntü grubu boyunca yeniden kullanır. Çok sayıda dosya tarıyorsanız bir tane geçirin. return_portrait: Falsebelge sahibinin fotoğraf kırpımını dışarıda bırakır. Sayfa kırpımını ve alanları alırsınız; günlüklerinizde ve depolamanızda bir yüz daha az durur.referencemeta.referenceolarak geri gelir (en fazla 128 karakter): bir taramayı kendi siparişinize ya da kullanıcınıza bağlamanın kolay yolu. Bir kimlik kartının iki yüzü iki çağrıdır; ikisine aynı referansı verin.- İstek gövdesini asla günlüğe yazmayın. O bir kimlik belgesidir.
code,request_idvedocs_urldeğerlerini yazın; desteğin ihtiyaç duyduğu her şey bunlardır.
Daha az veri tutmak
Yüklenen görüntü istek süresince bellekte tutulur ve kalıcı depolamaya asla yazılmaz. Sonuç, GET /v1/scans/{id} ile yeniden alabilmeniz için seçtiğiniz bir süre boyunca saklanır: istek başına options.retain_hours 0 ile 8760 arası değer alır ve 0 hiçbir şey saklamaz. JSON'a yalnızca bir kez ihtiyacınız varsa retain_hours: 0 gönderin ve yukarıdaki tekrar oynatma ödünleşimini kabul edin. İşleme AB'de yapılır.
Canlıya geçiş
DOC_CHEAP_API_KEY değişkenini kendi anahtarınıza ayarlayın, başka hiçbir şey değişmez: aynı uç nokta, aynı yanıt biçimi, aynı istemci. Kayıtlı bir anahtar dakikada 60 isteğe izin verir; bu yüzden Retry-After değerine uyan bir toplu iş kendi hız sınırlayıcısına ihtiyaç duymaz. Krediler $1'dan başlayarak kripto parayla (BTC, ETH, TRX ya da Ethereum veya Tron üzerinde USDT) satın alınır; bugün kartla ödeme yoktur.
Yanıttaki bir şeyi Python'dan ele almak zahmetliyse admin@doc.cheap adresine yazın.