Python sunduğu birçok string metoduyla metin tipindeki verilerin temizlenmesini ve analizini kolaylaştırır. Bazı özel durumlarda hazır metotları kullanarak sorunu çözmeyi denediğinizde zorlanabilir veya yazdığınız kod karmaşık bir hal alabilir. Düzenli ifadeler, ileri seviye metin veri temizliği ve analizi konusunda pratiklik sağlar.

Düzenli ifadeler (Regular Expressions – RegEx) nedir?

Neredeyse tüm yazılım dillerinde bulunan düzenli ifadeler (Regular expressions – RegEx), kendine has bir söz dizimi (syntax) kullanarak stringler içerisinde herhangi bir karakter setini aramaya, eşleştirmeye ve bulmaya yardımcı olur.

Düzenli ifadeleri kullanabilmek için kendine has söz dizimini (syntax) ve kurallarını öğrenmek gerekmektedir. Günlük hayatta çok sık kullanılmadığı için fonksiyonları veya kuralları unutmanız mümkündür.

RegEx neden önemlidir?

Düzenli ifadeler, sunduğu bazı avantajlar sayesinde her veri bilimcisi/analistinin çantasında olması gereken bir araçtır. Gerektiği yerlerde düzenli ifadeleri kullanabilmek aşağıdaki faydaları sağlar:

  • Karmaşık metinler içerisinde istenilen alt metni bulmaya yardımcı olur.
  • Sade ve anlaşılır kod yazımına katkıda bulunur.
  • Metin tipindeki verilerin temizliğini ve ön işlemesini kolaylaştırır.

re modülü

Python’da düzenli ifadeleri re modülü ile kullanabilirsiniz. Python’ın standart kütüphanelerinden biri olduğu için ekstra kurulum gerektirmez. Kullanmadan önce dosyanızın başında Pandas, NumPy gibi kütüphaneleri çağırdığınız gibi re modülünü de çağırmanız gerekmektedir.

import re

Bir metin içerisinde aranan düzenli ifadeye “desen (pattern)” adı verilir. Desenin, arama yapılan string içerisinde bulunduğu duruma “eşleşme (matched)” denir.

Python’da düzenli ifade metotları

re modülü düzenli ifadelerle çalışabilmek için bazı fonksiyonlara sahiptir. En önemli metotlar şöyle özetlenebilir:

Metot adıTanımıSöz dizimi (syntax)
match()Aranılan desen, stringin başında bulunduğu durumda eşleşmenin konumunu belirten bir eşleşme nesnesi üretir. Herhangi bir eşleşme bulunamadığı durumda None değeri üretir.re.match(“desen, “string”)
findall()Aranılan desen string içerisinde bulunduğu durumda, yakaladığı tüm eşleşmeleri liste olarak getirir.re.findall(“desen, “string”)
sub()Aranılan deseni string içerisinde bulduğu durumda, verilen diğer string değeri ile değiştirir.
Eşleşme bulunamadığı durumda hiçbir değişiklik yapmaz.
re.match(“desen, “yeni değer”, “string”)
search()match() fonksiyonu ile aynı işlevi vardır. Yalnızca ilk eşleşmeye dair eşleşme nesnesi üretir.re.search(“desen, “string”)
split()Aranılan desen string içerisinde bulunduğu durumda, eşleştiği bölgelerden string’i böler ve bölünmüş halini liste olarak getirir. Herhangi bir eşleşme olmadığı durumda stringin kendisini getirir.re.split(“desen, “string”)

re modülünün metotlarının nasıl çalıştığını birkaç örnek üzerinden inceleyelim. Örnek için 2016’nın hit şarkılarından biri olan Günah Benim‘in bol kafiyeli sözlerinden birini kullanacağız.

match()

lyrics = "Yine kalmışım gece bir başına, bir başına girdim yılbaşına"

pattern = "başına"
match_pattern = "Yine"

