C, PHP, VB, .NET

Дневникът на Филип Петров


* Регулярни изрази

Публикувано на 27 септември 2009 в раздел ПИК3 Java.

Вече се запознахме с методите indexOf() и lastIndexOf() за String и StringBuffer, чието действие беше да връщат индекс на началото на търсена дума вътре в текст. Чрез допълнителен параметър можеше да се търси второ, трето и т.н. срещане на тази дума. Какво да правим обаче ако ние не знаем точната дума, която търсим? Ако например имаме подаден текст, от който трябва да извадим e-mail адрес, то ние няма как да използваме споменатите два метода, защото ние не знаем самия низ за търсене (нали всъщност го търсим). Именно за такива ситуации се използват регулярните изрази (regular expressions).

Регулярен израз означава търсене на текст по даден шаблон. Обяснено по най-прост начин шаблон означава, че вместо да търсим „дали А е едно и също с Б“ вече търсим „дали А прилича на Б“. На начинаещ програмист, който тепърва се сблъсква с регулярни изрази, те ще му изглеждат изключително сложни за разбиране. След като овладее „езика им“ обаче нещата се променят. Всъщност всеки един регулярен израз може да бъде преведен с обикновена разговорна реч.

Нека в началото дефинираме най-важните символи, които ще използваме и дадем тяхното значение в разговорна реч:

  • text – търси думата „text“;
  • [a-z] – малки букви;
  • [^a-z] – всички символи БЕЗ малките букви;
  • [A-Z] – главни букви;
  • [^A-Z] – всички символи БЕЗ главните букви;
  • [0-9] – цифри от 0 до 9;
  • [^0-9] – всички символи БЕЗ цифри;
  • [a-zA-Z] – малки и големи букви;
  • [0-9a-zA-Z] – малки и големи букви и цифри;
  • [^%ABW] – всички символи без % и буквите A, B и W;
  • [a-z-[mnp]] – малки букви от a до z, но без буквите m, n и p;
  • [a-z-[m-p]] – малки букви от a до z, но без буквите от m до p;
  • \s – интервал, табулация, нов ред, връщане в начало на ред (еквивалентно на [ \t\n\r]);
  • \S – всички символи БЕЗ интервал, табулация, нов ред и начало на ред ([^ \t\n\r]);
  • \d – цифра (еквивалентно на [0-9]);
  • \D – всички символи БЕЗ цифрите (еквивалентно на [^0-9];
  • \w – букви, цифри и долна черта (еквивалентно на [a-zA-Z0-9_]);
  • \W – всички символи БЕЗ букви, цифри и долна черта ([^a-zA-Z0-9_]).

Предполагам вече се досетихте за общата идея. В квадратни скоби се заграждат шаблони на символ, който търсим. Така ако оградим [cat], то все едно сме казали „търсим символите ‘c’, ‘a’ или ‘t'“. Ако сложим ^ преди тях, то сме казали „търсим всички символи различни от…“. Специалните символи започващи с наклонена черта (\s. \S, \d,…) са просто съкратени записи на често използвани комбинации.

Оградените с квадратни скоби поредици от символи се наричат групи. Всяка група отговаря за един символ. Когато искаме да търсим потаряемост, то използваме следните операции:

  • + – „един или повече“, например „[a-z]+“ означава „търсим една или повече малки букви;
  • * – „нула или повече“, например „[A-Z]*“ означава „нула или повече главни букви“;
  • ? – „нула или една“, например „[0-9]?“ означава „нула или една цифра“.

Освен това доста често ни се налага не да търсим вътре в низ, а да анализираме цял низ. Затова могат да се използват следните две операции:

  • ^ – „започва с…“, например „^\w+“ означава „започва с една или повече букви, цифри или долна черта;
  • $ – „завършва с…“, например „[0-9]$“ означава „завършва с цифра“.

Нека разгледаме следния пример: ^\w+[@][a-zA-Z0-9\-]+[.][a-zA-Z]+$

Можем да го преведем като: „започва (^) с една или повече букви , цифри или долна черта (\w+), продължава със символ @ ([@]), продължава с една или повече букви, цифри или тире ([a-zA-Z0-9\-] – като \ е „escape character“ за тирето), продължава с точка ([.]), продължава няколко малки или големи букви ([a-zA-Z]) и точно така завършва ($). Е, не е напълно точно и изчерпателно, но с този шаблон можем да търсим дали един String е e-mail адрес от вида name@domain.dom (но не „хваща“ т.нар. country code domains, т.е. например name@domain.co.dom).

Сега вече можем да започнем да пишем програми за търсене по такива шаблони.  Ето как мога да валидирам моя e-mail адрес:

    // Низ, който ще сравняваме
    String email = "philip@abv.bg";
    // Саздаваме шаблон
    java.util.regex.Pattern p = null;
    p = java.util.regex.Pattern.compile("^\\w+[@][a-zA-Z0-9\\-]+[.][a-zA-Z]+$");
    // Създаваме "търсач" чрез шаблон и низ
    java.util.regex.Matcher m = p.matcher(email);

    if (m.matches()) System.out.println(email+" looks like an e-mail");
    else System.out.println(email+" do not look like an e-mail");

Метод matches() връща true или false в зависимост дали целия низ съвпада с шаблона.

Нека обаче разгледаме по-сложен вариант – търсене на подниз. Горният пример търсеше съвпадение по шаблон на целия низ. За да извършим това първо трябва да премахнем условията „започва с“ (^) и „завършва с“ ($). Освен това ни трябва функционалност да „прихващаме“ намерените поднизове. Това се реализира като направим т.нар. „групи“. В най-простия случай нека просто оградим търсения шаблон в скоби и така да създадем една „шаблонна група“:

    // Низ, в който сме записали имена и e-mail адреси на нов ред
    String str = "Philip: philip@abv.bg\nPetar: petar@domain.dom";
    // Саздаваме шаблон с една група
    java.util.regex.Pattern p = null;
    p = java.util.regex.Pattern.compile("(\\w+[@][a-zA-Z0-9\\-]+[.][a-zA-Z]+)");
    // Създаваме "търсач" чрез шаблон и низ
    java.util.regex.Matcher m = p.matcher(str);
    while(m.find()){
      // m.group() връща текста на текущата група
      System.out.println(m.group());
    }

Метод find() връща true или false и отговаря за това дали шаблонът е намерен или не. При повторното му извикване се търси второ срещане и т.н. Същото се случва с метод group(). При първоначалното му извикване връща текста на първата група. При повторното му извикване връща текста на втората и т.н.

Групите обаче може да са повече от една. Ето как можем да извлечем имената и пощите от горния пример:

    // Низ, в който сме записали имена и e-mail адреси на нов ред
    String str = "Philip: philip@abv.bg\nPetar: petar@domain.dom";
    java.util.regex.Pattern p = null;
    p = java.util.regex.Pattern.compile("([a-zA-Z]+): (\\w+[@][a-zA-Z0-9\\-]+[.][a-zA-Z]+)");
    java.util.regex.Matcher m = p.matcher(str);
    while(m.find()){
      // m.group(1) връща текста на първа група, а m.group(2) на втора група
      System.out.println(m.group(1)+" has e-mail address "+m.group(2));
    }

Когато регулярният израз е много сложен и групите са много, можем да се възползваме от „именовани групи“, които идват с Java 7. При тях в началото след ъгловите скоби се добавя ?<име>, което става и името на групата. Ето как можем да именоваме групите от горния пример:

    String str = "Philip: philip@abv.bg\nPetar: petar@domain.dom";
    java.util.regex.Pattern p;
    p = java.util.regex.Pattern.compile("(?<name>[a-zA-Z]+): (?<email>\\w+[@][a-zA-Z0-9\\-]+[.][a-zA-Z]+)");
    java.util.regex.Matcher m = p.matcher(str);
    while (m.find()) {
        // m.group("име") връща теста от подаденото име на група
        System.out.println(m.group("name") + " has e-mail address " + m.group("email"));
    }

Чрез „.*“ указваме, че очакваме „максималната редица от какви да е символи“. Ето един прост пример:

    String str = "This represents <b>HTML bold tag</b> and <i>italic tag</i>";
    java.util.regex.Pattern p = null;
    p = java.util.regex.Pattern.compile("(<.*>)");
    java.util.regex.Matcher m = p.matcher(str);
    while(m.find()){
      System.out.println(m.group());
    }

Резултатът от това изпълнение ще е „<b>HTML bold tag</b> and <i>italic tag</i>“ понеже търсим „максималната редица от какви да е символи заключени между ъглови скоби“. Ако желаем обратното – най-късата възможна редица – то слагаме въпросителна след звездичката:

    String str = "This represents <b>HTML bold tag</b> and <i>italic tag</i>";
    java.util.regex.Pattern p = null;
    p = java.util.regex.Pattern.compile("(<.*?>)");
    java.util.regex.Matcher m = p.matcher(str);
    while(m.find()){
      System.out.println(m.group());
    }

Имаме възможност да указваме и точно колко символа очакваме. Това става като се загради цяло число с „къдрави“ скоби – {n}. Ето как можем да валидираме телефонен номер от вида (+359)899488368:

    String str = "My phone number is (+359)899488368. Please call me.";
    java.util.regex.Pattern p = null;
    p = java.util.regex.Pattern.compile("(\\(\\+[0-9]{3}\\)[0-9]{9})");
    java.util.regex.Matcher m = p.matcher(str);
    while(m.find()){
      System.out.println(m.group());
    }

Ако в „къдравите скоби“ добавим втора цифра като параметър {m,n} това ще означава „между m и n символа“. Няма да даваме подробен пример за това. Същото се отнася и до конструкцията {m,} – означава „m или повече“.

В регулярните изрази е възможно и използване на оператор „или“. Това се получава чрез символ |. Ето един пример – ще търсим думата „cat“ или „fox“ в текста:

    String str = "The quick brown fox jumps over the lazy dog";
    java.util.regex.Pattern p = null;
    p = java.util.regex.Pattern.compile("(cat|fox)");
    java.util.regex.Matcher m = p.matcher(str);
    while(m.find()){
      System.out.println("Found "+m.group());
    }

Накрая ще споменем и още един често използван метод. Това е replaceAll(), който заменя подниз с друг. Ето едно примерно действие – програма, която променя български локален телефонен номер в международен формат:

    String phones = "Philip 0899488368\nPetar 0888555555";
    java.util.regex.Pattern p = null;
    p = java.util.regex.Pattern.compile("\\s08");
    java.util.regex.Matcher m = p.matcher(phones);
    String intPhones = m.replaceAll(" +(359)");
    System.out.println(intPhones);

Виждате, че маската е „празен интервал последван от цифрите 08 – точно както започват GSM номерата в България.

В заключение ще споменем, че regular expressions са изключително мощно средство за работа с низове, но изискват много внимание. Старайте се да правите вашите изрази колкото се може по-стриктни. Ако например очаквате да ви бъде подаден IP адрес, то шаблон

^[1-9][0-9]{0,2}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$

би ни се сторил добър, но той никак не е изчерпателен, защото 1.888.666.2 по този шаблон ще излезне валиден IP адрес, а то не е! Истинския шаблон за валидиране на IP адрес би бил много по-сложен:

^(0?0?[1-9]|0?[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(\\.(0?0?[0-9]|0?[0-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3}$

 



4 коментара


  1. Gas каза:

    Здравейте,
    Имам два въпроса:
    1. Какво означават \\ в („(\\(\\+[0-9]{3}\\)[0-9]{9})“) ?
    2. Трябва ли да се преобаразува съдържанието на един .txt файл в String, за да може той да бъде „претърсен“ с регулярните изрази и как би могло да стане това?

  2. Gas каза:

    За тези, които ще се опитат да доразработят някоя програма и специално търсенето на имейли ето какво открих аз:
    -условието за търсене на двата вида завършващи мейли трябва да изглежда така:
    …compile(„(\\w+[@][a-zA-Z0-9\\-]+[.][a-zA-Z]+[.][a-zA-Z]+)|(\\w+[@][a-zA-Z0-9\\-]+[.][a-zA-Z]+)“); и ще доведе до следния резултат:
    Found email N1: petyrr@gmail.com
    Found email N2: nikolay@gmail.co.uk

    Но ако местата на по-дългата и на по-късата маска се разменят резултатът ще бъде:
    Found email N1: petyrr@gmail.com
    Found email N2: nikolay@gmail.co

  3. На въпросите:

    1. Наклонената черта е „escape“ символ. Следователно две наклонени черти една след друга означава символа една наклонена черта.

    2. Едва ли има смисъл да се преобразува цял файл в „String“ и да се пази в паметта. Файловете могат да се претърсват директно – има си готови функции за това.

  4. Ивелина каза:

    Gas, регулярния израз, който си написал не е пълен.
    Я се пробвай да напишеш @ в началото на името, преди истинската @.
    Имам впредвид:
    deso@deso@abv.bg
    Мисълта ми е, не бива в началото да започваш с \w, а изрично да си напишеш примерно [a-z0-9_\.] или каквото там си прецениш.
    Поздрави
    Ивка

Добави коментар

Адресът на електронната поща няма да се публикува


*