Ana içeriğe geç
Kesit Python
Orta · Otomasyon, web ve araçlar

PDF ve Word belgeleriyle otomasyon

PDF'in metin katmanı ile taranmış görüntü farkını, pypdf ile birleştirme, bölme ve filigranı, pdfplumber ile tablo ve metin çıkarmayı, regex ile fatura ve ekstre alanı ayıklamayı, string.Template, Jinja2 ve docxtpl ile şablon doldurmayı, toplu işlemde hata dayanıklılığını ve KVKK maskelemeyi Lezzet Kurye ve Ilgaz Bank verisiyle öğrenirsin.

Uygulamada aç, 25 alıştırmayı çözÜcretsiz, hesap gerekmez, kurulum yok.

Bu sayfa dersin okuma sürümüdür. Metin bir kutuyu çalıştırmanı ya da adıma geçmeni söylüyorsa dersi uygulamada aç.

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ürNasıl oluşur?Nasıl okunur?İpucu
Dijital (metin katmanlı)Yazılımdan "PDF olarak kaydet", e-Fatura, banka ekstresipypdf, pdfplumber, PyMuPDFMetni fareyle seçebiliyorsun
Taranmış (görüntü)Tarayıcı, telefon fotoğrafı, faksOCR: Tesseract (pytesseract), bulut OCR servisleriextract_text() boş döner
KarmaTaranmış belgeye sonradan OCR katmanı eklenmişÖnce metni dene, kalite kötüyse OCRRakamlarda 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)
Kendi bilgisayarında denepip 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)
Türkçe sayı biçimi ve ilk eşleşme tuzağı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"))
Doğrula, sonra güvenAyıklanan her belgeye bir tutarlılık kontrolü ekle: ara toplam + KDV = genel toplam, satır tutarları toplamı = ara toplam, tarih dönemin içinde, VKN 10 hane, T.C. kimlik numarası sağlama algoritmasını geçiyor. Kontrolden geçmeyen belge otomatik işlenmez, insan onayı kuyruğuna düşer. Bu, "otomasyon yanlış tutarı muhasebeye işledi" vakalarının çoğunu önler.
Gerçek işteMuhasebe, satın alma ve İK ekiplerinde en çok talep gören otomasyonlar şunlardır: tedarikçi faturalarından tutar ve vade çekip Excel'e dökmek, banka ekstrelerini muhasebe kodlarıyla eşleştirmek, yüzlerce kişiye kişiselleştirilmiş sözleşme ya da bordro PDF'i üretmek, bir klasördeki belgeleri birleştirip arşivlemek. RPA araçları (UiPath, Power Automate) da bu işlerde arka planda çoğu zaman bir Python adımı çalıştırır.
Derinleş

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])
İşBuradaBilgisayarında
Word okuma, yazma, şablonpython-docxdocxtpl ile Jinja2 şablon
PDF metin okuma, birleştirme, bölmepypdfTablo için pdfplumber
PDF üretmereportlabHTML'den WeasyPrint
Taranmış PDF (OCR)Yokpytesseract
Türkçe harfreportlab'ın hazır yazı tiplerinde ş, ğ, ı gibi harfler yoktur. Gerçek belgede 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.

Uzmanlaş

Ş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.

Alıştırmalara geç (25)