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 \s | Rakam / harf-rakam-alt çizgi / boşluk | \d{3} → 3 rakam |
| \D \W \S | Yukarıdakilerin tersi | \D+ → rakam olmayanlar |
| . | Satır sonu dışında herhangi bir karakter | a.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} |
| ^ $ \b | Satı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|b | Veya | TL|USD|EUR |
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)))
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())
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ş: 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ım | Adı | 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)X | Olumlu geri bakış | X'ten önce Y vardı | (?<=#)\w+ |
(?<!Y)X | Olumsuz 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"))
(?<=...) 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.
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.Ü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.