Neden önemli? Her ay 200 PDF, bir Excel
Lezzet Kurye'nin finans ekibi her ay sonunda 30 restorana komisyon faturası keser, bankadan gelen ekstreleri muhasebe sistemine işler ve tedarikçi faturalarındaki tutarları kontrol eder. Bunların hepsi PDF ve Word belgesidir. Bir çalışan bu işi elle yaparsa günler sürer ve kopyala-yapıştır hataları kaçınılmazdır. Python ile belge otomasyonu üç işten oluşur: okumak (PDF'ten metin ve tablo çıkarmak), anlamlandırmak (regex ile alan ayıklamak, doğrulamak) ve üretmek (şablonu doldurup fatura, sözleşme, teklif yazmak). Hepsinin üstünde de kişisel veri (KVKK) sorumluluğu durur.
Ofis ve iş akışı otomasyonu dersinde PDF ve Word'e kısaca dokundun, Düzenli ifadeler (regex): metinde kalıp arama ve çıkarma dersinde kalıpları, Şablonla rapor üretme: string.Template ve Jinja2 dersinde Jinja2'yi öğrendin. Burada bunları belge hattında birleştiriyoruz. pypdf ve python-docx bu sayfada çalışır: dersin sonundaki "Gerçek Word ve PDF dosyası" görevleri gerçek dosya yazıp okur. pdfplumber burada yoktur. Gri kutulardaki kodları bilgisayarında dene. İlk alıştırmalarda bu kütüphanelerin döndürdüğü metinle çalışacaksın: gerçek hayatta da işin zor kısmı PDF'i açmak değil, çıkan metni doğru ayıklamaktır.
PDF'in yapısı: metin katmanı mı, taranmış görüntü mü?
PDF bir "sayfa tarifi"dir: harflerin koordinatlarını, yazı tiplerini ve görüntüleri tutar; paragraf, satır ya da tablo kavramı yoktur. Bu yüzden iki tür PDF ile karşılaşırsın:
| Tür | Nasıl oluşur? | Nasıl okunur? | İpucu |
|---|---|---|---|
| Dijital (metin katmanlı) | Yazılımdan "PDF olarak kaydet", e-Fatura, banka ekstresi | pypdf, pdfplumber, PyMuPDF | Metni fareyle seçebiliyorsun |
| Taranmış (görüntü) | Tarayıcı, telefon fotoğrafı, faks | OCR: Tesseract (pytesseract), bulut OCR servisleri | extract_text() boş döner |
| Karma | Taranmış belgeye sonradan OCR katmanı eklenmiş | Önce metni dene, kalite kötüyse OCR | Rakamlarda O/0, l/1 karışıklığı |
pypdf ile okuma, birleştirme, bölme, döndürme, filigran
# pip install pypdf
from pypdf import PdfReader, PdfWriter
okuyucu = PdfReader("faturalar/LK-2024-10-0001.pdf")
print(len(okuyucu.pages), okuyucu.metadata.title)
metin = okuyucu.pages[0].extract_text()
# Birleştirme: klasördeki tüm faturalar tek dosyada
yazici = PdfWriter()
for yol in sorted(Path("faturalar").glob("*.pdf")):
for sayfa in PdfReader(yol).pages:
yazici.add_page(sayfa)
yazici.write("ekim_faturalari.pdf")
# Bölme ve döndürme: 1-3. sayfalar ayrı dosyaya, 2. sayfa 90 derece
parca = PdfWriter()
for i in (0, 1, 2):
sayfa = okuyucu.pages[i]
if i == 1:
sayfa.rotate(90)
parca.add_page(sayfa)
parca.write("ilk_uc.pdf")
# Filigran: her sayfaya "TASLAK" PDF'ini bindir
damga = PdfReader("taslak_damga.pdf").pages[0]
for sayfa in yazici.pages:
sayfa.merge_page(damga)
pip install pypdf pdfplumber python-docx docxtpl. pypdf saf Python'dur, ek kurulum istemez. OCR için Tesseract programını ayrıca kurman gerekir (Windows'ta UB-Mannheim kurulum paketi, macOS'ta brew install tesseract tesseract-lang, Linux'ta apt install tesseract-ocr tesseract-ocr-tur) ve pip install pytesseract pdf2image.pdfplumber ile tablo, regex ile alan
pdfplumber harflerin koordinatlarını kullanarak satırları ve tabloları yeniden kurar. page.extract_text() okuma sırasına dizilmiş metni, page.extract_tables() hücre listelerini verir. Fatura gibi "etiket: değer" yapısındaki belgelerde ise en sağlam yol metni alıp regex ile alanları ayıklamaktır.
# pip install pdfplumber
import pdfplumber
with pdfplumber.open("ekstre_2024_03.pdf") as pdf:
metin = "\n".join(s.extract_text() or "" for s in pdf.pages)
tablolar = [t for s in pdf.pages for t in s.extract_tables()]
print(tablolar[0][:3]) # [['Tarih', 'Açıklama', 'Tutar'], ['03.03.2024', 'HAVALE', '3.300,00'], ...]
Aşağıda extract_text() çıktısı gibi bir fatura metni var (Lezzet Kurye'nin Ekim 2024 verisinden: Köşe Dönercisi'nin teslim edilen siparişlerinin brüt tutarı 44.005 TL, komisyon %15). Her alan için ayrı, satır başına çapalı (^ ve re.M) bir kalıp yazıyoruz:
import re
fatura = """LEZZET KURYE TEKNOLOJİ A.Ş.
KOMİSYON FATURASI
Fatura No: LK-2024-10-0001
Fatura Tarihi: 31.10.2024
Alıcı: Köşe Dönercisi
VKN: 4810527396
Brüt satış: 44.005,00 TL
Komisyon oranı: %15
Ara Toplam: 6.600,75 TL
KDV (%20): 1.320,15 TL
Genel Toplam: 7.920,90 TL
"""
def tr_sayi(metin):
"""'44.005,00' -> 44005.0 (binlik nokta, ondalık virgül)"""
return float(metin.replace(".", "").replace(",", "."))
alanlar = {
"no": re.search(r"^Fatura No:\s*(\S+)", fatura, re.M).group(1),
"tarih": re.search(r"^Fatura Tarihi:\s*(\d{2}\.\d{2}\.\d{4})", fatura, re.M).group(1),
"vkn": re.search(r"^VKN:\s*(\d{10})$", fatura, re.M).group(1),
"toplam": tr_sayi(re.search(r"^Genel Toplam:\s*([\d.,]+)", fatura, re.M).group(1)),
}
print(alanlar)
float("44.005,00") hata verir; daha kötüsü float("1.430") sessizce 1,43 olur. Önce binlik noktaları sil, sonra virgülü noktaya çevir. İkinci tuzak: re.search(r"Toplam:") metindeki ilk eşleşmeyi alır, yani "Ara Toplam"ı. Kalıbı satır başına çapala ve etiketi tam yaz.Şablon doldurma: string.Template, Jinja2 ve python-docx
Sözleşme, teklif ve fatura metinleri sabit bir iskelet ile değişen alanlardan oluşur. Hukuk biriminden gelen ve kod bilmeyen birinin düzenleyeceği basit metinler için string.Template idealdir: $ad ve ${tutar} yer tutucuları dışında hiçbir şey çalıştırmaz. substitute eksik alanda KeyError verir, safe_substitute yer tutucuyu olduğu gibi bırakır; bu yüzden ikincisini kullanıp geriye kalan $ alanlarını denetlemek iyi bir kalıptır.
from string import Template
import re
sablon = Template("""KREDİ KARTI LİMİT ARTIŞ BİLDİRİMİ
Sayın $ad_soyad,
$hesap numaralı kartınızın limiti ${yeni_limit} TL olarak güncellenmiştir.
Geçerlilik tarihi: $tarih""")
alanlar = {"ad_soyad": "İrem Çelik", "hesap": "100019", "yeni_limit": "35.000,00"}
metin = sablon.safe_substitute(alanlar)
eksik = sorted(set(re.findall(r"\$\{?(\w+)\}?", metin)))
print(metin)
print("Eksik alanlar:", eksik)
Döngü, koşul ve biçimlendirme gereken belgelerde Jinja2 kullan (Şablonla rapor üretme: string.Template ve Jinja2 dersinde ayrıntısıyla gördün). Kendi tl filtrenle Türkçe para biçimi kurabilirsin:
from jinja2 import Environment, StrictUndefined
def tr_para(x):
return f"{x:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
env = Environment(undefined=StrictUndefined, trim_blocks=True, lstrip_blocks=True)
env.filters["tl"] = tr_para
sablon = env.from_string("""Fatura: {{ no }}
{% for s in satirlar %}
{{ "%-20s"|format(s.ad) }} {{ s.adet }} x {{ s.fiyat|tl }} = {{ (s.adet * s.fiyat)|tl }} TL
{% endfor %}
Toplam: {{ satirlar|sum(attribute="tutar")|tl }} TL""")
satirlar = [{"ad": "Tavuk Döner Dürüm", "adet": 3, "fiyat": 140.0},
{"ad": "Patates Kızartması", "adet": 1, "fiyat": 65.0},
{"ad": "Et Döner Porsiyon", "adet": 1, "fiyat": 245.0}]
for s in satirlar:
s["tutar"] = s["adet"] * s["fiyat"]
print(sablon.render(no="SIP-2", satirlar=satirlar))
Word belgesi için python-docx paragrafları ve tabloları düzenler; ama bir yer tutucu Word içinde birkaç "run"a bölünmüş olabileceği için düz replace çoğu zaman işe yaramaz. Bu yüzden şablon doldurmada Jinja2 sözdizimini Word içinde kullanan docxtpl tercih edilir:
# pip install python-docx docxtpl
from docx import Document
from docxtpl import DocxTemplate
belge = Document("teklif.docx")
for p in belge.paragraphs:
print(p.style.name, p.text)
tablo = belge.tables[0]
tablo.add_row().cells[0].text = "Kurulum hizmeti"
belge.save("teklif_guncel.docx")
# Şablonda: "Sayın {{ musteri }}, {% for k in kalemler %}{{ k.ad }} ... {% endfor %}"
sablon = DocxTemplate("sozlesme_sablonu.docx")
sablon.render({"musteri": "Köşe Dönercisi", "oran": "%15", "kalemler": kalemler})
sablon.save("sozlesmeler/kose_donercisi.docx")
HTML'den PDF ve OCR
Biçimli PDF üretmenin en rahat yolu önce Jinja2 ile HTML üretip WeasyPrint ile PDF'e çevirmektir: CSS ile sayfa düzeni, logo ve tablo biçimi tasarımcının bildiği dilde kalır. Taranmış belgede ise önce OCR gerekir; OCR çıktısı hatalı olabileceği için ayıkladığın alanları mutlaka doğrula (toplam = ara toplam + KDV mi? VKN 10 hane mi?).
# pip install weasyprint pytesseract pdf2image
from weasyprint import HTML
HTML(string=html_fatura).write_pdf("LK-2024-10-0001.pdf")
from pdf2image import convert_from_path
import pytesseract
sayfalar = convert_from_path("taranmis_fatura.pdf", dpi=300)
metin = "\n".join(pytesseract.image_to_string(s, lang="tur") for s in sayfalar)
Toplu işlem ve hata dayanıklılığı
Gerçek klasörde 200 dosyanın birkaçı mutlaka bozuktur: şifreli PDF, eksik alan, farklı şablon. Tek bir dosya yüzünden bütün işin düşmesi kabul edilemez. Her dosyayı try/except içinde işle, başarılıları özet tabloya, başarısızları sebebiyle birlikte hata listesine yaz; iş bitince ikisini de raporla. Aşağıdaki kod geçici bir klasöre faturalar yazıp toplu okuyor:
import tempfile
from pathlib import Path
klasor = Path(tempfile.mkdtemp())
(klasor / "LK-0001.txt").write_text(fatura, encoding="utf-8")
(klasor / "LK-0002.txt").write_text(fatura.replace("LK-2024-10-0001", "LK-2024-10-0002").replace("7.920,90", "5.104,12"), encoding="utf-8")
(klasor / "LK-0003.txt").write_text("Bu dosya bozuk", encoding="utf-8")
ozet, hatalar = [], []
for yol in sorted(klasor.glob("*.txt")):
try:
metin = yol.read_text(encoding="utf-8")
no = re.search(r"^Fatura No:\s*(\S+)", metin, re.M).group(1)
toplam = tr_sayi(re.search(r"^Genel Toplam:\s*([\d.,]+)", metin, re.M).group(1))
ozet.append((no, toplam))
except AttributeError:
hatalar.append((yol.name, "alan bulunamadı"))
print(ozet)
print(hatalar)
Kişisel veri (KVKK) maskeleme
Ekstre, sözleşme ve başvuru belgeleri ad soyad, T.C. kimlik numarası, IBAN, telefon ve e-posta içerir. Bu belgeleri test ortamına, bir yapay zekâ servisine ya da başka bir ekibe göndermeden önce maskelemen gerekir. Maskeleme ile anonimleştirme aynı şey değildir: maskelenmiş veri başka kaynaklarla birleştirilerek kişiye bağlanabiliyorsa hâlâ kişisel veridir. Yalnız gereken alanı paylaş (veri minimizasyonu), maskelemeyi kaynağa en yakın adımda yap ve log'lara ham veri yazma.
import re
def isim_maskele(ad_soyad):
return " ".join(k[0] + "*" * (len(k) - 1) for k in ad_soyad.split())
def kvkk_maskele(metin):
metin = re.sub(r"\b(\w)[\w.]*@", r"\1***@", metin) # e-posta
metin = re.sub(r"\b0?5\d{2}[ ]?\d{3}[ ]?\d{2}[ ]?(\d{2})\b", r"05** *** ** \1", metin) # cep tel.
metin = re.sub(r"\bTR\d{2}(?:[ ]?\d{4}){5}[ ]?(\d{2})\b", r"TR** **** **** **** **** **\1", metin) # IBAN
return metin
print(isim_maskele("İrem Çelik"))
print(kvkk_maskele("İletişim: irem.celik@ornek.com, 0532 123 45 67, IBAN TR33 0006 1005 1978 6457 8413 26"))
Gerçek Word ve PDF dosyası
Aşağıdaki görevler tarayıcıda gerçek .docx ve .pdf yazar, açar ve okur; sonucu indirip kendi programında görebilirsin. İlk kullanımda inenler: python-docx yaklaşık 250 KB, pypdf yaklaşık 410 KB, reportlab yaklaşık 2 MB.
from docx import Document
d = Document()
d.add_heading("Kira sözleşmesi", level=1)
d.add_paragraph("Taraflar aşağıdadır.")
d.save("ornek.docx")
print([p.text for p in Document("ornek.docx").paragraphs])
| İş | Burada | Bilgisayarında |
|---|---|---|
| Word okuma, yazma, şablon | python-docx | docxtpl ile Jinja2 şablon |
| PDF metin okuma, birleştirme, bölme | pypdf | Tablo için pdfplumber |
| PDF üretme | reportlab | HTML'den WeasyPrint |
| Taranmış PDF (OCR) | Yok | pytesseract |
pdfmetrics.registerFont(TTFont(...)) ile bir TTF yazı tipi kaydet. Görevler bu yüzden İngilizce harflerle yazılmış metin kullanır.RPA araçları ve Python
UiPath ve Power Automate ekrandaki düğmelere basar; API'si olmayan eski programlar için uygundur, kod bilmeyen ekipler de kullanır ama lisans ister ve ekran değişince kırılır. Python dosya ve veri işinde daha sağlamdır, test edilir, ücretsizdir ama ekran otomasyonu zayıftır. Önce API ya da dosya yolunu ara; yoksa RPA düşün.
Şablona göre ayrıştırıcı seçmek
Farklı tedarikçilerin faturaları farklı düzendedir. Her şablon için bir ayrıştırıcı fonksiyon yaz ve metindeki imza ifadeye (ör. "LEZZET KURYE" ya da VKN) göre doğru fonksiyonu seçen bir yönlendirici kur. Yeni tedarikçi geldiğinde yalnız bir fonksiyon eklersin; tanınmayan şablon hata kuyruğuna gider.
Koordinatlı ayıklama
Etiketsiz alanlarda (ör. sağ üst köşedeki tarih) pdfplumber'ın page.within_bbox((x0, top, x1, bottom)) ile belirli bir dikdörtgenin metnini alırsın. Bu yöntem şablona çok bağlıdır; şablon değişince kırılır, bu yüzden testlerle korunmalıdır.
Belge yapay zekâsı
Çok çeşitli ve dağınık belgelerde (el yazısı, karışık düzen) bulut belge servisleri (Azure Document Intelligence, Google Document AI, AWS Textract) ya da çok kipli dil modelleri alanları JSON olarak çıkarabilir. Yine de çıktıyı aynı doğrulama kurallarından geçirmek ve kişisel veriyi göndermeden önce maskelemek gerekir.