Персональный блог Игоря Антонова aka "spider_net"

Регулярные выражения в Delphi


Рубрика: Delphi -> Программирование -> Журнал Хакер -> Статьи
Метки: | |
Просмотров: 13904
Регулярные выражения в Delphi

Регулярные выражения – один из главных «инструментов» заядлых линуксоидов и WEB-программистов. Проверить введенные пользователем данные, быстренько и непринужденно пропарсить какую-нибудь html страницу, найти заковыристый фрагмент в большом куске текста – задачи, решаемые за несколько минут с помощью регулярных выражений. Многие программисты считают, что использовать их дано лишь гуру. Мы так не думаем, поэтому расскажем и покажем все самое необходимое, чтобы при виде вот таких наборов символов «[wd-.]+@([wd-]+(.[w-]+)+)» ты не смущался и не испытывал чувство дискомфорта в нижней части живота.

Немного истории

История регулярных выражений начинается в далеких 40-х годах. Двое нейрофизиологов, Уоррен Мак-Каллох и Уолтер Питтс, трудились в то время над моделированием работы нервной системы на нейронном уровне. Спустя несколько лет, математик Стивен Клин сумел описать эти модели с помощью алгебры и дал им имя – регулярные множества. Вот так, постепенно, регулярные выражения стали набирать популярность среди кодеров. Такие знаменитые люди как, например, Кен Томпсон написали множество статей на тему использования регулярных выражений для выполнения самых разнообразных задач. Итак, регулярные выражения – технология поиска текстовых фрагментов в электронных документах, соответствующих определенным правилам.

Основы основ

Перед тем, как начать использовать регулярные выражения стоит разобраться с некоторыми понятиями. Начнем с литералов. Литерал – это любой отдельный символ. Например: а, b, с, d – литералы. Думаю, с этим все ясно. Едем дальше. Из одних литералов кашу не сваришь, поэтому на помощь приходят метасимволы (специальные символы, которые выполняют какое-либо дополнительное действие). Наверняка тебе не раз приходилось использовать метасимволы при работе в командной строке (неважно, Windows ли эта консоль или Unix).

Например, чтобы вывести в Windows список файлов определенного каталога, в cmd можно воспользоваться командой dir. Как быть, если мне нужно отобразить все имена файлов, у которых расширение html и htm? Можно выполнить команду dir и сломать глаза, выискивая нужные файлы, а можно воспользоваться конструкцией dir *.htm? В этой записи присутствует два метасимвола – «*» и «?» Звездочкой мы указываем, на то, что имя файла может состоять из любых символов (литералов), затем мы определяем расширение и ставим знак вопроса, который указывает, что после m может ничего не быть или быть один любой символ. Для нашей задачи этого вполне достаточно.

Попробуем написать пробный пример. В поле для регулярного выражения мы напишем одно лишь слово «Пример», а в качестве пробного текста: «Это простой Пример использования регулярных выражений». Жмем на Exec и видим, как во входном тексте выделилось слово «Пример». После нажатия на пимпу «ExecNext» программой будет произведена попытка поиска следующего фрагмента текста, соответствующего шаблону. В нашем случае регулярное выражение больше не сработает, поскольку слова «Пример» больше нигде нет.