print(re.match(pattern, lyrics))
print(re.match(match_pattern, lyrics))
None
<re.Match object; span=(0, 4), match='Yine'> 
  • İlk satırda belirtilen şarkı sözü içinde, aynı şarkı sözünde geçen bir kelime olan “başına” kelimesini aradık.
  • İlk aramada, match() fonksiyonu aradığımız kelimeyi şarkı sözünün başında bulamadığı için “None” değeri üretti.
  • Sonraki aramada, match() fonksiyonu aradığımız kelimeyi şarkı sözünün başında bulduğu için eşleşmenin konumunu gösteren bir nesne değeri üretti. Bu çıktıya göre aradığımız kelime stringin 0 ve 4. indeksi arasında yer alıyor.

findall()

lyrics = "Yine kalmışım gece bir başına, bir başına girdim yılbaşına"

pattern = "başına"

print(re.findall(pattern, lyrics))
['başına', 'başına', 'başına']
  • findall() fonksiyonu aradığımız deseni bulduğu zaman, tüm eşleşmeleri bir liste halinde getirir.
  • “başına” deseni cümlede 3 kere tekrarlandığı için 3 elemanlı bir liste elde ettik.

sub()

lyrics = "Yine kalmışım gece bir başına, bir başına girdim yılbaşına"

pattern = "başına"
new = "yaşına"

print(re.sub(pattern, new, lyrics))
Yine kalmışım gece bir yaşına, bir yaşına girdim yılyaşına 
  • sub() fonksiyonu aradığımız deseni bulduğu zaman, tüm eşleşmeleri istediğimiz bir başka metin ile değiştirir.
  • sub() fonksiyonuna aradığımız deseni, yeni metin değerini ve içerisinde arama yapılacak stringi parametre olarak girmemiz gerekmektedir.
  • Şarkının kafiyesini bozmadan, anlamını değiştirecek şekilde “başına” kelimesini “yaşına” ile değiştirdik.

search()

lyrics = "Yine kalmışım gece bir başına, bir başına girdim yılbaşına"

pattern = "başına"

print(re.search(pattern,lyrics))
<re.Match object; span=(23, 29), match='başına'> 
  • search() fonksiyonu, match() fonksiyonuna benzer bir şekilde çalışır. Aranılan desenin eşleştiği durumlarda bir konum nesnesi üretir. Match() fonksiyonundan farklı olarak, aranılan stringdeki ilk eşleşmeye dair konumu getirir.
  • “başına” kelimesi, ilgili şarkı sözünde 3 farklı yerde bulunmaktadır.
  • search() fonksiyonu ilk eşleşmeyi kayda alır ve bu eşleşmenin gerçekleştiği konumu getirir.
  • Bu bilgiye göre aradığımız desen ilk olarak 23 ve 29. indeksler arasında yer almaktadır.

split()

lyrics = "Yine kalmışım gece bir başına, bir başına girdim yılbaşına"

pattern = "başına"

print(re.split(pattern,lyrics))
['Yine kalmışım gece bir ', ', bir ', ' girdim yıl', '']
  • String metotlarından da tanıyacağımız split() fonksiyonu, desenin eşleştiği tüm noktalardan stringi bölerek yeni bir liste oluşturur.
  • “başına” kelimesi, ilgili şarkı sözünde 3 farklı yerde bulunduğu için, string 3 farklı noktadan kesilmiştir.
  • Geri dönen liste “eşleşme sayısı + 1” elemandan oluşur.

Regex fonksiyonları farklı özellikleriyle esneklik sağlasalar da, regex’in kullanışlılığını artıran gizli silah metakarakterlerdir.

Metakarakterler

Metakarakterler, yazılım dilleri için özel anlamı olan karakterlere denir. Regex’in gücü metakarakterlerden gelir. Bu karakterler sayesinde belirli bir düzene uyan metinleri arayabilirsiniz.

Karakter dizisi ([ ])

Karakter dizisi, bir karakter grubundaki tek bir karakterin aranılan metinde bulunup bulunmadığını kontrol eder. Yazım yanlışlarını kontrol etmek için kullanışlıdır.

str1 = "namert"
str2 = "dertli"
str3 = "Sert"
str4 = "dert"

pattern = "[msf]ert"

