23 Kasım 2011 Çarşamba

Düzenli Deyimler

Bir hesaplama sürecine girdi sağlarken, kimi zaman farklı girdilerin aynı anlama geldiğini veya farklı da olsalar benzer anlamlar taşıdığını söylemek isteriz. Örneğin, bir işleme devam etmek isteyip istemediğimizin sorulması durumunda, "Hayır" yerine "H" veya "h"nin de işi görmesi bize zamandan kazandıracak ve—"hayır" yerine "hıyar" yazdığınızı düşünün—hata oranını azaltacaktır. İşte tam bu noktada, bir grup karakter katarını sahip oldukları içeriğin ortak özelliklerini temel alarak betimleyen özel bir çeşit karakter katarı şeklinde tanımlayabileceğimiz düzenli deyimler işin içine girer. Bu yazımızda da yapacağımız, Java'da düzenli deyimler için sağlanan desteğe bakmak olacak.

Temel Kullanım


Java'da düzenli deyim desteği java.util.regex paketindeki türler vasıtasıyla sağlanır. Bu türlerin sunduğu işlevsellikten yararlanarak, olası girdileri betimleyen düzenli deyimin, programın çalıştırılması sırasında sağlanan asıl girdi ile eşleşip eşleşmediği kontrol edilir. Bunun için yapılması gereken, düzenli deyim ile girdinin argüman olarak geçirildiği Pattern.matches metodunun çağrılmasıdır. Aşağıdaki kod parçasından da görülebileceği gibi, düzenli deyimin String olması beklenirken, girdinin CharSequence arayüzünü destekleyen herhangi bir sınıftan olması mümkündür.
import java.util.regex.Pattern;
...
String düzenliDeyim = ...;
String girdi1 = ...;
boolean tanındıMı = Pattern.matches(düzenliDeyim, girdi1);
...
StringBuilder girdi2 = new StringBuilder("...");
tanındıMı = Pattern.matches(düzenliDeyim, girdi2);
matches metodunun iki özelliği bizi ikinci bir yol aramaya sevkedecektir. Öncelikle, döndürülen boolean değer, kullanıcının sağladığı girdinin düzenli deyimle uyumlu olup olmadığı konusunda fikir verir; girdinin yapısı hakkında herhangi bir bilgi edinmek olanaksızdır. Ayrıca, matches metodu, kendisine geçirilen düzenli deyimi içsel bir gösterime çevirdikten sonra girdinin uyumunu denetler. Bu ise, aynı düzenli deyimin birden çok kez kullanılması durumunda, içsel gösterime dönüşümün tekrar tekrar yapılmasıyla zaman kaybetmek anlamına gelir ki, çözüm Pattern.compile metodunun kullanımından geçer. Bu metot, kendisine geçirilen düzenli deyimi dönüştürür ve dönüşümün sonucunu tutan desen nesnesinin tutacağını döndürür.1 İstenecek olursa, bu tutacak aracılığıyla gönderilecek iletiler, girdinin düzenli deyimle uyuşması sonrasında girdinin bileşenlerine ilişkin sorgularımızı yanıtlayabilir.
import java.util.regex.*;
...
String düzenliDeyim = ...;
Pattern desen = Pattern.compile(düzenliDeyim);
String girdi1 = ...;
Matcher eşleştirci = desen.matcher(girdi1);
boolean tanındıMı = eşleştirici.matches();
if (tanındıMı) ... // girdi1'in bileşenlerini keşfet
...
StringBuilder girdi2 = new StringBuilder(...);
eşleştirici = desen.matcher(girdi1);
tanındıMı = eşleştirici.matches();
if (tanındıMı) ... // girdi2'nin bileşenlerini keşfet

Düzenli Deyim İçeriği


Düzenli deyim tanımında, betimlenmekte olan girdinin karakterleri ile birlikte bazı özel işleçler kullanılabilir. İstenecek olursa, düzenli deyim ile girdi arasındaki eşlemenin nasıl yapıldığını görebilmek adına düzenli deyim öbek adı verilen parçalara ayrılabilir. Ayrıca, eşleştirme sürecinin büyük/küçük harf ayrımı ve dönüşümü, satır ayırma gibi konularda nasıl davranacağını belirleyen bayraklardan da yararlanılabilir.

