Doğal Dil İşleme yazı serüvenimiz heyecanla devam ediyor. Veri ön işleme, veriyi tanıma olarak adlandırabileceğimiz son yazılarımıza ek olarak, bu yazımızda düzenli ifadeler konusuna girmek istedik. Dışarıdan her ne kadar doğrudan doğal dil işleme konusu olarak görülmese de biz bu konunun çok önemli olduğunu ve eğer bol pratik yapılır, iyi öğrenilirse, sonraki işlemler öncesinde işlerinizi çok kolaylaştıracağını düşünüyoruz. Metin içeren her türlü veri bilimi/ makine öğrenmesi projesinde kullanabileceğiniz bu beceriyi, beceri envanterinize atmanızı şiddetle öneriyoruz.
İyi öğrenmeler…
Kısa bir tanım ve konunun önemi ile başlayalım o halde.
Düzenli ifadeler çoğu belki de tüm modern programlama dillerinde bulunmaktadır. Gelişigüzel bir metin/ifade içerisinde istediğimiz formattaki ifadeleri bulmamızı sağlar. Düzenli ifadeler olmasaydı ardı arkasına birçok if – else yazmak gerekebilirdi. Bu modül, birkaç saatte yapabileceğiniz bir işlemi saniyeler içerisinde sizin yerinize yapabiliyor. [1]
Daha fazla uzatmadan işlemler üzerinden modülü anlamaya çalışalım. Zaten daha sonrası hayalgücünüze kalacak.
A| Temel İşlemler
Bir düzenli(istenen) ifadenin metin içerisinde nasıl aranacağıyla ilgilenir.
1| match()
match() metodu yalnızca, verilen ifadenin metinin başında olup olmadığını kontrol etmek için kullanılabilir. Gerekli olduğunu biz pek görmedik.
1 2 3 |
a = "Doğal Dil İşleme Yapay Zeka'nın alt dallarından biridir. Doğal Diller..." print(re.match("Doğal Dil", a)) |
1 |
Output: <re.Match object; span=(0, 9), match='Doğal Dil'> |
Aranılacak kelime, metin ile birlikte re.match metoduna verildi.
1.1 span()
İfadenin konumunu, daha sonrasında kullanılabilir, göze hoş bir biçimde veriyor.
1 2 3 4 |
a = "Doğal Dil İşleme Yapay Zeka'nın alt dallarından biridir. Doğal Diller..." eşleşme = re.match('Doğal Dil', a) print(eşleşme.span()) |
1 |
Output: (0, 9) |
Bu metoda dair daha fazla özellik var ancak bu metodu çok yararlı bulmadığımızdan dolayı ve yazıyı uzatmamak için burada yer vermek istemedik. Dileyenler kaynakçadan ve oluşturduğumuz notebooktan inceleyebilirler.
2| search()
Metinin her yerinde arama yapmak için kullanılır.
1.1 span() ve group() => eşleşme nesneleri?
1 2 3 4 5 6 7 8 |
a = "Python güçlü bir dildir" nesne = re.search("güçlü", a) # ifade'nin metindeki yerini öğreniyoruz print(nesne.span()) # ifade metinde var iste ekrana yazdırılıyor print(nesne.group()) |
1 2 |
Output: (7, 12) Output: güçlü |
Aranacak kelime ile birlikte metin , re.search() metoduna verildikten sonra bir değişkene atadık. Daha sonrasında bu değişken üzerinde farklı metodlar kullandık. Bunu ileride sık sık yapacağız.
1.2 Doğrudan aramak
Bu örneği listeler ile vermek istedik. Listelerde bu fonksiyonlar çalışmazlar. Çalışmaları için bir döngü ile içinde gezmeniz gerekir.
1 2 3 4 5 6 |
liste = ["kedi", "köpek", "fare"] # düzenli ifadeler metinler üzerinde işlediği için for dönügüsü kullanmamız gerekecek for eleman in liste: if re.search("köpek", eleman): print(eleman) |
1 |
Output: kedi |
Bazen veri bize liste veya farklı veri yapısında gelebilir veya biz işimize geldiği için dönüşüm yaparak öyle kullanmak isteyebiliriz. Veri liste türünde geldiği zaman ise yapacağımız işlem bu şekilde.
3| findall()
Arama sonucunda, bulunan tüm değerleri getirir.
Örnek senaryo: İstenilen ifadenin metinde kaç kere geçtiğini bulmak için kullanabiliriz.
1 2 3 |
metin = "Bir berber bir berbere Bre berber gel beraber Bir berber dükkanı açalım demiş" print(len(re.findall("berber",metin))) |
1 |
Output: 4 |
start() ve end()
İstenen ifadeler, metinde en son nerede kullanıldılarsa onların başlangıç ve bitiş indekslerini verirler.
1 2 3 |
kontrol = re.search("AlphaGo","Merhaba ben AlphaGo, senin adın nedir?") print(kontrol.start()) print(kontrol.end()) |
1 2 |
Output: 12 Output: 19 |
endpos()
Verilen ifade içerisindeki toplam karakter sayısını verir.
1 2 |
ifade = re.search("Doğal Dil İşleme","Doğal Dil İşleme alanında hayret verici başarılara sahipti") print(ifade.endpos) |
1 |
Output: 58 |
Dikkat edilirse baştan veri anlattığımız, kodun çalışma şekli hiç değişmiyor. Sadece fonksiyon isimleri değişiyor.
B| Meta Karakterler
Bir düzenli (belirli özelliklere sahip) ifadenin metin içerisinde nasıl aranacağıyla ilgilenir. Temel işlemlerde istenen ifadeye bakmıştık. Şimdi belirli özellikleri verip gerisini makineye bırakacağız.
Neler var => [] . * + ? {} ^ $ \ | ()
Uyarı: Metakarakterlerin, kurallarının kelime için geçerli olduğunu vurguladık defalarca. Bununla (kelime/sözcük) kasıt, boşluk gelmeden önceki tüm ifadedir.
ÖRNEK: https://web.archive.org/web/201210250 => Burada görüldüğü gibi hiç boşluk yoktur ve bizim kelime dediğimiz yapı ile aynıdır. Kurallar bu yapılar için geçerlidir.
1| Köşeli Parantez []
Girilen karakterler “veya” olarak algılanır, içlerinden yalnızca bir tanesi kelimede bulunmalıdır, daha fazlası veya azı değil.
1 2 3 4 5 |
liste = ["sezin", "selin","sekin","sein","sezlin","selzin"] for eleman in liste: if re.search("se[lz]in",eleman): print(eleman) |
1 2 |
Output: sezin Output: selin |
Görüldüğü üzere, l ve z harflerinin ikisinin birden geçtiği, hiçbirinin geçmediği kombinasyonlar ekrana yansımadı.
2| Nokta .
Kelime uzunluğu boyunca, noktadan geriye doğru her karakterin gelmesi serbesttir.
1 2 3 4 5 |
liste = ["metin", "selin","selim","keskin"] for eleman in liste: if re.search(".in",eleman): print(eleman) |
1 2 3 |
Output: metin Output: selin Output: keskin |
Görüldüğü gibi “in” den önce ne olduğu farketmeksizin her kelime ekrana basıldı.
3| Yıldız *
Kendinden hemen önce verilen karakterden sıfır (0) veya daha fazla sayıda bulunduğunda çıktı verir. Unutulmaması gereken diğer kural ise, ondan da önce karakterler verildiyse onların gelmesi zorunludur.
1 2 3 4 5 |
liste = ["metin","sakin", "selin","seeezgin","engin"] for eleman in liste: if re.search("se*",eleman): print(eleman) |
1 2 3 |
Output: sakin Output: selin Output: seeezgin |
Görüldüğü gibi “engin” kelimesi çıktı olarak görülmedi. Bunun sebebi “s” harfininde kelimelerde bulunma zorluğu.
4| Artı +
Yıldız meta karakteriden farklı olarak, kendinden hemen önce verilen karakterden bir (1) veya daha fazla sayıda bulunduğunda çıktı verir.
1 2 3 4 5 |
liste = ["metin","sakin", "selin","seeezgin","engin"] for eleman in liste: if re.search("se+",eleman): print(eleman) |
1 2 |
Output: selin Output: seeezgin |
Yıldız meta karakterinde “sakin” kelimesi kabul edilmesine rağmen, burada kabul edilmedi. Çünkü “e” harfinin en az 1 kere geçmesi isteniyor.
5| Soru İşareti ?
Yıldız ve artı metakarakterlerinin spesifik özelliklerini alıyor. Yani sadece sıfır (0) veya bir (1) defa geçen ifadeleri ekrana bastırıyor.
1 |
re.findall("@g?mail","E-posta adresimiz test@mail.com, test@gmail.com ve test@gggggmail.com") |
1 |
Output: ['@mail', '@gmail'] |
Diğerlerinin aksine g harfinin 5 kere geçtiği ifadeyi ekrana bastırmadı.
6| Süslü Parantez {}
Süslü parantez içine yazacağımız sayı ile, hemen öncesinde geçen karakterin, kelime içerisinde kaç kere geçmek istediğini belirtiyoruz. Bu kurala uyanlar ekrana yansıyor.
1 2 3 4 5 |
liste = ["sezin", "seezin", "seeezin"] for eleman in liste: if re.search("se{3}",eleman): print(eleman) |
1 |
Output: seeezin |
7| Şapka ^
Sözcüğün hangi karakter ile başlamasını istiyor isek şapka meta karakterinden hemen sonra onu koymamız yeterli. Dikkat edilir ise karakter değil, karakterler ifadesi geçti anlatımda. Yani şapka meta karakterinin yanına, boşluk gelene kadar koyduğunuz tüm karakterleri görmek ister sorguda.
1 2 3 4 5 |
liste = ["sezin", "metin", "ali"] for eleman in liste: if re.search("^a",eleman): print(eleman) |
1 |
Output: ali |
8| Dolar $
Kelimenin hangi karakterler ile bitmesini istiyor isek dolar meta karakterinden hemen önce onu koymamız yeterli. Şapka da geçerli olan son kural burada da geçerlidir ve çok önemlidir. Dİğer meta karakterler ile karıştırılmaması gerekir.
1 2 3 4 5 |
liste = ["sezin", "metin", "ali"] for eleman in liste: if re.search("n$",eleman): print(eleman) |
1 2 |
Output: sezin Output: metin |
9| Ters Eğik Çizgi \
Kodlama yaparken de görmeye alışkın olduğumuz kaçış karakteridir. Buradaki önemi ise dolar($) gibi meta karakterlerin de kodlarımızda kullanımını sağlamak.
1 2 3 |
metin = "Arabanın değeri: 55000$ " print(re.findall("[0-9]+\$",metin)) |
1 |
Output: ['55000$'] |
Kodu tek tek inceleyelim.
[0-9]+\$ => Yazının ilerleyen kısımlarında \d olarak karşımıza çıkacak bu ifade bu aralıktaki rakamlardan herhangi birinin geleceğini söylüyor.
Artı (+) meta karakteri ile de en az 1 tane rakam geleceğini ancak daha fazla da gelebileceğinin mümkün olduğunu anlıyoruz.
$ karakterini tek ters eğik çizgi (\) meta karakteri olmadan kullansaydık, makine bunu meta karakter olarak algılayacak ve bize bu kurallar dahilinde bir çıktı yansıtamayacaktı. Ters eğik çizgi kullandığımızda ise $ işareti bir meta karakter değil, bilinen dolar işareti olarak kelimede yerini aldı.
10| Düz Çizgi |
Birden fazla düzenli ifadeyi kullanmamızı sağlar. Verilen düzenli ifadelerden bir tanesini sağlaması yeterlidir.
1 2 3 4 5 |
liste = ["sezin", "selam", "metin","sakız"] for eleman in liste: if re.search("^se|in$",eleman): print(eleman) |
1 2 3 |
Output: sezin Output: selam Output: metin |
“selam” kelimesi “se” ifadesi ile başladıkları için,
“metin” kelimesi ise “in” ile bittiği için ekrana yazıldı.
“sezin” ise iki kurala da uyuyor.
11| Parantez ()
Şapka ve dolar meta karakterlerinde üzerinde durduğumuz konu olan, birden fazla karakteri etkileme durumunu, diğer meta karakterlerde nasıl uygulayacağımızı merak etmiş olabilirsiniz. Evet böyle bir şey mümkün. Bunu parantez meta karakteri ile sağlayabiliyoruz.
1 2 3 4 5 |
yeni_liste = ["abef","abcdef","abdcdef"] for eleman in yeni_liste: if re.search("ab(cd)*ef",eleman): print(eleman) |
1 2 |
Output: abef Output: abcdef |
“c” ve “d” harfleri birlikte sorgulanmıştır. Yıldız (*) meta karakteriyle birlikte şu anlama gelmiştir. => “cd” ifadesi sıfır (0) veya daha fazla geçiyor ise ekrana bastır.
“abdcdef” ifadesi fazladan “d” karakterini içerdiği için ekrana basılmadı.
C| Özel Dizilier
1| \w ve \W
Küçük harf ile başlayan, \w, \s, \d gibi özel diziler bazı işlemleri yapmamızı sağlarken, \W, \S, \D gibi özel diziler ise bu işlemlerin tersini yapmamızı sağlar. Burada işlenecek olarak özel diziler dışında daha fazla özel dizi var. Ancak onlar için kaynaklar müracaat etmenizi tavsiye ediyoruz.
\w metindeki harf, sayı ve alt çizgi için kullanılır. Özel ismi alfanumerik karakterlerdi. Kuralı sağlayan ifadeleri liste şeklinde basar ekrana.
1 2 3 |
metin = "Bugün 19_* 2021" print(re.findall("\w+",metin)) |
1 |
Output: ['Bugün', '19_', '2021'] |
\W ise geri kalanları alır. Bunların dışında geçen şeyleri saydırmak için kullanabilirsiniz.
1 2 3 |
metin = "Bugün 19_* 2021" print(re.findall("\W",metin)) |
1 |
Output: [' ', ' ', '*', ' '] |
Görüldüğü gibi 2 adet boşluk olunca onları bile 2 ayrı eleman olarak aldı. Yıldız karakterini de listeye dahil etti. Bazı özel karakterlerin metinde olmamasını istersen kullanabiliriz. Ancak çok sık işimize yarayacağını söylemeyiz.
2| \s ve \S
İngilizcedeki “space” yani “boşluk” kelimesinin baş harfinden geliyor diye düşünebilir ve kodlayabiliriz zihnimize.
Yani adından da anlaşılabileceği gibi bize boşluk imkanı veriyor. 1 veya daha fazla adet boşluk olmasını istediğimizde/ buna göz yumduğumuzda kural işini yapmış oluyor. Gelin bir örnek ile inceleyelim.
1 2 3 4 |
metin = "Bugünkü-konumuz düzenli ifadeler" ifade = re.search("\s[a-z].+",metin) print(ifade.group()) |
1 |
Output: düzenli ifadeler |
\s sayesinde boşluk olmayan “Bugünkü-konumuz” gibi ifadeleri elemiş olduk.
[a-z] ile biraz önce \w ile yaptığımız işlemi yapabildik.
.+ => [a-z] den en az 1 tane ama istediğin kadar alabilirsin demiş olduk.
3| \d ve \D
İngilizcedeki “double” yani “ondalık” kelimesinin baş harfidir. [0-9] arasındaki sayıları yani rakamları ifade eder. Rakamları tespit etmek için kullanırız.
Önce \d özel dizisini görelim.
1 2 3 4 |
metin = "Bugün 15 mart yarın 6nisan" ifade = re.search("\d+\s\w+",metin) print(ifade.group()) |
1 |
Output: 15 mart |
\d+ => En az 1 tane sayı geçsin
\s => 1 adet boşluk olsun
\w => En az 1 adet alfanumerik değer geçsin
\D özel dizisi ise tahmin edileceği üzere, bu işlemin tersini vermekte yani sayı olmayan değerleri ekrana bastırmaktadır.
1 2 3 4 |
metin = "Bugün15" ifade = re.search("\D+",metin) print(ifade.group()) |
1 |
Output: Bugün |
Böylece ifademizdeki gereksiz sayıları atmış olduk.
D| Ekstralar
compile() => re.I ve re.S kullanımı
Düzenli ifadeler karakter dizilerine göre biraz daha yavaş çalışırlar.
Ancak düzenli ifadelerin işleyişini hızlandırmanın da bazı yolları vardır. Bu yollardan biri de compile() metodunu kullanmaktır.
1 2 3 4 5 |
metin = "Bugün 5 nisan yarın 6 Nisan" derle = re.compile("\d\s\w+", re.I) tarihler = derle.findall(metin) print(tarihler) |
1 |
['5 nisan', '6 Nisan'] |
re.I sayesinde hem büyük harf hem de küçük harf ile başlayan ifadeleri getirmiş olduk.
1 2 3 4 5 |
a = "PythonPython \nPython Ben Python,\nMonty Python ifadesi \n\C#" derle = re.compile(".*Python", re.S) nesne = derle.search(a) if nesne: print(nesne.group()) |
1 2 3 |
Output: PythonPython Output: Python Ben Python, Output: Monty Python |
Nokta (.) meta karakteri sonraki satırda aramada kullanılamıyor. Bunu sağlamak için böyle bir metot var.
sub() + subn()
Çok bilinen replace() metodu yerine kullanılabilir. Daha karmaşık metinlerde tercih edilir.
subn() ile de kaç değişiklik yapıldığı gözükür
1 2 3 4 |
metin = "Bugün 5 nisan yarın 6 Nisan" derle = re.compile("nisan",re.I) print(derle.subn("ekim",metin)) |
1 |
('Bugün 5 ekim yarın 6 ekim', 2) |
“nisan” ve “Nisan” => “ekim” oldu ve toplamda 2 adet değişiklik yapılmış oldu.
E| Örnek Uygulama
Web kazıma ve düzenli ifadeler ile ilgili işlemleri yapabilmek için gerekli bazı kurulumları ve veriyi hazır hale getirmek işlemleri yapıldıktan sonra düzenli ifadeler devreye giriyor.
.+html => En az bir karakter ile başlayan, sınırsız uzunluğa sahip(boşluk olmadan) ve html ile biten ifadeleri getir.
(.+) => En az bir karakter ile başlayan, sınırsız uzunluğa sahip ancak bunları bir bütün şeklinde sağlayan ifadeyi getir.
1 2 3 4 5 |
import re from urllib.request import urlopen url = "https://web.archive.org/web/20121025012131/http://www.istihza.com/py2/icindekiler_python.html" f = urlopen(url) |
1 2 3 4 5 6 7 |
çıktı = "Başlık: {};\nBağlantı: {}\n" regex = 'href="(.+html)">(.+)</a>' for i in f: nesne = re.search(regex, str(i, 'utf-8')) if nesne: print(nesne.groups()) |
Yazbel Python sitesinden aldığımız bu örnek, şimdiye kadar öğrendiğimiz bir çok özelliği bir arada sunuyor. Biz birkaçını açıkladık. Geri kalan kısımlar size ödev 🙂
Serinin Diğer Yazıları;
Doğal Dil İşlemeye (DDİ) Giriş
DDİ’nin Arka Planı, Zorlukları, Geleceği
Doğal Dil İşleme ve String İşlemleri
Doğal Dil İşleme Kütüphaneleri
DDİ’de Veri Ön İşleme-1
DDİ’de Veri Ön İşleme-2
DDİ’de Part of Speech Tagging ve Saklı Markov Modeli
Yazarlar: Mustafa Selim ÖZEN, Saygın YILDIZ
Kaynaklar
[1] https://www.sinanerdinc.com/python-re-modulu
[2] https://python-istihza.yazbel.com/standart_moduller/regex.html
[3] https://www.youtube.com/watch?v=sSD_xCade-g&t=320s
[4] https://alhydrtprk.medium.com/python-re-regular-expression-mod%C3%BCl%C3%BC-6dec531a8434
[5] https://erdincuzun.com/ileri-python/regular-expressions-duzenli-ifadeler/
[6] https://www.tutorialspoint.com/python/python_reg_expressions.htm
[7] https://www.w3schools.com/python/python_regex.asp
[8] https://docs.python.org/3/howto/regex.html
[9] https://regex101.com/