re.findall(pattern, str1)
StringEşleşti mi?Eşleşen string
namertEvetnamert
dertliHayır
SertHayır
sertEvetsert
dertHayır
  • “namert”, m, s veya f karakterlerinden birini içerdiği ve “ert” ile bittiği için eşleşme sağlandı.
  • “d” aradığımız karakter dizisine ait bir eleman olmadığı için eşleşme sağlanamadı.
  • Üçüncü örnekte “s” aradığımız karakterlerden biri olmasına rağmen, büyük/küçük harf hassasiyetinden dolayı eşleşme sağlanamadı.
  • Bir sonraki örnekte “s” karakteri, karakter dizisi grubundaki gibi küçük harf ile yazıldığı ve string “ert” ile bittiği için eşleşme sağlandı.
  • Son örnekte,”d” aradığımız karakter dizisine ait bir eleman olmadığı için stringin sonu “ert” ile bitse de eşleşme sağlanamadı.

Karakter dizisi ile arama yaptığınız durumlarda, büyük/küçük harf duyarlılığını hesaba kattığınızdan ve stringin sonunun istediğiniz gibi bittiğinden emin olmalısınız.

Negatif karakter dizisi (^)

Karat (^) sembolünü kullanarak, aranılan karakter dizisinin eşleşmediği stringleri bulabilirsiniz. Biraz önceki örneğe negatif karakter dizisi ile tekrar bakacak olursak:

str1 = "namert"
str2 = "dertli"
str3 = "Sert"
str4 = "dert"

pattern = "[^msf]ert"

re.findall(pattern, str1)
StringEşleşti mi?Eşleşen string
namertHayır
dertliEvetdert
SertEvetSert
sertHayır
dertEvetdert
  • “namert”, m, s veya f karakterlerinden birini içerdiği ve “ert” ile bittiği için eşleşme sağlanamadı.
  • “d” aradığımız karakter dizisine ait bir eleman olmadığı ve “ert” ile devam ettiği için eşleşme sağlandı.
  • Üçüncü örnekte “S” aradığımız karakterlerden biri olmadığı için eşleşme sağlandı.
  • Bir sonraki örnekte “s” karakteri, karakter dizisi grubundaki gibi küçük harf ile yazıldığı ve string “ert” ile bittiği için eşleşme sağlanamadı.
  • Son örnekte,”d” aradığımız karakter dizisine ait bir eleman olmadığı ve stringin sonu “ert” ile devam ettiği için eşleşme sağlandı.

Karakter aralığı (range)

Belirli bir rakam veya harf aralığındaki bir karakteri arayabilmek için karakter aralığı özelliğinden faydalanabilirsiniz. Karakter dizisinde olduğu gibi köşeli parantezlerin kullanılır, farklı olarak ise karakter aralığını belirtmek için tire (-) işareti kullanılır.

Örnek:

  • [a-e] –> a, b, c, d, e harfleri aranır.
  • [0-5] –> 0, 1, 2, 3, 4, 5 rakamları aranır.
  • [A-Z] –> A’dan Z’ye tüm büyük harfler aranır.
  • [A-Za-z] –> A’dan Z’ye tüm büyük harfler, daha sonrasında da a’dan z’ye tüm küçük harfler aranır.

Python’da karakter aralığını kullanarak arama yaptığınız durumda aralıkta bulunan karakterlerden sadece birisi ile eşleşme sağlanır.

Basit bir örnek ile karakter aralığının çalışma prensibini görebiliriz:

str1 = "Badem Sadem Kadem Yadem"

pattern = "[B-M]adem"

re.findall(pattern, str1)
['Badem', 'Kadem']
  • Desenin ilk karakterinin B ile M harfleri arasındaki herhangi bir karakter olabileceğini, devamının ise “adem” ile biteceğini belirttik.
  • Birinci ve üçüncü kelime bu şartları sağladığı için eşleşme sağlandı.

Karakter sınıfı (Özel karakter dizileri)

Karakter sınıfı veya özel karakter dizileri, karakter aralığı ile tanımlanan aralıkları daha basitçe ve hızlıca tanımlayabilmek için kullanılır. En çok tanımlanan aralıkların ([0-9], [a-z] vs.) özel karşılığı olarak ifade edilebilir.