Düzenli deyim içeriğini oluşturan karakterlerin bazıları, tıpkı karakter sabitlerinde olduğu gibi, özel bir biçimde yorumlanır. Örneğin, '(' yeni bir öbek başlatırken ')' en son başlatılan öbeği kapatır. Dolayısıyla, bu karakterleri [ve diğerlerini] özel görevleri dışında sade bir karakter olarak görmek istediğimizde niyetimizi söz konusu karakterin önüne '\' koyarak belirtmemiz gerekir. Buna ek olarak, bazı sıradan karakterler, önlerine '\' konulmak suretiyle, tıpkı 'n' karakterinin '\n' haline getirilmesinde olduğu gibi, özel bir anlam kazanır. Mesela, 'd' alfabedeki bir harfe karşılık gelen karakteri temsil ederken, '\d' ondalık sayıları yazmakta kullanılan herhangi bir rakamı temsil eden karakter grubuna karşılık gelir. Dolayısıyla, "\\{\\d\\}" yegâne elemanı tek basamaklı bir ondalık sayı olan kümeleri betimler.
System.out.print(Pattern.matches("\\{\\d\\}", "{3}")); // ⇒ true
'\' karakterlerinin çokluğu başınızı döndürdü değil mi? Bunun nedeni, karakter katarımızın bir kez String sabiti, bir kez de düzenli deyim olarak yorumlanmasıdır. Yani, yukarıdaki komutun ilk argümanı önce String sabiti gibi yorumlanacak ve "\{\d\}" haline dönüştürüldükten sonra düzenli deyim olarak ele alınacaktır. Bu sıkıcı durum, alıntılama düzeneği ile biraz olsun düzeltilebilir. Düzenli deyimin içinde geçen \Q (İng., quote), \E (İng., end quote) görülene kadar hiçbir şeyin yorumlanmayacağını bildirir.2 İstenecek olursa, kendisine geçirilen String nesneyi \Q ve \E ile çevreleyen Pattern.quote metodu da aynı amaçla kullanılabilir.

Tek bir karmaşık sayı içeren kümeyi betimleyen düzenli deyim aşağıda verilmiştir. Dikkat ederseniz, düzenli deyimin baş ve son tarafındaki yorumlanmayan parçaların sınırlarını belirleyen \Q ve \E yorumlanmakta olup, sırasıyla, \\Q ve \\E şeklinde yazılmak zorundadır.
import static java.lang.System.out;
import static java.util.regex.Pattern.*;
...
String desen ="\\d\\s*(\\+|-)\\s*\\d";
out.print(
  matches("\\Q{(\\E" + desen + "\\Q)}\\E", "{(3 -5i)}")); // ⇒ true
out.print(
  matches(quote("{(") + desen + quote(")}"), "{(3+4i)}")); // ⇒ true

Karakter Sınıfları


Düzenli deyim oluştururken, karakterlerin yanısıra ortak özellikleri bulunan karakterleri gruplayan karakter sınıflarını da kullanabiliriz. Örneğin, Java'da kullanılabilecek tanımlayıcı adlarını betimlemek istediğimizde, _ ve tüm alfabetik karakterlerin ilk karakter olarak geçebileceğini söyleyerek bu karakterleri aynı sınıfa koymuş oluyoruz. Bu noktada, karakter sınıflarının bir katarı değil tek bir karakteri tanımladığı unutulmamalıdır. Dolayısıyla, tek rakamları betimleyen bir karakter sınıfının düzenli deyim tanımında kullanılması tek rakamlardan oluşan çok basamaklı bir sayının değil, {1', '3', '5', '7', '9'} kümesinin bir tek elemanının kullanılabileceği anlamını taşır.

Köşeli ayraç çifti ile sınırlanan karakterler ve diğer karakter sınıflarından oluşan karakter sınıfları, içerilen karakterlerin yanyana yazılmasının yanısıra, tümleyen sınıfın dışlanması ile de tanımlanabilir. Örneğin, "[13579]" '1', '3', '5', '7' ve '9' karakterlerinden oluşan bir karakter sınıfını tanımlarken, "[^02468]" '0', '2', '4', '6' ve '8' karakterleri dışındaki tüm karakterleri kapsar. Bu noktada, ikinci örneğimizin sadece tek rakamları kapsamadığını, listelenenler dışındaki herhangi bir karakteri kapsadığının altını çizelim. Buna karşılık, rakamların ve çift sayı olmayan karakterlerin kesişimini alan "[0-9&&[^02468]]" deyimi, tümleme işleci (^) ve kesişim işlecinden (&&) yararlanarak ilk örneğimizle eşdeğer bir sonuç verir.

