Статьи Королевства Дельфи

Текст с высоты птичьего полета или Регулярные выражения


Раздел Подземелье Магов

"Look for a white shirt and a white apron," said the head which had
been put together, speaking in a rather faint voice. "I'm the cook."
L. Frank Baum, The Emerald City of Oz

При решении прикладных задач, полезно рассматривать их с высоты "птичьего полета". Многие знают что это может существенно ускорить разработку, но не многие этим пользуются.

Разница в посимвольной обработке строк и обработке с помощью регулярных выражений в том, что в первом случае Вы думаете прежде всего как достичь цели, а во втором - а какая цель Вам собственно нужна ? %-) Кроме того, посимвольные алгоритмы трудно модифицировать, не говоря уж о том, что любая модификация сопровождается перекомпиляцией приложения.

В этой небольшой статье собрано несколько иллюстраций использования регулярных выражений в Delphi.

Прим.

  • Если для Вас приведенные примеры выражений выглядят как древнеегипетские письмена, то ознакомьтесь с описанием их синтаксиса в любой книге о Perl или на . Они гораздо проще чем кажутся !
  • Для компиляции этих примеров достаточно добавить в список файлов проекта и вписать 'uses regexpr;' в юниты, где Вы используете регулярные выражения.
Детектор лжи

Предположим, Вам необходимо выманить ;) у пользователя адрес его электронной почты (моральную сторону и маркетинговую обоснованность подобной затеи мы здесь рассматривать не будем).

Идея в том, что если отвергать синтаксически некорректные адреса, то большинству пользователей надоест играть в эту орлянку и они либо откажутся от Вашей программы / уйдут с web-страницы, либо введут синтаксически корректный адрес. А как рядовому юзеру проще всего ввести такой адрес ?.. Правильно ! Проще всего ввести свой реальный e-mail !

Естественно, что вариант с p := Pos ('@', email); if (p > 1) and (p < length (email)) then ... проблемы не решает. Желательно как минимум просмотреть строку на предмет отсутствия некорретных символов а также наличия домена второго (или выше) уровня. Конечно, любой программист напишет такой анализатор... строк этак на *дцать и с перспективой перекомпилировать программу если что-то не впишется в эту проверку.


А теперь забудьте о посимвольной обработке и посмотрите на этот же анализатор, упрятанный в одну строку : if ExecRegExpr ('[\w\d\-\.]+@[\w\d\-]+(\.[\w\d\-]+)+', email) then ... gotcha! ... Регулярные выражения позволяют гибко реализовать достаточно изощренные проверки. Вот, скажем абсолютно корректная проверка на ... римские цифры любой величины (шаблон позаимствован из книги "Mastering Perl"): const Mask1 = '^(?i)M*(D?C{0,3}|C[DM])(L?X{0,3}|X[LC])(V?I{0,3}|I[VX])$'; ... if not ExecRegExpr (Mask1, DBEdit1.Text) then begin ... show error message ... DBEdit1.SetFocus; end;

Персонального www-робота - каждому !
В последнее время появилось неимоверное число программок, вылущивающих информацию из web-страниц. Так вот, на мой взгляд это гораздо разумнее делать с помощью регулярных выражений. Не изобретайте велосипед, используйте метро ! 8-)

Например вот таким нехитрым способом можно получить курс доллара и дату этого курса программно, не рассматривая рекламные баннеры (да простят меня CityCat и ФинМаркет ;) ).



Бросьте на форму TBitBtn, TLabel и TNMHTTP (TNMHTTP здесь использован исключительно для упрощения примера. Использовать эту гадость в реальной жизни не советую :-E~ ) и вставьте такой код обработки нажатия BitBtn1: procedure TForm1.BitBtn1Click(Sender: TObject); const Template = '(?i)Официальный курс ЦБ по доллару' + '.*Дата\s*Курс\s*Курс пок.\s*Курс прод. [^<\d]*' + '(\d?\d)/(\d?\d)/(\d\d)\s*[\d.]+\s*([\d.]+)'; begin NMHTTP1.Get ('http://win.www.citycat.ru/finance/finmarket/_CBR/'); with TRegExpr.Create do try Expression := Template; if Exec (NMHTTP1.Body) then begin Label1.Caption := Format ('Курс на %s.%s.%s: %s', [Match [2], Match [1], Match [3], Match [4]]); end; finally Free; end; end; В этом примере используется очень мощный механизм backtrack, отличающий NFA (non-deterministic finite state machine) реализацию регулярных выражений от DFA (deterministic finite state machine). В случае с NFA (на базе которого построен и TRegExpr) мы получаем возможность работать с подвыражениями, что и использовано в примере выше для выделения из шаблона элементов даты и собственно курса.



Кстати, здесь уже проявляются и ограничения регулярных выражений (см. ). Решая подбную задачу, я бы предварительно обработал текст: убрал бы незначимые тэги (ИМХО для надержного анализа достаточно оставить только табличные тэги), из оставшихся тэгов убрал бы все модификаторы (size, align и т.п.), убрал бы все переводы строк, а табуляции заменил на пробелы и убрал после этого повторяющиеся пробелы. После этого можно уже написать гораздо более надежное регулярное выражение.

