Кратко съдържание



страница73/73
Дата21.07.2018
Размер8.57 Mb.
1   ...   65   66   67   68   69   70   71   72   73

Групи в регулярните изрази


На няколко места дотук в темата споменавахме за групиране и групи в регулярните изрази. Всъщност групите са една широко използвана конс­трукция, която позволява логическо обособяване на части от шаблона, както и извличане на различни видове информация именно като отделни части от общия регулярен изрази.

Създаването на група в шаблона вече описахме няколкократно. Доста­тъчно е просто да оградим част от израза в кръгли скоби – "Това е израз с (група)".


Предимства на групите


Предим­ствата на групите са няколко. Те позволяват някои метасимволи (за количество, за избор и др.) да се прилагат върху цяла логическа част от шаблона (вече използвахме това в някои от примерите).

Друга много важна функция на групите е, че те ни дават възможност да извличаме точно тези части от съвпадението, които ни интересуват. Под­низовете, които съвпадат с частите от шаблона в групите, се запазват паралелно с цялото съвпадение. Така можем с едно съвпадение на по-голям шаблон едновременно да извлечем различни части текстова инфор­мация и с помощта на групирането да ги обособим и да ги използваме поотделно.

Може би най-голямото предимство на групите, що се отнася до процеса на търсене, е възможността да се реализират т. нар. обратни препратки (backreferences). С тяхна помощ можем в хода на изпълнението да полу­чим стойността на съвпадението с дадена група и да я използваме по някакъв начин на друго място по-нататък в шаблона или за заместване, което ни осигурява голяма гъвкавост. Можем например да търсим отварящ HTML таг от вида <таг> и после да искаме да намерим съответния затва­рящ – . Ако тук обособим "таг" в група, ще можем да търсим затва­рящия таг именно като използваме обратна препратка към тази група.

Ще разгледаме още някои подробности по синтаксиса на групирането, както и някои примери, които ще демонстрират нагледно изложеното по-горе, след което ще обясним как групите могат да се използват при програмирането с регулярни изрази на .NET.


Неименувани (анонимни) групи


Както вече споменахме, групирането става по най-естествения начин – чрез символите ( и ). Всяка част от шаблона, оградена с кръгли скоби, автоматично се третира от машината на регулярните изрази като група. Когато тази група съвпадне с подниз в текста, машината продължава да търси съвпадения със следващия символ, но запазва в паметта стойността на съвпадението с групата. Тези запазени стойности се индексират с номера, които се запълват в реда на срещането на групите в шаблона.

Анонимни групи – пример


Да видим един прост пример:

Шаблон: ^(\w+)=(\w+);$

Нека имаме един списък от опции и техните стойности, записани като редове във вида "option=value;". Един кратък подобен списък може да изглежда например така:

filtering=anisotropic;

details=hi;

resolution=1024;

enable_shades=1;

Ако искаме да извлечем двойките опции и стойности, можем да търсим в многоредов режим с помощта на горния израз. На всеки ред първото \w+ ще открие като съвпадение точно текста на option, а второто – текста на value. При това, понеже сме поставили тези части от шаблона в скоби, съвпаденията с тях ще бъдат запазени в паметта – първата под номер 1, а втората – под номер 2. Под номер 0 се запазва съвпадението с целия шаблон. На края на реда машината ще спре да търси, защото сме намерили съвпадение. В този момент трябва да извлечем съвпаденията, които сме получили в групите, защото когато поискаме ново търсене, стойностите им ще се презапишат от новите съвпадения.

Това е важно да се запомни – стойността, прихваната от групата, е само тази от последното съвпадение и тя презаписва всички предишни полу­чени стойности. Следният пример демонстрира това:



Текст: бира

Шаблон: ([рбиа])+

Запазена в групата стойност: а
Шаблон: ([рбиа]+)

Запазена в групата стойност: бира



Тук и в двата случая + е "лаком" и съвпадението с шаблона е целият низ. В първия случай това означава четири пъти да се намери съвпадение с частта в групата, защото плюсът се отнася към нея. От тях само послед­ната стойност ("а") остава запазена. Във втория случай групата огражда шаблона и запазва цялото съвпадение.

Именувани групи


Синтаксисът на регулярните изрази в .NET ни дава възможност да слагаме и имена на групите, които дефинираме. Това придава по-добър вид на регулярния израз и обособява по-добре използваните групи като логи­чески цялости, защото става ясно каква е целта им.