Betimlenen karakter sınıfındaki karakterlerin ardışık olması halinde, tüm karakterleri teker teker yazmaktansa aralık işlecini (-) kullanabiliriz. Buna göre, "[abcçde12345]" yerine "[a-eç1-5]" yazmak yeterli olacaktır. [Dikkat ederseniz, aralık tanımının ASCII temelli olması nedeniyle Türkçe'ye özel 'ç' ayrıca eklenmek zorunda.] Bunun bir sonucu olarak—karakterler arasında kullanıldığında aralık işleci görevini gördüğü için—'-' sınıf içinde ancak ve ancak birinci sırada geçebilir. Dolayısıyla, temel aritmetik işleçlerin tanımı "[-+*/]", bu sınıfın tümleyeni "[^-/*+]" şeklinde yapılmalıdır.

Sıklıkla kullanılan bazı karakter sınıfları önceden tanımlanmışlardır. Söz konusu karakter sınıflarının yeniden tanımlanması hem zamandan kayıp hem de hataya açık bir çabadır. Ancak, bu karakter sınıflarından bazılarının ASCII temelli tanımlandığı ve özel ayarlamalar yapılmadığı müddetçe alfabemizdeki kimi harfleri barındırmayacağı akılda tutulmalıdır.
  • .: Unicode tablosundaki herhangi bir karakter.
  • \p{ASCII} ([\x00-\x7F]): Unicode tablosunun ASCII altkümesinde bulunan karakterler.
  • \p{Alpha} ([a-zA-Z]): ASCII tablosundaki alfabetik karakterler. \p{Lower} ve \p{Upper}, sırasıyla, küçük ve büyük alfabetik karakterleri tanımlar.
  • \p{Digit} veya \d ([0-9]): Ondalık sayıların basamaklarında kullanılabilecek rakamlar. 16'lı tabandaki sayıların basamakları \p{XDigit} ile tanımlanır. Ondalık sayıların tümleyeni olan sınıf—yani, ondalık rakam olmayan karakterler—\D ile temsil edilir.
  • \p{Alnum} ([\p{Alpha}\p{Digit}]): Alfanümerik karakterler.
  • \w ([a-zA-Z0-9_]): Çoğu programlama dili tarafından tanımlayıcı adı oluşturmakta kullanılan karakterler. Tümleyen sınıf \W ile temsil edilir.
  • \p{Punct}: Noktalama imleri, ayırıcılar ve işleçler.
  • \p{Graph} ([\p{Alnum}\p{Punct}]): Fiziksel gösterimi bulunan karakterler.
  • \p{Print} ([\p{Graph}\x20]): Basılabilir karakterler. [\x20 boşluk karakterine karşılık geliyor.]
  • \p{Cntrl} ([\x00-\x1F\x7F]): Kontrol karakterleri.
  • \p{Space} veya \s ([ \t\n\x0B\f\r]): Bir metnin görünümünü düzenlemek için yararlanılan beyaz boşluk karakterleri.
  • \p{Blank} ([ \t]): Boşluk ve sekme karakterleri.

Yukarıdaki karakter sınıflarından ASCII tablosuna sınırlı olanların davranışı UNICODE_CHARACTER_CLASS bayrağı kullanılarak değiştirilebilir. Buna göre; "\p{Lower}" ile "ç" eşleşmezken, "(?U)\p{Lower}" ile "ç" eşleşecektir. Aynı etkiyi, Pattern.compile metoduna ikinci argüman olarak geçirilen bayraklar arasına UNICODE_CHARACTER_CLASS sabitini koyarak da yaratabiliriz.
import java.util.regex.Pattern;
import static java.util.regex.Pattern.*;
...
Pattern küçükHarf = compile("(?U)\\p{Lower}");
küçükHarf = compile("\\p{Lower}", UNICODE_CHARACTER_CLASS)); // Yukarıdakiyle aynı
Unicode tablosunu kullanmanın bir diğer yolu, Character sınıfındaki yüklemler vasıtasıyla işini gören karakter sınıflarından yararlanmaktır. Bu karakter sınıflarının adları, Character sınıfının ilişkin yüklemindeki is öneki yerine java konulmasıyla oluşturulur. Örnek olarak, argümanının birbirini tamamlayan karakter çiftlerinden ((), [], {}, <>, vd.) birine ait olup olmadığını denetleyen isMirrored yüklemini ele alalım. Düzenli deyimimizdeki bir karakterin bu tür bir karakter ile eşleşmesini istediğimizde yapmamız gereken, isMirroredjavaMirrored'a çevirmek ve karakter sınıfı adı olarak kullanmaktır. Dolayısıyla, "\p{javaMirrored}" ">" veya "(" ile eşleşirken çift olarak gelmeyen diğer karakterlerle eşleşmeyecektir.
küçükHarf = compile("\\p{javaLowerCase}"); // Yukarıdakilerle aynı
Pattern çifttenBiri = compile("\\p{javaMirrored}");

Düzenli Deyim İşleçleri


Temel düzenli deyim oluşturma işleci olan bitiştirme, iki karakterin [veya karakter öbeğinin] yanyana yazılmasıyla ifade edilip özel bir simgenin kullanımını gerektirmezken, geçiş sayısını belirtmek amacıyla farklı niceleme işleçlerinden yararlanılabilir. Karakteri takiben yazılan bu işleçlerden joker grubu olarak adlandırabileceklerimiz *, + ve ?, sırasıyla, 0 veya daha fazla sayıda, 1 veya daha fazla sayıda ve 0 veya 1 kez anlamına gelir. Yineleme sayısının kesin olması durumunda, kıvrımlı ayraç çifti ({}) arasında yazılacak bir sayı işi görecektir; yineleme sayısının alt ve üstten sınırlandırılması ise virgülle ayrılmış sınırların kıvrımlı ayraç çifti arasında verilmesiyle mümkün olurken, üst sınırın yazılmaması yinelemenin alt sınırdan az olmamak üzere belirsiz bir sayıda olabileceği anlamına gelir.

