Версия для печати


Текст с высоты птичьего полета или Регулярные выражения
http://www.delphikingdom.com/asp/viewitem.asp?catalogID=163

Андрей Сорокин
дата публикации 20-03-2000 00:00

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

"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 или на http://regexpstudio.com/RU/TRegExpr/Help/RegExp_Syntax.html.
    Они гораздо проще чем кажутся !
  • Для компиляции этих примеров достаточно добавить regexpr.pas в список файлов проекта и вписать '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, лежит здесь:
http://regexpstudio.com/RU/.

Андрей Сорокин
Специально для Королевства «Delphi»