А вот так можно достаточно надежно вынуть из неформализованного текста все Санкт-Петербургские номера телефонов (представленные как '(812)123-4567' или '+7 (812) 12-345-67' и т.д., причем извлечены будут внутригородские части номеров): procedure ExtractPhones (const AText : string; APhones : TStrings); begin with TRegExpr.Create do try Expression := '(\+\d *)?(\((\d+)\) *)?(\d+(-\d*)*)'; if Exec (AText) then REPEAT if Match [3] = '812' then APhones.Add (Match [4]) UNTIL not ExecNext; finally Free; end; end;

Господин Оформитель
Необходимо некий текст отобразить в html-странице, но предварительно желательно выделить гиперссылками все встречающиеся в нем URL.
Вот пример реализации (он не всегда сработает, но ведь 100% распознавание даже теоретически невозможно, да и в такого рода задачах не страшно если что-то не будет найдено. Страшно впустую тратить время на вспомогательные по сути вещи): type TDecorateURLsFlags = ( // Включаемые в видимую часть гипер-ссылки поля durlProto, // Протокол ('ftp://' или 'http://') durlAddr, // IP-адрес или символическое имя домена durlPort, // номер порта (например ':8080') durlPath, // путь (unix-формат) durlBMark, // объект внутри страницы (напрмер '#bookmark') durlParam // параметры запроса (например '?ID=13&User=Pupkin') ); TDecorateURLsFlagSet = set of TDecorateURLsFlags; function DecorateURLs (const AText : string; AFlags : TDecorateURLsFlagSet = [durlAddr, durlPath]) : string; const URLTemplate = '(?i)' // регистро-независимый режим + '(' + '(FTP|HTTP)://' // Протокол + '|www\.)' // Позволяет отловить ссылки указанные без 'http://' + '([\w\d\-]+(\.[\w\d\-]+)+)' // IP-адрес или символическое имя домена + '(:\d\d?\d?\d?\d?)?' // номер порта + '(((/[%+\w\d\-\\\.]*)+)*)' // путь (unix-формат) + '(\?[^\s=&]+=[^\s=&]+(&[^\s=&]+=[^\s=&]+)*)?' // параметры запроса + '(#[\w\d\-%+]+)?'; // объект внутри страницы var PrevPos : integer; s, Proto, Addr, HRef : string; begin Result := ''; PrevPos := 1; with TRegExpr.Create do try Expression := URLTemplate; if Exec (AText) then REPEAT s := ''; if CompareText (Match [1], 'www.') = 0 then begin Proto := 'http://'; Addr := Match [1] + Match [3]; HRef := Proto + Match [0]; end else begin Proto := Match [1]; Addr := Match [3]; HRef := Match [0]; end; if durlProto in AFlags then s := s + Proto; // Match [1] + '://'; if durlAddr in AFlags then s := s + Addr; // Match [2]; if durlPort in AFlags then s := s + Match [5]; if durlPath in AFlags then s := s + Match [6]; if durlParam in AFlags then s := s + Match [9]; if durlBMark in AFlags then s := s + Match [11]; Result := Result + System.Copy (AText, PrevPos, MatchPos [0] - PrevPos) + '<a href="' + HRef + '">' + s + '</a>'; PrevPos := MatchPos [0] + MatchLen [0]; UNTIL not ExecNext; Result := Result + System.Copy (AText, PrevPos, MaxInt); // Tail finally Free; end; end; { of function DecorateURLs -------------------------------} Обратите внимание, что в приведенном выше примере Вы имеете возможность легко выделять из URL протокол, домен, путь и параметры запроса (см. параметр AFlags).



Панацея ?
Возможно, в этом месте уже не лишним будет умерить пыл энтузиастов, в особенности тех, кому случалось использовать Перл.

Дело в том, что Перл - интерпретирующий язык. Основное следствие из этого - чем меньше операторов выполняется, тем быстрее (как правило) работает программа. В большистве случаев регулярное выражение отработает быстрее чем самый элементарный посимвольный анализ строки.

Поэтому, не кажется диким реализация функции Trim как выражения '^\s*(\S*)\s*$'.

Думаю, не надо объяснять насколько это глупо в истинно компилируемом Паскале. Так что, если анализируемая строка имеет простую структуру - напишите элементарный и очень быстрый цикл по ее разбору и не связывайтесь с регулярными выражениями.

Кроме того, не рекомендую использовать регулярные выражения там, где нужен полноценный парсер. Если, например, Вам нужно разобрать на теги HTML - поищите для этого более подходящий инструмент !

Если же искомая или проверяемая строка имеет сложную структуру, если эта структура может меняться, тогда это наш клиент ;) Если же описание должно меняться без перекомпиляции программы, то серьезной альтернативы регулярным выражениям практически нет.

Успехов !

Да, чуть не забыл, библиотека которая устраняет досадную забывчивость разработчиков Delphi и позволяет использовать в Delphi регулярные выражения без необходимости таскать за собой какие-либо DLL, лежит на

или
.

Андрей Сорокин
Специально для




Содержание раздела