Именуваните групи се дефинират подобно на неименуваните, но след отварящата скоба трябва да напишем и името на групата по следния начин: (?regular_expression) или (?'name'regular_expression). Между двата варианта няма разлика – може да се използват като взаимо­заменяеми. Първият е по-удобен за низове, защото там трябва да се escape-ват кавичките, докато вторият може да е от полза при ASP тагове, където счупените скоби имат специално значение. Да обърнем внимание, че тук не може да стане объркване с метасимвола ? за 0 или 1 повторе­ния, защото той трябва да стои след валиден за регулярния израз символ или конструкция, а отварящата скоба не е такава.

За яснота, да променим горния шаблон като добавим имена на групите:

Шаблон: ^(?<option>\w+)=(?<value>\w+);$

По този начин сме дефинирали групите option и value и можем да дос­тъпваме запазените от тях съвпадения по име, което опростява логиката на програмата и четимостта на кода.

Номериране на групите


Именуваните групи също се номерират заедно с неименуваните, въпреки че имат и име. При това машината на регуляр­ните изрази следва следното правило: подред се номерират първо неименуваните групи по реда на срещането им в израза отляво надясно, а след това и именуваните – пак отляво надясно. Това поведение е характерно само за .NET и не важи за други платформи и езици, които позволяват работа с регулярни изрази (при тях всички групи се номерират в реда на срещане).

Например в израза "(\w+)_(?\d+)_(\s+)" групата (\w+) ще получи номер 1, групата (\s+) – номер 2, а (\d+) ще има номер 3 и ще може също да се достъпва с името group1. Това понякога е объркващо и трябва да се внимава. По възможност е добре да се използват или само именувани, или само неименувани групи. Разбира се, ако има групи, които използваме само за да прилагаме метасимволи към част от шаблона наведнъж, няма смисъл да им слагаме имена, защото това затруднява четенето на кода. Там можем да използваме специалната конструкция за група, която не запазва съвпадение. За нея ще стане дума по-късно в темата.


Търсене с групи в .NET


Време е да видим как можем лесно да се възползваме от възможностите на групирането в .NET и да извличаме информацията, запазена в групите.

Класовете Group и GroupCollection


В пространството от имена System.Text.RegularExpressions групите се представят с класа Group. Този клас е доста подобен по функционалност на класа Match – и двете представят низ, който при търсенето се е оказал съвпадение с шаблона, само че Match пази съвпадението с целия регуля­рен израз, а Group пази съвпадението с някоя част от шаблона, която е дефинирана като група.

Подобно на Match, класът Group предлага свойства, които описват мястото на съвпадението, стойността му и дължината му – Index, Value и Length. Свойството Success ни показва дали съответната група е намерила съвпадение.

Класът GroupCollection е просто колекция от обекти на класа Group. Както ще видим след малко, обикновено този клас използваме за достъп до колекция от всички групи, дефинирани в даден регулярен израз. Свойствата и методите на GroupCollection са подобни на тези на MatchCollection и на повечето колекции в .NET.

Как извличаме информацията от групите?


Както вече знаем, след всяко търсене с регулярен израз, получаваме обект от класа Match, който описва съвпадението. За да проверим запазе­ните в групите съвпадения, използваме свойството Groups на класа Match. Това свойство има стойност от тип GroupCollection и ни предоставя точно тези съвпадения. Следният пример показва как да извличаме информаци­ята от групите:

static void Main()

