Ana içeriğe geç
Kesit Python
Orta · Standart kütüphane

Düzenli ifadeler (regex): metinde kalıp arama ve çıkarma

re.search/match/findall/sub ile kalıp arar, gruplarla veri çıkarır, telefon/e-posta/IBAN benzeri doğrulamalar yazar ve regex tuzaklarından kaçınırsın.

Uygulamada aç, 22 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?

Ilgaz Bank'a gelen EFT açıklamalarından fatura numarasını çıkarmak, Anadolu Market müşteri formundaki telefonun doğru biçimde olduğunu kontrol etmek, log dosyasından hata kodlarını toplamak... Bunların ortak noktası: metin içinde kalıp (pattern) aramak. str.find ve in sabit metin arar; "5 ile başlayan 10 haneli sayı" gibi kalıpları ise düzenli ifadeler (regular expression, regex) bulur. Regex küçük bir dildir; doğru kullanınca 20 satırlık ayrıştırma kodu bir satıra iner, yanlış kullanınca kimsenin okuyamadığı bir satır çıkar. Bu ders ikisi arasındaki çizgiyi öğretir.

Temel sözlük

YapıAnlamÖrnek
\d \w \sRakam / harf-rakam-alt çizgi / boşluk\d{3} → 3 rakam
\D \W \SYukarıdakilerin tersi\D+ → rakam olmayanlar
.Satır sonu dışında herhangi bir karaktera.c → abc, a-c
[abc] [^abc] [a-z]Küme / küme dışı / aralık[A-Z]{2} → iki büyük harf
* + ? {n,m}0+ / 1+ / 0-1 / n ile m arası\d{2,4}
^ $ \bSatır başı / sonu / kelime sınırı^TR → TR ile başlar
( ) (?P<ad> ) (?: )Grup / adlandırılmış grup / yakalamayan grup(?P<yil>\d{4})
a|bVeyaTL|USD|EUR
Her zaman ham dize kullanKalıpları r"..." ile yaz. Aksi halde Python "\b"yi geri silme (backspace) karakteri sanır, "\d" için uyarı verir. r öneki ters eğik çizgiyi olduğu gibi bırakır.

Dört ana fonksiyon

re.search metnin herhangi bir yerinde ilk eşleşmeyi bulur; re.match yalnızca baştan eşleşir; re.fullmatch tüm metnin kalıba uymasını ister (doğrulama için en doğrusu); re.findall tüm eşleşmeleri liste olarak verir. search/match/fullmatch eşleşme yoksa None döndürür; bu yüzden sonucu önce if ile kontrol edersin.

import re

aciklama = "EFT - Fatura No: FT-2026-00431, tutar 1.250,00 TL"
m = re.search(r"FT-\d{4}-\d{5}", aciklama)
print("search :", m.group() if m else None, "| konum:", m.span())
print("match  :", re.match(r"FT-", aciklama))          # baş 'EFT' olduğu için None
print("match2 :", re.match(r"EFT", aciklama).group())
print("full   :", re.fullmatch(r"\d{10}", "5321234567") is not None)
print("full2  :", re.fullmatch(r"\d{10}", "532123456X") is not None)
print("findall:", re.findall(r"\d+", aciklama))
print("bos    :", re.search(r"\d+", "rakam yok"))      # None

Gruplar: eşleşmenin parçalarını almak

Parantez, eşleşmenin bir bölümünü yakalar (capture). group(1) ilk grubu, groups() hepsini verir. Adlandırılmış gruplar (?P<ad>...) ile kodu okunur yapar; groupdict() doğrudan sözlük döndürür. Yakalamak istemediğin ama gruplaman gereken yerde (?:...) kullan.

import re

satir = "2026-03-15 14:30:07 ERROR odeme-servisi: zaman asimi (kod=504)"
kalip = r"(?P<tarih>\d{4}-\d{2}-\d{2}) (?P<saat>[\d:]+) (?P<seviye>[A-Z]+) (?P<servis>[\w-]+): (?P<mesaj>.*)"
m = re.search(kalip, satir)
print(m.group("seviye"), "-", m.group("servis"))
print(m.groupdict())
print(m.group(1), m.group(3))      # sıra numarasıyla da erişilir