Yineleme işleçlerinden +, * ve bitiştirmenin birlikte kullanımına denk olduğu için işlevsellik adına bir şey katmaz. Örneğin "a+" "aa*" şeklinde ifade edilebilir. Ancak; okunabilirliği artırması ve düzenli deyim derleyicisinin kimi eniyilemeleri yapmasını olanaklı kılması nedeniyle "a+" deyiminin kullanımı daha doğru olacaktır. Benzer gözlemler diğer işleçlerin bazı kullanımları için de yapılabilir. Mesela, aynı nedenlerden ötürü (okunabilirlik ve eniyileme) "(ab|b)" yerine—|, ayırdığı deyimler arasında uygulanan "veya" işlecine karşılık gelir—"a?b" deyiminin yeğlenmesi yerinde olacaktır.

Niceleyicilerin sadece en son karakteri [veya karakter öbeğini] nicelediği unutulmamalıdır. Örneğin, "Ali+" "Al" ile başlayıp bir veya daha fazla sayıda 'i' ile devam eden girdileri betimler, bir veya daha fazla sayıda "Ali" değerini değil. Buna karşılık, "(Ali){2,}" şeklinde tanımlanan düzenli deyim, ayraçlar vasıtasıyla yapılan öbek tanımı sayesinde, iki veya daha fazla sayıda "Ali" değerinin geçtiği karakter katarlarını betimler.

Önceki paragraflarda anlatılan niceleyicilerin işleyiş mantığı, girdide sağlanan karakterlerden mümkün olduğunca çok tüketecek şekilde bir eşleme yapmak şeklindedir. Örneğin, "birberber" sabitini başarılı bir şekilde betimleyen ".*ber" düzenli deyiminde bulunan ".*" olabildiğince çok karakter yutacak ve "birber" ile eşleşecektir. Bir diğer deyişle, düzenli deyimin sonundaki "ber" girdinin sonundaki "ber" ile eşleşecektir. Yinelemenin en az sayıda karakter yutularak yapılması için ise niceleyici sonrasına ? konulması gerekir. Mesela, "*.?ber" deyimindeki "*.?", "birberber" içindeki ilk üç karakteri tüketecektir. Yani, eşleşmenin mümkün olduğu durumlarda ilk kullanımdaki niceleyici olabildiğince çok yiyerek açgözlü davranırken ikincisi olabildiğince az yiyerek gönülsüz davranacaktır.

İşleyiş ayrıntılarına girildiğinde, açgözlü niceleyicilerin bir zaafı ortaya çıkar: düşük performans. Örneğimiz üzerinden anlamaya çalışalım. Eşleştirici, "birberber" sabitinin ".*ber" düzenli deyiminin betimlediği kümede olup olmadığına karar vermek istediğinde, öncelikle ".*" ile tüm girdiyi tüketir ve "ber" kısmının eşleştirilmesi için geriye hiçbir şey kalmaz. Sonucun başarısızlık olması üzerine eşleştirici, bir karakter geriye sarar ve ".*" ile "birberbe" sabitini eşleştirerek "ber" kısmını "r" ile eşleştirmeye çalışır. Bu da olmayınca, yapılacak olan bir kere daha geriye sarmaktır. Bu sefer, ".*" "birberb" ile eşleştirilir ve arda "er" kalır. Üçüncü hüsranın sonrasında girdinin geriye sarılması ile "*." "birber", "ber" ise sondaki "ber" ile eşleşir ve bu güzel haber [dördüncü denemeyi takiben] kullanıcıya muştulanır. Geri sarmanın getireceği performans düşüklüğünün önüne geçmek, kimi zaman bir diğer niceleyici grubunun kullanılması ile mümkün olabilir: sahiplenici niceleyiciler. Benzer şekilde çalışan bu niceleyiciler, açgözlü eşdeğerlerinin aksine geri sarma işlemine başvurmaz ve eşleştirmenin başarısızlıkla sonlandığını ilan eder. Bundan dolayı, "birberber" ".*+ber" tarafından kabul edilmeyecektir. Çünkü, ".*+" açgözlü davranarak tüm girdiyi tüketecek ve geriye "ber" ile eşleştirilecek bir şey kalmayacaktır. Bu, geriye sarmanın olmaması ile birleştiğinde, sonucun olumsuz olacağı anlamına gelir. Yani, açgözlü niceleyiciyle eşleştirilen bazı girdiler sahiplenici niceleyiciyle eşleştirilemeyecektir. O zaman, işlev açısından eşdeğer olup bize performans açısından kazandırdıkları bir örnek görerek sahiplenici niceleyicilerin gerekliliğine ikna olalım.
Pattern.matches("\\d*+\\(Ev\\)", "123456789Ev)") // → false
Bir numarayı ev telefonu olduğu bilgisiyle birlikte betimleyen bu düzenli deyimde, sahiplenici niceleyici (*+) yerine açgözlü uyarlamanın (*) kullanılması yarar getirmeyeceği gibi daha düşük bir performansa neden olacaktır. Çünkü, dokuz basamaklı sayıyı yedikten sonra girdide '(' arayan eşleştirici, dokuz kez geriye sarıp önceki karakterlerin hiçbirinin aradığı karakter olmadığını pahalı yoldan öğrenecektir. Yani, geriye sarma sonucunda fazladan keşfedilecek bir eşlemenin olmaması geri sarmaya tenezzül etmeyen sahiplenici niceleyiciyi öne çıkarmaktadır.