Шаблон получился очень простым. Если бы регулярные выражения предназначались только для этого, от них не было бы толку. Попробуем усложнить пример. В качестве входной строки определим: «Первый мой номер: +7-924-111-11-34, а второй +7-231-331-55-55». Наша с тобой задача будет найти в этом тексте все номера телефонов. Первое, что приходит на ум – это описать телефоны в качестве шаблона. К сожалению, этот способ не подойдет. Его нельзя назвать универсальным. Стоит изменить номер телефона и шаблон станет бесполезным. Введем в качестве шаблона: «+[0-9-]+». Попробуем запустить поиск. Работает? А почему? :). Для понимания вопроса мы для начала разобьем шаблон на части:

  • + - явно указываем, что первым символом должен быть плюс. Поскольку символ «+» относится к метасимволам, то просто взять и поставить его мы не можем. Нам придется экранировать его с помощью слеша (слеш перед метасимволом превращает его в обычный литерал).
  • [0-9-] – в квадратных скобках принято описывать символьные классы (интервалы литералов), которые называются квантификаторами. Чтобы описать какой-нибудь интервал, нужно просто указать начальный и конечный символ. Например, 0-9 – соответствует всем цифрам от 0 до 9. Таким же способом можно задавать и буквенные интервалы: a-z (все латинские буквы в нижнем регистре), a-zA-Z (латинские буквы как в нижнем, так и в верхнем регистрах). Мы собираемся искать номера телефонов, а они могут состоять из цифр и знака «-», который их разделяет. Больше быть ничего не должно.
  • «+» в данном случае, знак плюса, выполняет роль метасимвола. Теперь мне бы хотелось обратить твое внимание на описание символьных классов. В квадратных скобках все перечисленные символы действуют совершенно по-другому. Например, если поставить перед символами метасимвол ^, то это будет означать уже не начало строки, а отрицание. Например: [^abc] – все символы, кроме abc.
  • Остальные метасимволы, указанные в символьном классе теряют свою силу и становятся обычными литералами.

    Рассмотрим такую задачку. Имеется две строки, в которой содержатся буквы и цифры. Задача состоит в том, что необходимо выбрать часть строки, в которой сначала идут подряд 3 латинские буквы, а за ними цифры в интервале от 3 до 6. Строки следующие:

    abdajD345bhad5124jjdaabc3456
    abdj23456kfej3456fe

    Попробуй решить данную задачку самостоятельно, на основе полученных знаний. Мое решение будет выглядеть так: «[a-z]{3}[3-6]{4}». Наиболее удобным способом решения данной задачи является установка ограничений на количество находимых символов. Итак, нам нужно найти 3 латинских буквы. Можно, конечно, три раза записать класс [a-z], а можно просто в фигурных скобках указать их количество. Это я и сделал. Следующим условием является цифры от 3-6. Указываем их в классе, не забыв про количество. Вот и все. В результате поиска по данному шаблону в первой строке будет выбраны «abc3456», а во второй «fej3456».

    Регулярные выражения в Delphi

    В Delphi нет модуля/компонента для работы с регулярными выражениями. На мой взгляд, это существенное упущение Borland (теперь - Code Gear Embarcadero). Так считаю не только я, но и тот перец, который закодил класс, благодаря которому перед нами открываются безграничные возможности использования регепсов. Итак, перед тем как переходить к рассмотрению практических примеров, потрудись скачать с сайта разработчиков (http://www.regexpstudio.com) архив с модулем и примерами. Можешь не бегать так далеко, а просто взять наш диск и все слить с него. Подключив к своему проекту RegExpr (именно так он называется), тебе становится доступным для создания новый объект типа TRegExpr, который обладает следующими свойствами и методами .

    Собираем свой антиспам-лист

    В качестве первого практического примера я решил сделать что-нибудь крайне полезное. Первое, что мне пришло в голову, написать тулзу для выдирания е-mail адресов с html страниц, чтобы точно знать, на какие мыльники ты никогда в жизни не соберешься послать нежелательную корреспонденцию. Пожалуй, наша программа будет уметь не просто грабить мыльники, но и собирать по нашему приказу и линки (чтобы их впоследствии не посещать). В качестве входных параметров софтина будет получать путь к папке, в которой хранятся html и htm файлы. Итак, включаемся в процесс.

    Как обычно, создаем в Delphi новый проект и сразу же подключаем недавно скачанный нами модуль. После этого нарисуем простенькую форму, внешний вид которой ты можешь наблюдать на соответствующей иллюстрации.

    Принцип действия примера будет следующим. По нажатию кнопки перед пользователем должен появляться диалог выбора директории. После выбора каталога, управление передается самописной процедуре FindFiles() . Ее код приведен в соответствующей врезке.

    Код процедуры FindFiles()

    var
    _se:TSearchRec;
    begin
    //если в директории для поиска отсутствует слеш, то нужно его добавить
    if dir[length(dir)]<>'' then
     dir:=dir+'';
    //начинаем поиск
    if FindFirst(dir+'*.htm?', faAnyFile, _se)=0 then
     repeat
      findMailsUrls(dir+_se.Name, mode);
     until FindNext(_se)<>0;
    //если нашли поддиректорию, то начинаем поиск в ней.
    if FindFirst(dir+'*.*', faDirectory, _se)=0 then
    begin
    repeat
     if ((_se.Attr and faDirectory)=faDirectory) and (_se.Name[1]<>'.') then
      FindFiles(dir+_se.Name+'', mode);
    until FindNext(_se)<>0;
    FindClose(_se);
    end;

    В этой процедуре реализован алгоритм рекурсивного поиска файлов по маске. Для поиска используется функция FindFirst() , в качестве параметров которой нужно передать:

  • Директорию, в которой нужно искать файлы, соответствующие маске.
  • Атрибуты искомых файлов (системный, архивный, только для чтения, любой).
  • Структуру типа TSearchRec , в которую попадут результаты поиска. Для прохода по всем вложенным папкам используется рекурсия (вызов процедурой самой себя). Если вместо директории нашелся нужный файл, то значит, можно смело передавать работу процедуре findMailsUtils() , которая в зависимости от последнего параметра, будет искать либо мыльники, либо url’ы. Код процедуры findMailUtils приведен во врезке, из названия которой ты никогда не догадаешься о ее содержимом :).
  • Код процедуры findMailUtils

    var
    _tempFile:TStringList;
    _regexp:TRegExpr;
    i, b:integer;
    begin
    //Инициализируем объекта для работы с регулярными выражениями
    _regexp:=TRegExpr.Create;
    //Устанавливаем шаблон поиска в зависимости от условия.
    case mode of
    //Будем искать мыльник
    0: _regexp.Expression:='[wd-.]+@([wd-]+(.[w-]+)+)';
    //Будем искать url адрес
    1: _regexp.Expression:='(http|ftp)://([wd-]+(.[wd-]+)+)(([wd-=?\./]+)+)*';
    End;
    _tempFile:=TStringList.Create;
    _tempFile.LoadFromFile(file_name);
    ProgressBar1.Max:=_tempFile.Count;
    b:=StrToInt(CountMailLAbel.Caption);
    for i:=0 to _tempFile.Count-1 do
    begin
     progressBar1.Position:=i;
     if (_regexp.Exec(_tempFile.Strings[i])) then
     repeat
      ResultMemo1.Lines.Add(_regexp.Match[0]);
      Inc(b);
      CountMailLabel.Caption:=IntToStr(b);
     until not _regexp.ExecNext;
    end;
    //Освобождаем память
    _regexp.Free;
    _tempFile.Free;

    Взглянем на вторую врезку. Перед использованием объекта для работы с регулярными выражениями, его нужно инициализировать, после которой можно приступать и к составлению регулярного выражения. Поскольку мы собираемся закодить более-менее универсальную тулзу, придется проверить передаваемый в процедуру параметр mode. Значение 0 будет свидетельствовать о том, что нам требуется распотрошить файлы на предмет мыльников, а 1 будет означать, что нужны только url-адреса. Для отлова e-mail адресов я устанавливаю вот такой шаблон [wd-.]+@([wd-]+(.[w-]+)+). На первый взгляд он абсолютно непонятен. Давай разбираться:

  • [wd-.]+ - эта часть описывает адрес электронной почты до знака собачка. В соответствии со стандартом, здесь могут быть любые буквы (w), цифры от 0-9 (d), знак «-» и точка. После описания символьного класса нужно поставить метасимвол «+», иначе под данную часть шаблона у тебя будут попадать одиночные символы.
  • @ - понятно, что e-mail адрес не может быть без значка собачки, поэтому нам необходимо его описать. 3. ([wd-]+(.[w-]+)+) – доменная часть e-mail адреса описывается в этом небольшом кусочке. Все используемые здесь метасимволы должны быть тебе уже известны, поэтому я не буду повторяться. Просто внимательней посмотри на эту часть и все станет на свои места. Единственное, про что я тебе не рассказывал, так это про скобки. В регулярных выражениях они выполняют двойную роль - описывают группы литералов и сохраняют эту группу в специально предопределенных переменных. В данном выражении я буду сохранять отдельно имя домена и доменную зону.
  • Вот и все, одной строчкой мы описали шаблон для поиска мыльника, не правда ли классно? Думаю, что да, но не стоит забывать, ведь если mode = 1, то значит, нужно искать url’ы. Шаблон для определения ссылки выглядит более громоздко: (http|ftp)://([wd-]+(.[wd-]+)+)(([wd-=?\./]+)+)*. Опять же, попробуем разобраться с его внутренностями.

  • (http|ftp):// - опишем возможные протоколы. Любой адрес для обращения к узлу с помощью протокола http или ftp должен начинаться с http:// или ftp:// соответственно. В скобках я указываю сначала приставку http, затем вертикальную черту, которая соответствует логическому ИЛИ и уже после нее вторую возможную приставку – ftp. В итоге наш шаблон будет срабатывать как на ссылки ftp ресурсов, так и http.
  • ([wd-]+(.[wd-]+)+) – вот таким образом можно описать адрес узла. Данная конструкция будет одинаково хорошо срабатывать и на адреса вида http://192.168.0.1 – т.е. IP адрес, как и на символьные адреса. Скобками группируем условия, т.к. если просто написать диапазон литералов в одном классе и поставить метасимвол +, то выражение не будет правильно работать. Ради эксперимента советую поупражняться и попробовать составить другой шаблон.
  • (([wd-=?\./]+)+)* - поскольку линк может вести на какой-нибудь файл, то мы обязаны это предусмотреть. В пути могут присутствовать различные символы: «/», «?», «=» и т.д. Поскольку часть из них являются метасимволами, то мы должны их экранировать – поставив перед ними еще один слеш. Установив шаблон можно открывать наш файл, запускать перебор по строкам, получая результат поиска. Для большей информативности, я показываю количество найденных совпадений в одном из label.
  • Еще один полезный примерчик

    Представим себе ситуацию. Ты нашел себе базу номеров своего оператора. Только вот незадача – она вся хранится в обычном текстовом файле. Было бы здорово перегнать все эти данные в какую-нибудь БД и потом сортировать, производя поиск удобными средствами. Для перегонки (блин, крутое слово, напоминает мне о деревенском самогоне) можно написать и отладить свой алгоритм, но лучше и проще воспользоваться регулярными выражениями. Итак, новая задачка. У нас имеется текстовый файл с записями вида: «Василий Петрович 12.02.1975 +7-912-455-24-14». Наша цель - разделить информацию в строке и записать в колонки: Имя, Фамилия, Дата рождения, номер телефона. Для решения поставленной задачи я создал в своем проекте еще одну закладку и придал ей следующий вид (см. картинку).

    По нажатию кнопки, предназначенной для открытия файла, накатай код из правильной врезки (а правильная врезка называется «перегонный куб») и возвращайся к тексту статьи за объяснениями.

    Перегонный куб

    var
    _regexp:TRegExpr;
    _tempFile:TStringList;
    I:Integer;
    begin
    if not (OpenDialog1.Execute) then
     Exit;
    ListView1.Items.Clear;
    Edit2.Text:=OpenDialog1.FileName;
    _regexp:=TRegExpr.Create;
    _regexp.Expression:='([^s]+)s([^s]+)s([d.]+)s([d+-]+)';
    _tempFile:=TStringList.Create;
    _tempFile.LoadFromFile(OpenDialog1.FileName);
    for i:=0 to _tempFile.Count-1 do
    begin
     _regexp.Exec(_tempFile.Strings[i]);
     
     if (_regexp.Exec) then
      with ListView1.Items.Add do
      begin
       Caption:=_regexp.Match[1];
       SubItems.Add(_regexp.Match[2]);
       SubItems.Add(_regexp.Match[3]);
       SubItems.Add(_regexp.Match[4]);
      end;
    end;
    _regexp.Free;
    _tempFile.Free;

    Как и в прошлом примере, перед тем, как использовать объект TRegExpr его нужно инициализировать, а уже потом присвоить текст регулярного выражения. Для решения данной задачки можно составить вот такой ([^s]+)s([^s]+)s([d.]+)s([d+-]+) шаблон. Как обычно, остановимся на тексте шаблона подробней.

  • ([^s]+) – в имени могут содержаться любые символы кроме пробела, поэтому при описании символьного класса я явно указываю на это (^ - отрицание, s – разделитель). По условию мы должны сохранить найденное имя, поэтому берем всю конструкцию в скобки.
  • s(^s)+) – после имени должен обязательно идти разделитель, а раз так, то нужно обязательно его указать (s). Далее следует шаблон для выделения фамилии. Он идентичен шаблону определения имени.
  • s([d.]+) – шаблон для даты рождения. В дате не могут использоваться буквы, поэтому устанавливаем лишь набор цифр (d) и точку, которая служит как разделитель.
  • s([d+-]+) – пробел, цифры, знак + и – задают номер телефона. Все легко и просто. При положительном выполнении метода Exec, в свойстве Match у нас будут все разделенные данные, доступ к которым осуществляется через объект_рег_выражений.match[n], где n – номер вхождения. На данном этапе получается, что у нас уже есть разделенные данные, а значит пора их сохранять. Ты можешь сохранять их сразу в БД, а я для своего примера сохраняю их в ListView.
  • Итог

    Использовать регулярные выражения удобно и не так сложно, как может показаться на первый взгляд. Сегодняшние простые, но в тоже время полезные примеры - лишнее тому подтверждение. К сожалению, в рамках одной статьи мы не можем рассказать тебе все о регулярных выражениях. Тема настолько большая, что для ее изучения потребуется прочитать немало умных книг. Мы в тебя верим, и знаем, что при особом желании ты во всем разберешься. Ну а пока можешь задавать свои вопросы мне на мыло.

    Статья опубликована журнале "Хакер" (http://xakep.ru). Ноябрь 2007 г.

    Ссылка на опубликованную статью сайта издания: http://goo.gl/X3VZyc

    Ссылка на журнал: http://goo.gl/L4Hp90

    Оставьте комментарий!
    comments powered by HyperComments