# findall: grup varsa yalnızca grupları döndürür
fiyatlar = "laptop 25.999 TL, mouse 349 TL, çanta 1.200 TL"
print(re.findall(r"(\w+) ([\d.]+) TL", fiyatlar))
# finditer: eşleşme nesneleriyle gez
for e in re.finditer(r"(?P<urun>\w+) (?P<fiyat>[\d.]+) TL", fiyatlar):
    print(e["urun"], "→", e["fiyat"].replace(".", ""))

re.sub: bul ve değiştir

re.sub(kalip, yeni, metin) tüm eşleşmeleri değiştirir. yeni içinde \1 ya da \g<ad> ile grup kullanabilirsin; karmaşık dönüşümlerde yeni yerine fonksiyon verirsin. KVKK gereği kart numarası maskeleme tipik bir örnektir.

import re

mesaj = "Kart 5401 2345 6789 1234 ile 350 TL ödeme; TC 12345678901 doğrulandı."
# Kartın ilk 4 ve son 4'ünü bırak, ortayı maskele
maskeli = re.sub(r"(\d{4}) \d{4} \d{4} (\d{4})", r"\1 **** **** \2", mesaj)
print(maskeli)
# 11 haneli sayıyı tamamen gizle
print(re.sub(r"\b\d{11}\b", "***********", maskeli))
# Tarih biçimi değiştir: 2026-03-15 -> 15.03.2026
print(re.sub(r"(\d{4})-(\d{2})-(\d{2})", r"\3.\2.\1", "Rapor tarihi 2026-03-15"))
# Fonksiyonla değiştirme: fiyatları yüzde 10 artır
print(re.sub(r"\d+", lambda m: str(int(m.group()) * 11 // 10), "mouse 350 TL, çanta 1200 TL"))
# Fazla boşlukları teke indir, kaç tane değişti?
yeni, adet = re.subn(r"\s+", " ", "çok    fazla   boşluk")
print(yeni, "|", adet)

İş kalıpları: telefon, e-posta, IBAN

Kurumsal formlarda en çok karşılaşılan üç doğrulama. Amaç "biçimsel olarak makul mü?" sorusuna cevap vermektir; gerçek doğrulama (IBAN kontrol basamağı, e-posta adresinin var olması) regex'in işi değildir.

import re

TELEFON = re.compile(r"^(?:\+90|0)?\s?5\d{2}\s?\d{3}\s?\d{2}\s?\d{2}$")
EPOSTA = re.compile(r"^[\w.+-]+@[\w-]+(?:\.[\w-]+)+$")
IBAN_TR = re.compile(r"^TR\d{2}\s?(?:\d{4}\s?){5}\d{2}$")   # TR + 24 hane = 26 karakter

for t in ["0532 123 45 67", "+90 532 123 45 67", "5321234567", "0212 123 45 67", "532 12 34"]:
    print(f"{t:>18} →", bool(TELEFON.match(t)))
for e in ["ayse.yilmaz@anadolumarket.com.tr", "ali@firma", "kötü@@adres.com"]:
    print(f"{e:>34} →", bool(EPOSTA.match(e)))
for i in ["TR33 0006 1005 1978 6457 8413 26", "TR330006100519786457841326", "TR12 3456"]:
    print(f"{i:>34} →", bool(IBAN_TR.match(i)))
Neden compile?re.compile kalıbı bir kez derler ve bir nesne verir; döngüde binlerce kez kullanacaksan okunurluk ve hız kazandırır. Python zaten son kalıpları önbelleğe (cache) alır, bu yüzden tek seferlik kullanımda fark yoktur; compile'ın asıl faydası kalıba ad vermektir.

Tuzaklar

1. Açgözlülük (greedy)

.* olabildiğince çok karakter yutar. <b>x</b> ve <b>y</b> metninde <b>.*</b> tüm satırı alır. Tembel (lazy) sürüm .*? en kısa eşleşmede durur.

2. Nokta ve özel karakterler

. "her karakter" demektir; gerçek nokta için \. yaz. Kullanıcıdan gelen metni kalıba gömeceksen re.escape() ile kaçır.

3. match ile doğrulama

re.match(r"\d{10}", "5321234567abc") eşleşir çünkü yalnızca baştan bakar. Doğrulama için fullmatch ya da ^...$ kullan.

4. Çok satırlı metin ve bayraklar

import re

html = "<b>laptop</b> ve <b>mouse</b>"
print("açgözlü:", re.findall(r"<b>.*</b>", html))
print("tembel :", re.findall(r"<b>.*?</b>", html))

print("nokta  :", re.findall(r"1.5", "1.5 ve 1x5 ve 125"))
print("kaçmış :", re.findall(r"1\.5", "1.5 ve 1x5 ve 125"))
print("escape :", re.escape("fiyat (TL)?"))

log = "INFO baslat\nERROR disk dolu\nerror: yetki yok\nINFO bitti"
print("IGNORECASE:", re.findall(r"^error.*", log, flags=re.IGNORECASE | re.MULTILINE))
print("MULTILINE yok:", re.findall(r"^ERROR.*", log))

# VERBOSE: uzun kalıbı açıklamalı yaz
TUTAR = re.compile(r"""
    (?P<miktar>\d{1,3}(?:\.\d{3})*(?:,\d{2})?)   # 1.250,00 gibi
    \s*(?P<birim>TL|USD|EUR)                      # para birimi
""", re.VERBOSE)
print(TUTAR.search("Toplam: 1.250,00 TL ödendi").groupdict())
Regex her şeyin cevabı değilHTML/JSON/CSV gibi yapılı biçimleri regex ile ayrıştırma; her birinin kendi ayrıştırıcısı var (json, csv, html.parser). Basit "ile başlıyor mu", "içeriyor mu" sorularında str.startswith ve in hem hızlı hem okunurdur. Regex'i düzensiz metinden kalıp çıkarmak için sakla.
Derinleş

Derinleş: ileri bakış, geri başvuru, bölme ve Türkçe metin

search, findall, gruplar ve sub ile çoğu işi çözersin. Ama iş hayatında şu sorular gelir: "Şifrede en az bir rakam ve bir büyük harf var mı?" (tek kalıpla), "Aynı kelime iki kez yazılmış mı?", "Adres alanı virgül, noktalı virgül ya da tireyle ayrılmış, nasıl bölerim?", "Kullanıcının yazdığı formülü parçalara nasıl ayırırım?", "Neden [a-zA-Z] Şükrü'yü reddediyor?". Bu bölüm bu araçları ekliyor. Metin temizleme ve kelime sayma gibi doğal dil işleme konuları "NLP ve metin" dersinde.

1. İleri ve geri bakış (lookaround)

Bakış (lookaround) ifadeleri bir konumda "şu gelir mi / şu var mıydı?" diye kontrol eder ama karakter tüketmez; eşleşmeye dahil olmazlar. Bu yüzden "kendisi değil, yanındaki önemli" durumları için idealdir: tutarın önünde TL olsun ama TL'yi alma, şifrede rakam bulunsun ama yeri önemli değil.

YazımAdıAnlamıÖrnek
X(?=Y)Olumlu ileri bakışX'ten sonra Y gelir\d+(?= TL)
X(?!Y)Olumsuz ileri bakışX'ten sonra Y gelmez\d+(?!%)
(?<=Y)XOlumlu geri bakışX'ten önce Y vardı(?<=#)\w+
(?<!Y)XOlumsuz geri bakışX'ten önce Y yoktu(?<!-)\d+
import re

metin = "Kulaklık 750 TL, kargo 49 TL, indirim %15, iade -120 TL"
print(re.findall(r"\d+(?= TL)", metin))          # TL'den önceki sayılar
print(re.findall(r"(?<!-)\b\d+(?= TL)", metin))  # eksi işaretli olanlar hariç
print(re.findall(r"(?<=%)\d+", metin))           # yüzde işaretinden sonraki

# Şifre kuralı: en az 8 karakter, 1 rakam, 1 büyük harf (sıra önemsiz)
KURAL = re.compile(r"(?=.*\d)(?=.*[A-ZÇĞİÖŞÜ]).{8,}")
for s in ["anadolu2026", "Anadolu2026", "Kısa1"]:
    print(s, bool(KURAL.fullmatch(s)))

# Binlik ayırıcı: sağdan 3'lü gruplar önüne nokta (karakter tüketmeden)
print(re.sub(r"(?<=\d)(?=(?:\d{3})+(?!\d))", ".", "Toplam 1234567 TL"))
Geri bakış sabit uzunluk isterPython'da (?<=...) içindeki kalıp sabit uzunlukta olmalı: (?<=TL|USD) hata verir çünkü 2 ve 3 karakter farklıdır. Çözüm: (?:(?<=TL)|(?<=USD)) ya da grup yakalayıp grubu kullanmak.

2. Geri başvuru: kalıp içinde "aynısı"

\1, \2 ya da adlandırılmış (?P=ad), kalıbın içinde "1. grubun yakaladığı metnin aynısı" demektir. Tekrarlanan kelimeleri, açılan tırnakla aynı türde kapanan tırnağı, aynı ayraçla yazılmış tarihleri yakalamak için kullanılır. Değiştirme metninde de \1 kullanılabilir; ama değiştirme metnini de mutlaka ham dize (r"...") olarak yaz.

import re

not_metni = "Müşteri bu bu ürünü iade etti etti ama kargo ücreti ödenmedi."
print(re.findall(r"\b(\w+) \1\b", not_metni))
print(re.sub(r"\b(\w+)( \1\b)+", r"\1", not_metni))

# Ayraç tutarlı mı? 15.03.2026 olur, 15.03/2026 olmaz
TARIH = re.compile(r"\d{2}(?P<ayrac>[./-])\d{2}(?P=ayrac)\d{4}")
for t in ["15.03.2026", "15/03/2026", "15.03/2026"]:
    print(t, bool(TARIH.fullmatch(t)))

print(re.findall(r"""(["'])(.*?)\1""", """kod="A-1" ad='Çay' not="yarım'"""))

3. re.split ve eşleşme nesnesinin ayrıntıları

str.split tek bir ayraçla böler; re.split kalıpla böler: "virgül, noktalı virgül ya da tire; etrafında boşluk olabilir". Kalıpta yakalayan grup varsa ayraçlar da sonuç listesine girer; istemiyorsan (?:...) kullan. Eşleşme nesnesi (Match) ise yalnız group()'tan ibaret değildir: span(), start(), end() konum verir (metni vurgulamak, hatayı göstermek için), lastgroup hangi adlı grubun eşleştiğini söyler.

import re

adres = "Kızılay Mah. ;  Atatürk Blv, No: 12 - Çankaya/Ankara"
print(re.split(r"\s*[;,\-]\s*", adres))
print(re.split(r"\s*([;,\-])\s*", "a ; b,c"))     # yakalayan grup: ayraçlar da gelir
print(re.split(r"\s+", "  çok   boşluk  ", maxsplit=1))

m = re.search(r"(?P<kod>FT-\d{4}-\d{5})", "EFT açıklama: FT-2026-00431 ödendi")
print(m.group("kod"), m.span(), m.start("kod"), m.end())
satir = "TR01;12a0;havale"
hata = re.search(r"[^\d;A-Za-z]|(?<=;)\d*[a-z]\d*(?=;)", satir)
print(satir)
print(" " * hata.start() + "^" * (hata.end() - hata.start()), "hatalı alan")

4. Mini ayrıştırıcı (tokenizer): lastgroup ile

Kullanıcının yazdığı bir indirim formülünü (fiyat * 0.9 - 50) ya da bir arama sorgusunu parçalara ayırmak için birden çok adlı grubu | ile birleştirip finditer ile gezersin; her eşleşmenin lastgroup'u parçanın türünü söyler. Hiçbir türe uymayan karakteri yakalayan bir "HATA" grubu en sona konur. Python'un kendi belgelerindeki tokenizer tarifi budur ve küçük kural motorlarının temelidir.

import re

PARCALAR = [
    ("SAYI",   r"\d+(?:\.\d+)?"),
    ("AD",     r"[^\W\d]\w*"),
    ("OP",     r"[+\-*/()]"),
    ("BOSLUK", r"\s+"),
    ("HATA",   r"."),
]
KALIP = re.compile("|".join(f"(?P<{ad}>{k})" for ad, k in PARCALAR))

def parcala(formul):
    for m in KALIP.finditer(formul):
        tur = m.lastgroup
        if tur == "BOSLUK":
            continue
        if tur == "HATA":
            raise ValueError(f"{m.start()}. konumda beklenmeyen karakter: {m.group()!r}")
        yield tur, m.group()

print(list(parcala("fiyat * 0.9 - 50")))
try:
    list(parcala("fiyat # 2"))
except ValueError as e:
    print(e)

5. Türkçe karakterler ve Unicode

Python 3'te str kalıpları varsayılan olarak Unicode'dur: \w ç, ğ, ı, ö, ş, ü, İ harflerini de kapsar, \d başka alfabelerdeki rakamları da (ör. Arapça-Hint rakamları) eşler. Sorun genellikle elle yazılan kümelerdedir: [a-zA-Z] "Şükrü"yü reddeder. "Yalnız harf" demek için [^\W\d_] (harf-rakam-alt çizgi kümesinden rakam ve alt çizgiyi çıkar) kullan. Yalnız ASCII rakam istiyorsan [0-9] yaz ya da re.ASCII bayrağı ver. re.IGNORECASE Türkçe İ/ı kuralını bilmez: "İSTANBUL" ile "istanbul" eşleşir ama "ISPARTA" ile "ısparta" eşleşmez; karşılaştırmadan önce kendi Türkçe küçültme fonksiyonunla normalleştir.

Regex101 ile deneKarmaşık bir kalıbı koda koymadan önce regex101.com'da "Python" seçeneğiyle dene: her parçanın ne yaptığını açıklar ve eşleşmeleri renklendirir. Denediğin örnekleri sonra pozitif/negatif test olarak koduna taşı.
Gerçek işteBankacılıkta açıklama alanlarından fatura no, abone no, plaka çıkarmak; e-ticarette ürün başlıklarından beden/renk/kapasite ayıklamak; log izlemede hata satırlarını servis bazında saymak regex'in günlük işleridir. Bu kalıplar bir kaliplar.py modülünde, her biri için "eşleşmeli/eşleşmemeli" örnek listesiyle birlikte tutulur ve pytest ile parametrik test edilir. Kullanıcı girdisine uygulanan kalıplarda girdi uzunluğu sınırlanır (ReDoS) ve re.escape unutulmaz.
Uzmanlaş

Üretim kodunda regex disiplini

Kurumsal projelerde kalıplar modülün başında büyük harfli sabit olarak re.compile ile tanımlanır ve yanına bir yorumla "ne eşleşir, ne eşleşmez" örnekleri yazılır. Her kalıbın en az bir pozitif ve bir negatif test örneği olur; çünkü regex'ler değiştirilirken en çok "önceden eşleşen artık eşleşmiyor" hatası çıkar. re.VERBOSE ile parçalara açıklama eklemek üç ay sonra kendine iyilik yapmaktır.

Güvenlik: bazı kalıplar belirli girdilerde üstel zaman alır (ReDoS, örn. (a+)+$ kalıbına uzun "aaaa...b" vermek). Kullanıcıdan gelen metne iç içe niceleyici içeren kalıp uygulayacaksan girdinin uzunluğunu sınırla. Python 3.11 ile gelen (?>...) atomik gruplar ve sahiplenici niceleyiciler (a++) geri izlemeyi keserek bu sorunu azaltır.

Unicode: Python 3'te \w varsayılan olarak Türkçe harfleri de kapsar (re.ASCII bayrağı vermediğin sürece). Büyük/küçük harf duyarsız eşleşmede "İ/i" ve "I/ı" ayrımı yanıltabilir; kritik karşılaştırmalarda metni önce normalize et. Son olarak, bir kalıbı yazdıktan sonra dört beş örnek metinle etkileşimli olarak denemek (regex101 gibi araçlar ya da küçük bir Python döngüsü) hatayı üretime gitmeden yakalar.

Alıştırmalara geç (22)