Bayraklar


Pattern.compile metodu, eşlemenin nasıl yapılacağını etkileyen bayraklar da alabilir. Eşleştiricinin üzerinde çalışacağı tüm girdiler için geçerli olacak bu bayraklar, istenecek olursa, benzer bilgilerin düzenli deyimin içine yerleştirilmesi suretiyle de etkinleştirilebilir veya geçersiz hale getirilebilir. Pattern sınıfı içinde sabit olarak tanımlanan bu bayraklar ve anlamları şöyledir.
  • UNICODE_CASE (?u): Büyük küçük harfler arasındaki dönüşüm ve eşitlik denetimleri Unicode tablosu temel alınarak yapılır. Türkçe'yi temel alarak işlem yapmak istiyorsanız—mesela, i'den I yerine İ'ye dönüşüm yapılmasını istiyorsanız—bu bayrağı aklınızdan çıkarmamanızda yarar olacaktır.
  • CASE_INSENSITIVE (?i): Harflerin eşlenmesi sırasında büyük küçük ayrımı yapılmayacaktır. UNICODE_CASE ile birlikte kullanılmadıkça bu bayrağın etkisinin ASCII tablosundaki karakterlere sınırlı kalacağı unutulmamalıdır.
  • UNICODE_CHARACTER_CLASS (?U): Kullanılacak ASCII temelli karakter sınıflarının Unicode tablosunu temel alarak işlev görmesini sağlar. Bu bayrağın etkin kılınması ile birlikte, UNICODE_CASE bayrağının da otomatikman etkinleştirildiği akılda tutulmalıdır.
  • COMMENTS (?x): Düzenli deyim içindeki beyaz boşluklar ve '#' karakteri ile sonrasındaki satır sonuna kadar her şey göz ardı edilir.
  • MULTILINE (?m): Girdinin satır ayırıcı karakter(ler)inin (Unix temelli işletim dizgelerinde yeni satır karakteri ('\n'), Microsoft işletim dizgelerinde satır başı ('\r') ve yeni satır karakterleri) olduğu yerlerden ayırarak satırlar halinde incelenmesini sağlar. Buna göre, ^ ve $ artık girdinin başı ve sonunu değil, incelenmekte olan satırın başı ve sonunu belirtecektir.
  • UNIX_LINES (?d): Satırların Unix temelli işletim dizgelerinde olduğu gibi yeni satır karakteri ile sonlandığı varsayılacaktır.
  • DOTALL (?s): "." deyiminin satır ayırıcıları da betimlemesini sağlar. Bu, satır ayırıcılarının da ".*" ile yutulacağı anlamına gelir ve bu sebepten dolayı kimi zaman tek satır kipi olarak da adlandırılır.
  • LITERAL: Eşleştiricinin karakter katarını yorumlamayacağını bildiren bu bayrağın etkisi, düzenli deyimin "\Q"-"\E" çiftiyle çevrelenmesi yoluyla da elde edilebilir.
  • CANON_EQ: Unicode tablosunda birden çok nokta ile temsil edilen veya birden çok karakterin bileştirilmesi ile de oluşturulabilecek karakterlerin değişik karşılıklarının birbirine eşit olmasını sağlar. Örneğin, 231 (0xE7) nolu konumdaki ç harfi, c harfine kanca iminin (\u0327) monte edilmesiyle de oluşturulabilir.
    Pattern desen = compile("c\u0327", CANON_EQ);
    out.println(desen.matcher("ç").matches()); // ⇒ true
    
    Dikkat edecek olursanız, \u önekiyle sağlanan karakterlerin, String sabitinin oluşturulması sırasında içselleştirilmesi nedeniyle, önüne ikinci bir \ konulmasına gerek yoktur.

Bayrakların düzenli deyim içinde belirtilmesi durumunda, birden çok sayıda bayrak aynı noktada etkinleştirilebileceği gibi, istenen bayraklar geçersiz de kılınabilir. Örneğin, (?Ui) betimlemenin Unicode tablosunu temel alarak büyük-küçük farkı gözetmeksizin yapılacağını ifade ederken, (?-d) [belki de daha önce etkinleştirilmiş olan] Unix usulü satır ayırıcı bayrağını o anki noktadan itibaren geçersiz hale getirmektedir.

Öbekler


Kimi zaman, düzenli deyimle girdi arasında bir eşleşmenin olup olmamasının yanısıra, eşleşme ile ilgili kimi ayrıntıları da öğrenmek isteriz. Örneğin, telefon numaralarını betimleyen bir düzenli deyimde, alan kodu ve numara ile ayrı ayrı ilgileniyor olabiliriz. Bu gibi bir durumda yapmamız gereken, düzenli deyimi karakter öbeklerine ayırmak ve eşleme sonrasında bu öbekleri sorgulamak olmalıdır.

