Регулярні вирази в Delphi

  1. Трішки історії
  2. основи основ
  3. Регулярні вирази в Delphi
  4. Збираємо свій антиспам-лист
  5. Код процедури FindFiles ()
  6. Код процедури findMailUtils
  7. Ще один корисний прімерчік
  8. Перегінний куб
  9. підсумок

Регулярні вирази - один з головних «інструментів» завзятих линуксоидов і 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. Таким же способом можна задавати і літерні інтервали: az (всі латинські літери в нижньому регістрі), a-zA-Z (латинські літери як в нижньому, так і в верхньому регістрах ). Ми збираємося шукати номери телефонів, а вони можуть складатися з цифр і знака «-», який їх розділяє. Більше бути нічого не повинно.
  • «+» В даному випадку, знак плюса, виконує роль метасимвола. Тепер мені б хотілося звернути твою увагу на опис символьних класів. У квадратних дужках всі перераховані символи діють зовсім по-іншому. Наприклад, якщо поставити перед символами метасимвол ^, то це буде означати вже не початок рядка, а заперечення. Наприклад: [^ abc] - все символи, крім abc.
  • Решта метасимволу, зазначені в символьному класі втрачають свою силу і стають звичайними літералами.

    Розглянемо таку задачу. Є два рядки, в якій містяться літери і цифри. Завдання полягає в тому, що необхідно вибрати частину рядка, в якій спочатку йдуть підряд 3 латинські букви, а за ними цифри в інтервалі від 3 до 6. Рядки наступні:

    abdajD345bhad5124jjdaabc3456 abdj23456kfej3456fe

    Спробуй вирішити це завдання самостійно, на основі отриманих знань. Моє рішення буде виглядати так: «[az] {3} [3-6] {4}». Найбільш зручним способом вирішення даного завдання є установка обмежень на кількість знаходять символів. Отже, нам потрібно знайти 3 латинських букви. Можна, звичайно, три рази записати клас [az], а можна просто в фігурних дужках вказати їх кількість. Це я і зробив. Наступним умовою є цифри від 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

    Як бути, якщо мені потрібно відобразити всі імена файлів, у яких розширення html і htm?
    Htm?
    У цьому записі присутній два метасимвола - «*» і «?
    Працює?
    А чому?
    Htm?
    Wd - =?
    Ось і все, одним рядком ми описали шаблон для пошуку Мильники, чи не так класно?
    Wd - =?
    Wd - =?