Karakter sınıfıDesenAçıklama
Rakam\dHerhangi bir rakam aranır. [0-9] ile aynı işlevi görür.
Karakter\wHerhangi bir rakam, büyük veya küçük harf, alt tire karakterleri aranır. [A-Za-z0-9_] ile aynı işlevi görür.
Boşluk\sHerhangi bir boşluk aranır. [\t\r\n\f\v] ile aynı işlevi görür.
Nokta.Yeni satır karakteri dışındaki diğer karakterler aranır.

Karakter dizilerinde olduğu gibi karakter sınıflarında da negatif aramalar yapmak mümkündür. Karakter dizilerinden farklı olarak karat karakteri yerine büyük harfler kullanılır.

Karakter sınıfıDesenAçıklama
Rakam\DRakam olmayan herhangi bir karakter aranır. [^0-9] ile aynı işlevi görür.
Karakter\WHerhangi bir rakam, büyük veya küçük harf, alt tire karakterleri dışında kalan diğer karakterler aranır. [^A-Za-z0-9_] ile aynı işlevi görür.
Boşluk\SHerhangi bir boşluk dışında kalan diğer karakterler aranır. [^\t\r\n\f\v] ile aynı işlevi görür.

Karakter dizileri ile aradığımız karakter hakkında detaylı bir bilgi vermeden, sadece sayı, boşluk veya karakter olduğunu belirterek arama yapabiliriz. Örnek olarak, aşağıdaki il yazımlarından plaka kodu belirtilen ilk versiyonu ile eşleşme sağlayalım.

str1 = "01 Adana, Adana"

pattern = "[\d][\d] Adana"

re.findall(pattern, str1)
['01 Adana']
  • Desenin ilk iki karakterinin sayı olacağını belirttik fakat bu karakterlerin hangi sayı olacağına dair bir bilgi vermedik.

Çapa (Anchors)

Herhangi bir metnin, belirli bir desen ile başlayıp başlamadığını veya bitip bitmediğini çapalar sayesinde kontrol edebilirsiniz.

ÇapaDesenAçıklama
Başlangıç^Karat işaretinden sonra gelen desen metnin başında bulunursa eşleşme sağlanır.
Bitiş$Dolar işaretinden sonra gelen desen metnin sonunda bulunursa eşleşme sağlanır.

Not: Karat (^) sembolü bir karakter dizisi içerisinde yani köşeli parantez içerisinde kullanıldığında negatiflik bildirir.

Bu örnekte, il verisinin mutlaka plaka kodu ile başlamasını istediğimizi varsayalım. Bunu kontrol etmek için:

il = "01 Adana"

pattern = "^\d\d Adana"

re.findall(pattern, il)
['01 Adana']

Niceleyiciler (Quantifiers)

Bu noktaya kadar kullanılan ifadeler tek bir karakter veya sabit bir metin araması ile ilgiliydi. Niceleyiciler yardımı ile aradığınız karakterlerin adetini belirtebilirsiniz.

Niceleyiciler, sabit ve opsiyonel niceleyiciler olarak iki gruba ayrılır.

Sabit niceleyiciler, adından anlaşılacağı gibi, aranılan desenin belli bir sayıda tekrarlanacağını belirtir. Örnek olarak, elimizde belli sabit hatlı telefon numaları olsun ve biz bu numaraların alan kodlarını yani ilk 3 rakamlarını ayırmak isteyelim. Bunun için aşağıdaki gibi bir desen yazmamız gerekmektedir:

Sabit niceleyicilerin esneklik sağlayan, birden farklı kullanım amaçları vardır.

Sabit NiceleyiciDesenAçıklama
Tam olarak{x}x defa
‘den Fazla{x, }x veya daha fazla
‘den Fazla ‘den Az{x, y}En az x defa, en fazla y defa

Opsiyonel niceleyiciler, daha fazla esneklik sağlayarak bir desenin belli başlı miktarlarda aranmasına olanak sağlar.