Öbekler, ilgilenilen karakterlerin ayraç çifti arasına alınması ile oluşturulur. Eşleştirme sonrasında atıfta bulunulabilmesi için, öbekler açış ayraçlarının düzenli deyimdeki geçiş sırasına göre numaralandırılırlar. Örneğin, genç kızlık soyadını koruyarak adını yazan bayanların adları "(\p{Alpha}+)\s((\p{Alpha}+)-(\p{Alpha}+))" ile betimlenebilir. [Düzenli deyimi sınamak istediğinizde, \ yerine \\ koymayı unutmayınız.] Bu düzenli deyim, ad ile başlayıp bir boşlukla devam eden ve birbirinden - ile ayrılmış iki soyadını tanımlamaktadır. Ad ilk öbekle eşleşirken, tüm soyadı ikinci, eşin soyadı üçüncü, ve nihayet, genç kızlık soyadı dördüncü öbek olarak eşleştirilecektir.

Oluşturulan bir öbeği atıfta bulunarak kullanmak istediğimizde, bunu geçiş sırasının önüne \ koyarak sağlayabiliriz; \0 her zaman girdinin tümü ile eşleştirilir. İlk üç rakamın alan kodu ile aynı olduğu telefon numaralarını betimleyerek buna bir örnek verelim: "(\d{3})-(\1\d{4})". İlk öbeği alan kodu, ikinci öbeği telefon numarası ile eşleştiren bu düzenli deyim, "532-5321234" numarasını betimlerken "232-5321234" numarasını betimlemeyecektir.