{

Regex regex = new Regex(@"^(\w+)=(\w+);$");



string text = "filtering=anisotropic;";

Match match = regex.Match(text);

while (match.Success)

{

Console.Write("\n\n");



Console.WriteLine(

"Съвпадение: \"{0}\" - начало {1}, дължина {2}",

match, match.Index, match.Length);

Console.WriteLine("Брой групи: " + match.Groups.Count);

for (int i=0;i

{

Console.WriteLine(



"Група номер {0}, име \"{1}\"",

i, regex.GroupNameFromNumber(i));

Console.WriteLine(

"\tСтойност: \"{0}\", започва на {1}",

match.Groups[i].Value, match.Groups[i].Index);

}

match = match.NextMatch();



}

}
/* Output:

Съвпадение: "filtering=anisotropic;" - начало 0, дължина 22

Брой групи: 3

Група номер 0, име "0"

Стойност: "filtering=anisotropic;", започва на 0

Група номер 1, име "настройка"

Стойност: "filtering", започва на 0

Група номер 2, име "стойност"

Стойност: "anisotropic", започва на 10

*/


От примера се вижда това, за което говорихме по-рано – че под група с номер 0 се пази цялото съвпадение. Всъщност класът Match наследява Group и реално match.Groups[0] е самият обект match.

Забележете също употребата на метода GroupNameFromNumber(int) на класа Regex. Този метод не е статичен и може да се използва само за обект–шаблон. Връща име на група по даден номер (спомнете си как се номерират групите в израза!). Аналогичният метод GroupNumberFromName( string) пък ни дава номера на група с дадено име в израза, стига да има такава (и -1 в противен случай).


Именуване и номериране


Именуваните групи можем да достъпваме и чрез името им, например match.Groups["option"] или match.Groups["value"]. Самите имена на групите можем да получим наведнъж с метода GetGroupNames(). Номера­та, на които те отговарят съответно, се връщат пък от GetGroupNumbers(). И двата метода нямат статични варианти и връщат масиви. С тяхна помощ можем да си припомним начина на номериране на групите – първо ано­нимните, после именуваните:

static void Main()

{

Regex regex = new Regex(



@"Пример ((\w+)\s(?[руг]+)(пи))*");

string text = "Пример с групи";

Match match=regex.Match(text);

Console.Write("\nИмена на групи: ");

foreach (string name in regex.GetGroupNames())

{

Console.Write("{0} <::> ", name);



}

Console.Write("\n");

Console.WriteLine("Номера на групи:");

foreach (int number in regex.GetGroupNumbers())

{

Console.WriteLine("{0} - {1} - {2}", number,



regex.GroupNameFromNumber(number),

match.Groups[number].Value);

}

}

/* Output:



Имена на групи: 0 <::> 1 <::> 2 <::> 3 <::> named <::>

Номера на групи:

0 - 0 - Пример с групи

1 - 1 - с групи

2 - 2 - с

3 - 3 - пи

4 - named - гру

*/


Тук именуваната група е дефинирана преди групата (пи), но понеже (пи) е анонимна, тя получава по-малък номер. Анонимните в примера се нареждат по реда на отварящите скоби.

Парсване на лог – пример


Ще разгледаме два по-практически примера за демонстрация на извлича­нето на различни части информация от текста с помощта на групи. Пър­вият пример е за парсване на потребителски лог, в който имаме данни в следния формат:

<име на потребител> <IP адрес> <време в системата>

Искаме да извлечем и изведем тази информация в малко по-удобен вид. За целта ще използваме шаблон с групи. Ето и целият код:

static void Main()

{

string text = "gosho 62.44.18.124 02:44:50\n" +



"root 193.168.22.18 22:12:38\n" +

"nakov 217.9.231.126 00:07:24";

string pattern = @"(?\S+)\s+" +

@"(?([0-9]{1,3}\.){3}[0-9]{1,3})\s+" +

@"(?

MatchCollection matches = Regex.Matches(text, pattern);

foreach (Match match in matches)

{

Console.WriteLine("name={0,-8} ip={1,-16} time={2}",



match.Groups["name"], match.Groups["ip"],

match.Groups["time"]);

}

}


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


Извличане на хипервръзки в HTML документ – пример


С помощта на групите можем да разширим нашия пример за таговете за хипервръзки, така че да не извличаме целия таг, а само текста му и адре­са, към който връзката сочи. За целта изпълняваме следните стъпки.

  1. Отваряме VS.NET и създаваме нов конзолен проект.

  2. Добавяме връзка към System.Text.RegularExpressions в клаузите за пространствата от имена и въвеждаме следния код в главната функция:

    static void Main()

    {

    string text = @" This is a hyperlink:



    close the window

    ... and one more link:


    href=/main.aspx > main page

    < a href = 'http://www.nakov.com'>Nakov's

    home site < /a >";


    string hrefPattern = @"<\s*a\s[^>]*?\bhref\s*=\s*" +

    @"('(?[^']*)'|""(?[^""]*)""|" +

    @"(?\S*))[^>]*>" +

    @"(?(.|\s)*?)<\s*/a\s*>";


    Match match = Regex.Match(text, hrefPattern);

    int i=1;


    while (match.Success)

    {

    Console.WriteLine("Връзка {0}, започва от позиция {1}",



    i, match.Index);

    string linktext = match.Groups["linktext"].Value;

    Console.WriteLine("текст={0}", linktext);

    string url = match.Groups["url"].Value;

    Console.WriteLine("адрес={0}", url);

    Console.WriteLine();

    i++;

    match = match.NextMatch();



    }

    }


  3. Стартираме програмата и получаваме следния резултат:


Работа с обратни препратки


Стигаме и до по-интересните възможности, които групирането предлага – използването на обратни препратки в регулярния израз. Чрез тях можем да използваме запазеното в групите като част от остатъка от шаблона. Както вече споменахме, това е особено полезно, ако искаме някаква част от текста да се повтаря, но не знаем точно каква е тя. В текста на шаблона обратните препратки се обозначават с конструк­ции от вида \X, където X е номерът на неименуваната група. В примера с настройките и стойностите стойността на option можем да използваме в регулярния израз, като напишем \1:

Шаблон: ^(\w+)=(\1\w+);$

Текст:


filtering=anisotropic;

details=hi;

background=background-image;

resolution=1024;

Съвпадение: background=background_image;

Група 0: background=background_image; (цялото съвпадение)

Група 1: background

Група 2: background_image


За разлика от стария пример, тук поставяме условие втората група да търси съвпадение, започващо с подниза, който е вече запазен в първата. Затова съвпадение има чак на третия ред, където началото на value в двойката е именно съдържащото се в частта option.

Разбира се обратна препратка не можем да използваме в групата, която я дефинира, т.е. не може да имаме например "\d(\w+\1)$" като регулярен израз. Това предизвиква неуспешно съвпадение във всеки текст. В част­ност, метасимволът \0 не може да се използва никъде в израза като обратна препратка.

Не можем също да използваме нито скобите за групиране, нито обратни препратки вътре в клас от символи с квадратни скоби. Там те губят специалното си значение и стават литерали, като конструкцията \X може да означава осмичен ASCII код, както вече видяхме в частта за escaping.

Обратни препратки към именувани групи


В текста на регулярния израз обратна препратка към именувана група става чрез конструкцията \k или \k'name':

Шаблон: ^(?\w+)=(?\k\w+);$

е еквивалентно на: ^(?<option>\w+)=(?<value>\1\w+);$



Както си спомняме, групата option е и група номер 1, ето защо двата записа са еквивалентни. Възможно е да използваме и само \<option> вместо \k<option>. Със следния пример ще търсим в декларации на пот­ребителски имена и съответни пароли и ще извлечем всички редове, при които името и паролата съвпадат:

string text =

"gosho &boza!!36\n" +

"pesho pesho\n" +

"ivo kaka*mara\n" +

"kaka #k@k@22\n" +

"test test";

string pattern = @"^(?\S+)\s+(\)$";

MatchCollection matches =

Regex.Matches(text, pattern, RegexOptions.Multiline);

foreach (Match match in matches)

{

Console.Write("{0} ", match.Groups["user"]);



}

// Output: pesho test


Извличане на HTML тагове от документ – пример


С помощта на обратните препратки вече можем да усъвършенстваме по-сериозно нашия пример с таговете за хипервръзки. Този път ще извли­чаме информация за всички HTML тагове, които срещнем в документа. Това са поднизове от вида: attrN>text. За целта ще използваме следния израз:

<\s*(?[A-Za-z]\w*)(?[^>]*)>(?.*?)

Да разгледаме този шаблон внимателно, за да разберем защо той изпъл­нява поставената задача:

Шаблон: <\s*

Коментар: Започваме със символа "<" и преминава през празното прост­ранство след него (ако има).


Шаблон: (?[A-Za-z]\w*)

Коментар: Тук очакваме да намерим валиден HTML таг. Нито един такъв таг не започва с цифри, ето защо задължаваме първият символ да е буква, след което следват един или повече word characters. Така наме­ре­ното име на таг запазваме в групата tag.


Шаблон: (?[^>]*)>

Коментар: Тъй като * от предишната част е "лаком", то тук започваме от границата на нова дума. Отбелязали сме произволен брой символи, различни от >, които ни дават подниза с атрибутите на тага – запазваме ги в група attributes. Следва и затварящата скоба >.


Шаблон: (?.*?)

Коментар: Между отварящия и затварящия таг има произволен текст, който ще пазим в групата text. Правим звездичката "мързелива", за да не улови целия низ до края.


Шаблон:

Коментар: Затварящият таг е същият като отварящия, само че преди името му има символа /. Ние запазихме името в групата tag, която е също с номер 1. Следователно \1 ще накара машината на регулярните изрази да търси за точно това име и ще сме сигурни, че сме намерили правилния затварящ таг. Тук можем да използваме и конструкцията , за да улавяме и тагове с празни места вътре.



Нека сега съставим нов проект и въведем кода, който обработва този регулярен израз:

using System;

using System.Text.RegularExpressions;


class TagMatch

{

static void Main()



{

string text = "" +

"
Title
" +

"
Text and" +



"link
" +

"
1   ...   65   66   67   68   69   70   71   72   73


База данных защищена авторским правом ©obuch.info 2016
отнасят до администрацията

    Начална страница