Opsiyonel NiceleyiciDesenAçıklama
Sıfır veya daha fazla*Yıldız işaretinden önce gelen desen metinde hiç bulunmuyorsa veya herhangi bir sayıda bulunuyorsa eşleşme sağlanır.
Bir veya daha fazla +Artı işaretinden önce gelen desen metinde bir veya daha fazla bulunuyorsa eşleşme sağlanır.
Sıfır veya bir defa?Soru işaretinden önce gelen desen metinde sıfır veya bir adet bulunuyorsa eşleşme sağlanır.

Anlatımı netleştirmek için örnekler üzerinden gidelim. Son örnekte olduğu gibi, il verisinin mutlaka plaka kodu ile başlamasını istediğimizi varsayalım. Bu sonucu elde etmek için artık birden fazla stratejiye sahibiz:

il = "01 Adana"

pattern1 = "^\d\d Adana"
pattern2 = "[\d]{2} Adana"
pattern3 = "[\d]+ Adana"

print(re.findall(pattern1, il))
print(re.findall(pattern2, il))
print(re.findall(pattern3, il))
['01 Adana']
['01 Adana']
['01 Adana']
  • İlk yöntemde, bir önceki örnekte olduğu gibi karat (^) ifadesini kullanarak, ilk 2 karakterin rakam olması gerektiğini belirttik.
  • İkinci yöntemde, art arda iki defa rakam belirtmek yerine, \d ifadesi ile bir rakam aradığımızı ve sabit niceleyici olan köşeli parantez ile de rakamın iki adet olduğunu belirttik.
  • Üçüncü yöntemde ise, yine \d ifadesini kullanarak bir rakam aradığımızı ifade ettikten sonra opsiyonel bir niceleyici olan toplama işaretini kullanarak birden fazla rakam aradığımızı belirttik.

Gruplayıcılar

Gruplayıcılar ile herhangi bir eşleşmenin belirli bir parçasını çekip alabilirsiniz. Gruplar yardımı ile herhangi bir grup karakterden önce veya sonra gelen desenleri de arayabilirsiniz. Regex’te gruplar parantezler () yardımı ile belirlenir.

Örnek olarak, şirketlerin iletişim sayfalarını taradığımızı ve iletişim için e-posta adreslerini listelediğimizi varsayalım. E-posta adreslerini de kullanıcı adı ve domain olarak iki farklı sütunda saklamak istiyoruz. Yemeksepeti üzerinden örnek yapalım:

email = "info@yemeksepeti.com"

pattern = "([\w\.-]+)@([\w\.-]+)"

re.findall(pattern, email)
[('info', 'yemeksepeti.com')]

Gruplayıcılar, birden fazla kullanıldığı zaman kodun okunabilirliğini korumak adına isimlendirilebilir. Gruplayıcının açılış parantezinden sonra ?P<isim> söz dizimini kullanarak gruplayıcılarınızı isimlendirebilir, bu isimleri ve group() fonksiyonunu kullanarak isimleriyle erişebilirsiniz.

email = "info@yemeksepeti.com"

pattern = "(?P<user>[\w\.-]+)@(?P<domain>[\w\.-]+)"

regex = re.search(pattern, email)

print("Kullanıcı adı: " + regex.group("user"))
print("Domain adı: " + regex.group("domain"))
Kullanıcı adı: info 
Domain adı: yemeksepeti.com

Gruplayıcıların sağladığı avantajlardan bir diğeri, aranılan grubun önünde veya arkasında bir desene bakarak eşleşme yapma imkanı sağlamasıdır.

Düzenli ifadeler, ilk başta anlaması zor ve unutması kolay bir konudur. Alışması ve uzmanlaşması zaman alabilir. Benim tavsiyem, regexin syntax’ına odaklanmak yerine, genel hatlarıyla regex ile neler yapabileceğinizi ve nerede ihtiyacınız olabileceğini öğrenmenizdir.

Kaynakça

Python – 3.6.3 Documentation
Yazım konusunda pratik yapmak için faydalı araçlar: Regexr ve Regex 101
Python re modülü