İstediğimiz takdirde, öbeklere kendilerine verilen ad ile de atıfta bulunulabilir. Bunun için, öbeğe uygun görülen adın üçgen ayraç çifti arasına alınıp adlı öbeği başlatan (? sonrasına yazılması gerekir; öbeğe atıfta bulunmak istenmesi durumunda ise, öbek adının üçgen ayraçlarla birlikte \k'ye eklenmesi yeterli olacaktır. Buna göre, telefon numarası örneğimiz şöyle de yazılabilir: "(?<kod>\d{3})-(\k<kod>\d{4})". Bu arada; bir öbeğe ad verilmesi, söz konusu öbeğin geçiş sırasının geçersiz olduğu anlamına gelmez. İsteyecek olursak, düzenli deyimimizi "(?<kod>\d{3})-(\1\d{4})" olarak da tanımlayabiliriz.

Kimi zaman, eşleştirilen bir öbeği göz ardı etmek isteyebiliriz. Yani; eşleşmenin başarılı olması sonrasında söz konusu öbeğin hesaba katılmasını istemeyebiliriz. Bunun için yapmamız gereken şey, öbeğin (?: ile başlatılmasından ibarettir. Bu durumda, göz ardı edilen öbeğin açış ayracı öbek sırasını saptamakta yararlanılan sayacı etkilemeyecektir. Son olarak, şu nokta da unutulmamalıdır: adlandırılan öbekler göz ardı edilemezler.

Eşleştirici Kipleri


Java'daki düzenli deyim desteği, Pattern ve Matcher sınıflarındaki matches metotlarında gerçekleştirilenden farklı eşleştiriciler de sağlar. Bunlardan Pattern sınıfındaki split iletisi, argümanındaki CharSequence kategorisine ait katarı, ileti alıcının temsil ettiği deseni kullanarak String nesnelerine böler ve bu String'leri içeren diziyi döndürür.
...
Pattern desen = Pattern.compile(":");
String[] bilgi = desen.split("Gökçe Begüm:Ege:532-1234567");
// bilgi[0] ← "Gökçe Begüm", bilgi[1] ← "Ege", bilgi[2] ← "532-1234567"
İstenecek olursa, split'e sağlanacak ikinci argüman ile döndürülecek dizinin eleman sayısı sınırlandırılabilir. Örneğin, yukarıdaki kullanımda ikinci argüman olarak 2 geçirilmesi, ilki "Gökçe Begüm" ikincisi "Ege:532-1234567" değerine sahip iki elemanlı bir dizi döndürecektir.

Göz atacağımız diğer eşleştiriciler marifetlerini Matcher nesnelerine gönderilen iletiler yoluyla gösterirler. Dolayısıyla, düzenli deyimin compile metodu ile derlenmesi sonucu elde edilen desene (Pattern nesnesi) matcher iletisinin gönderilmesi yapacağımız şeylerin başında gelmelidir. İkinci adım olan eşleştiricinin çağrılması öncesinde, işlemin etkili olacağı girdi bölgesi region iletisi ile belirtilebilir. [Daha sonra eşleştiricimizin girdinin hangi bölümünü ele aldığını görmek istersek, ilişkin bölgenin tanımlanmasında kullanılan argüman değerlerini döndüren regionStart ve regionEnd iletilerinden yararlanabiliriz.] Son adım olarak ise, desen ile girdinin uyuşması halinde icra edilecek bir inceleme aşaması vardır. Bu, String argümanlı group iletisinin yanısıra MatchResult arayüzünde yer alan iletilerin eşleştiriciye gönderilmesi ile yapılablir. Dolayısıyla, tipik eşleştirici kullanımı aşağıdaki şablonu takip edecektir.
import java.util.regex.*;
...
Pattern desen = Pattern.compile(...);
Matcher eşleştirici = desen.matcher(...);
eşleştirici.region(başİndis, sonİndis + 1); // Seçimli
... // Eşleştiriciyi uygun bir kipte kullan.
if (tanındıMı) {
  MatchResult sonuç = eşleştirici.toMatchResult();
  ... // sonuç'u incele
}
Eşleştiricinin yaratılması sonrasında girdinin ele alınması noktasında farklı çalışma kiplerini temsil eden üç yüklemden bahsedebiliriz: matches, lookingAt, find. Önceki örneklerimizden de gördüğümüz gibi matches, girdi ile [düzenli deyimin içselleştirilmiş karşılığı olan] deseni eşleştirirken girdinin tümünü tüketmeye çalışır; aksi takdirde, eşleştirme sonucu olumsuz olacaktır. Bundan dolayıdır ki, ".*ber" "birberbere" sabitini tanımaz. Çünkü, ".*" ile "birber" ve "ber" ile girdideki "ber" eşleşmesini takiben sondaki "e" açıkta kalır ki, bu matches metodundan false döndürülmesine neden olur. Artık girdinin eşleşmeyi engellemesi istenmiyorsa, matches yerine lookingAt iletisi kullanılmalıdır. matches'da olduğu gibi, girdinin ilgilenilen bölgesinin başından itibaren eşleştirerek işini gören lookingAt, girdinin sonunda eşleşme ile kapsanmayan karakterlerin kalmasına itiraz etmez ve true döndürür. Dolayısıyla, bu eşleştirici kipinde ".*ber" "birberbere" sabitini betimliyor kabul edilecektir. Ne var ki, lookingAt iletisi de, tıpkı matches gibi, girdi başının desenle uyuşmaması durumunda devamında ne olursa olsun false döndürür. Örneğin "a.*b", ne matches ne de lookingAt ile kullanıldığında, "qawdesb" girdisini betimlemeyecektir. Çünkü, her iki eşleştirici kipi de düzenleyici deyim başındaki 'a' değerini girdinin en başında arayacak ve başarısızlık sonrasında false döndürecektir. Sıkıntımızın çözümü, eşleştiriciyi find kipiyle kullanmakta yatar. Seçimli bir int argüman bekleyen bu eşleştirici kipi, girdinin başındaki eşleşmeyen karakterleri atlar ve deseni girdi içinde arar; girdinin başındaki ve sonundaki eşleşmeyen parçalar sonucun olumsuz olmasına neden olmaz. Dolayısıyla, "a.*b" düzenli deyiminin "qawdesbc" içinde aranması, baştaki "q" değerini göz ardı ettikten sonra, "awsdesb" ile eşlemeyi sağlayacak ve sonda artan "c" değerine rağmen true döndürecektir.

Eşleştirici kipleri arasındaki bir diğer fark, daha önceden eşleştirilmiş desen-girdi çiftinin sıfırlanmaksızın tekrar kullanılması durumunda sergilediği davranış biçimidir. Girdinin tümünü eşleştirmeye çalışan matches, girdiyi tüketmiş olduğu için ikinci ve sonraki kullanımlarında her zaman false döndürürken, lookingAt her zaman ilk kullanımda döndürdüğü sonucu döndürür. Dolayısıyla, aşağıdaki kod parçası sonsuz döngü içinde standart çıktı ortamına ba yazmaktan başka bir şey yapmayacaktır. [MatchResult arayüzü iletilerinden olan group, argümanında geçirilen sıradaki öbeği döndürür; 0 eşleştirilen tüm girdi parçasına karşılık gelir ve aynı etki group iletisini argümansız kullanmakla da yaratılabilir.]
String düzenliDeyim = "ba", girdi = "baba";
Pattern desen = Pattern.compile(düzenliDeyim);
Matcher e = desen.matcher(girdi);
while (e.lookingAt())
  System.out.println(e.group(0));
Bu durumun önüne geçilmesi lookingAt iletisinin üzerinde çalıştığı bölgenin aşağıdaki gibi değiştirilmesi ile mümkündür. Argümansız end iletisi, en son eşleştirmenin tükettiği son karakterin ötesindeki ilk karakterin indisini döndürür; öbek sayısını belirten bir tamsayının geçirilmesi halinde ise, aynı ileti belirtilen öbeğin sonunu takip eden karakterin indisini döndürür. [e.end() gönderisinin etkisi, e.start() + e.group().length() ile de sağlanabilir.]
while (e.lookingAt()) {
  System.out.println(e.group(0));
  e.region(e.end(), girdi.length());
}
find iletisi ise, başarılı eşleştirmenin ardından girdinin eşleşme sonrasındaki karakterinden devam ederek işini görür. Buna göre, yukarıdaki kod parçası şöyle de yazılabilir.
while (e.find())
  System.out.println(e.group(0));
Girdinin aynı veya farklı bölgelerinin farklı desenler kullanılarak eşleştirilmesi için, girdiyi parçalamak ve farklı eşleştiriciler kullanmaktansa, usePattern iletisi tercih edilmelidir. Bu iletinin kullanıldığı noktadan itibaren, ileti alıcı konumundaki eşleştirici girdinin sonuna veya bir sonraki usePattern kullanımına kadar tüm eşleştirmeleri söz konusu iletiye geçirilen deseni kullanarak yapacaktır.
String düzenliDeyim = "ab", girdi = "abba";
Pattern desen = Pattern.compile(düzenliDeyim);
Matcher e = desen.matcher(girdi);
if (e.find()) {
  System.out.println(e.group(0));
  e.usePattern(Pattern.compile("ba"));
  if (e.find())
    System.out.println(e.group());
}
Aynı eşleştiricinin farklı bir desenle kullanılmasını olanaklı kılan bir diğer ileti, CharSequence kategorisindeki yeni düzenli deyimi argüman olarak bekleyen reset'tir. usePattern'dan farklı olarak bu ileti, eşleştiriciyi girdinin başına konumlandırarak bölge tanımlarını geçersiz kılar. Bu işlemin desen değiştirilmeden yapılması için reset iletisinin argümansız uyarlamasının kullanılması yeterli olacaktır.

Eşleştiricilerin kimi kullanım desenleri, yüksek kullanım potansiyelleri nedeniyle Matcher sınıfında gerçekleştirilen iletiler halinde desteklenirler. Bunlardan biri olan appendReplacement, daha ziyade find ile birlikte kullanılır ve girdinin eşleşmeyen kısmını ilk argümanındaki StringBuffer türlü karakter tamponuna olduğu gibi eklerken, eşleştirilen parça yerine ikinci argümandaki String'i koyar. Bu iletiyi tamamlayan appendTail ise, başarısız bir eşleştirme çabası sonrasında girdinin ilişkin parçasını argümanındaki karakter tamponunun sonuna ekler.
String düzenliDeyim = "k", girdi = "bakbakbakşuna";
Pattern desen = Pattern.compile(düzenliDeyim);
Matcher e = desen.matcher(girdi);
StringBuffer tampon = new StringBuffer();
while (e.find())
  e.appendReplacement(tampon, "h");
String sonuç = e.appendTail(tampon).toString();
Buna göre, yukarıdaki döngünün ilk dönüşündeki eşleştirme ilk iki karakteri atlayacak ve desenimizi üçüncü konumdaki "k" ile eşleştirecektir. Sonuç olarak, tampon ile gösterilen bölgeye atlanan parça ("ba") değiştirilmeden, eşleştirilen parça ("k") ise "h" olarak eklenecektir. Dolayısıyla, ilk döngünün sonuna gelindiğinde tampon değişkeninde "bah" biriktirilmiş olacaktır. Döngünün ikinci ve üçüncü dönüşlerinde de benzer şekilde çalışan kod parçası, tamponda biriktirilen değeri "bahbahbah" haline dönüştürecektir. Dördüncü dönüşte, deseni "şuna" ile eşleştirmeye çalışan find olumsuz sonuç döndürecek ve döngüden çıkılacaktır. Bunu takiben gönderilen appendTail iletisi ise eşleştirilemeyen girdiyi tampon sonuna ekleyerek işi tamamlayacaktır.

Bir önceki paragrafta anlatılan değiştirme işlemi, replaceAll iletisi ile de yapılabilir. Anılan ileti, desenin eşleştirildiği girdi bölümlerini argümanında geçirilen String ile değiştirir. Benzer bir işlev gören repeatFirst ise, değişikliği eşleşmenin olduğu ilk noktada yapmakla yetinir.
String düzenliDeyim = "k", girdi = "bakbakbakşuna";
Pattern desen = Pattern.compile(düzenliDeyim);
Matcher e = desen.matcher(girdi);
String sonuç = e.replaceAll("h"); // sonuç ← "bahbahbahşuna"

  1. İki yöntem ile bir programın doğrudan yorumlanması ve Bytecode gibi bir aradile çevrildikten sonra yorumlanması arasında koşutluk kurabiliriz.
  2. Bu uygulama, ifadenin konuşmacı tarafından yorum katılmadan aktarıldığını ifade eden dilimizdeki "Başbakan aynen şöyle dedi: ...", İngilizce'deki "The Prime Minister said, quote ... end quote" kalıplarına benzetilebilir.

Hiç yorum yok:

Yorum Gönder

Not: Yalnızca bu blogun üyesi yorum gönderebilir.