[an error occurred while processing this directive] IT • archiv :: Print

IT • archiv


[an error occurred while processing this directive]

[an error occurred while processing this directive]

Типовые выражения для осуществления поиска

[an error occurred while processing this directive](none) [an error occurred while processing this directive](none)[an error occurred while processing this directive] ::
[an error occurred while processing this directive](none)
[an error occurred while processing this directive]([an error occurred while processing this directive] Бенедикт Ченг [an error occurred while processing this directive])

[an error occurred while processing this directive](none)

Удобство типовых выражений для анализа и обработки текста.

Code Tutorial
PDF versionPDF версия
Обзор
Существует большое количество приложений для обработки конфигурационных или регистрационных файлов путем вычленения необходимых данных из моря дополнительной информации или текста.В настоящее время вместо стандартных классов String или StringTokenizer с их рудиментарными способностями обрабатывать строки используются более совершенные инструменты для обработки и анализа текстовых файлов с помощью сравнительных шаблонов (pattern matching). (2700 слов)

Существует большое количество приложений для обработки конфигурационных или регистрационных файлов путем вычленения необходимых данных из моря дополнительной информации или текста. Также в них применяются парсеры для анализа простых выражений. В настоящее время вместо стандартных классов String или StringTokenizer с их рудиментарными способностями обрабатывать строки используются более совершенные инструменты. Вам уже не нужно писать неразборчивый код с множеством подстрочных функций типа charAt или StringTokenizer , при условии, что вы освоите технику обработки и анализа текстовых файлов с помощью сравнительных шаблонов (pattern matching).

Если вы программировали на Perl или каком-либо из языков с возможностями устойчивых типовых выражений, то вы тогда должны знать, насколько проще с их помощью выполняется обработка текста и поиск по шаблону. Если вы незнакомы с термином типовое выражение (regular expression), то поясню, что речь идёт о наборе символов, определяющих некий шаблон (pattern) для поиска соответствующего ему отрывка строки.

Многие языки, в том числе Perl, PHP, Python, JavaScript и Jscript поддерживают типовые выражения для обработки текста, а некоторые текстовые редакторы используют типовые выражения для выполнения поиска и замены. А что же Java? Во время, когда готовилась эта статья, была одобрена и принята спецификация Java Specification Request, содержащая библиотеку выражений для обработки текста. Можно рассчитывать на то, что в следующей версии JDK она уже будет присутствовать.

Но как быть, если библиотека выражений нужна уже сегодня? К счастью, её можно скачать с открытого проекта Jakarta ORO на сайте Apache.org . В данной статье я вначале поснакомлю вас с основными положениями теории типовых выражений, а затем покажу, как они могут использоваться с Jakarta-ORO API.

Типовые выражения 101

Начнём с простого. Предположим, вам нужно найти строку, содержащую слово "cat". Вашим типовым выражением может быть само слово "cat". Если ваш поиск не предполагает ограничений по регистру, то в радиус поиска попадут также слова типа " cat alog", " Cat herine" или "sophisti cat ed":

Regular expression: cat
Matches: cat, catalog, Catherine, sophisticated

Точечное представление

Допустим, вы играете в слова и вам нужно подобрать слово из трёх букв, первая из которых "t" а последняя "n". Допустим также, что у вас есть словарь английского языка, и вы намерены провести поиск по всему содержанию, пользуясь типовым выражением. Для создания такого выражения вы можете воспользоваться представлением с символом-заменителем, в данном случае точкой. Выражение будет выглядеть как "t.n", ему будут соответствовать слова "tan", "Ten", "tin" и "ton"; а так же в эту группу может войти большое количество не имеющих смысла слов типа "t#n", "tpn" и даже "t n". Это происходит за счёт того, что символ точки может заменять любой символ, включая пробел, табулятор и даже перевод строки:

Regular expression: t.n
Matches: tan, Ten, tin, ton, t n, t#n, tpn, etc.

Представление с квадратными скобками

Чтобы предотвратить появление в списке поиска бессмысленных слов, отсутствующие символы можно заменить выражением с квадратными скобками ("[]"). Так, к примеру, шаблону "t[aeio]n" будут соответствовать только слова "tan", "Ten", "tin" и "ton", а вот "Toon" уже не войдёт в этот список, поскольку скобками замещается только один символ:

Regular expression: t[aeio]n<
Matches: tan, Ten, tin, ton

Оператор OR

Если вам нужно, чтобы "toon" тоже соответствовал шаблонному выражению, можно использовать символ "|", который по существу является «или»-оператором. Чтобы выполнить последнее условие, воспользуемся типовым выражением "t(a|e|i|o|oo)n". Здесь вы уже не можете применить квадратные скобки, так как они определяют только один символ. Вместо них используются обычные скобки "()", которые могут также использоваться для группировки (об этом несколько позже):

Regular expression: t(a|e|i|o|oo)n
Matches: tan, Ten, tin, ton, toon

Квантификаторное представление

В Таблице 1 приведены квантификатoрные (количественные) представления, используемые для указания количества повторений стоящего слева от значка выражения:

Выражение Количество повторений
* 0 или более раз
+ 1 или более раз
? 0 или 1 раз
{n} точно n раз
{n,m} от n до m раз
Таблица 1.

Предположим, вам нужно найти в текстовом файле номер социального страхования. Формат номера социального страхования в США выглядит так: 999-99-9999. Типовое выражение, которым вы будете пользоваться, показано на Рисунке 1. В типовых выражениях дефисные представления ("-") имеют особое значение. Они определяют радиус соответствия чисел от 0 до 9. В результате вам нужно отказаться от комбинации символа дефиса со стоящи перед ним символом "\" если мы используем обычный дефис, разделяющий цифры в номере социального страхования.

Соответствия номеров социального страхования формата 123-12-1234.
Рисунок 1. Соответствия номеров социального страхования формата 123-12-1234.

Если во время поиска вам захочется определить наличие дефиса необязательным (скажем и 999-99-9999, и 999999999), вы можете использовать количественное (квантификаторное) представление с "?". Это выражение показано на Рисунке 2:

Соответствия номеров форматам 123-12-1234 и 123121234.
Рисунок 2. Соответствия номеров форматам 123-12-1234 и 123121234.

Теперь рассмотрим другой пример. Формат автомобильных регистрационных номеров в США содержит четыре цифры и две буквы. Сначала типовое выражение должно включать цифровую часть "[0-9]{4}", за которой будет следовать текстовая "[A-Z]{2}". На Рисунке 3 выражение показано целиком:

Соответствия регистрационных номеров формата 8836KV.
Рисунок 3. Соответствия регистрационных номеров формата 8836KV.

НЕТ-представления

Представления с "^" также называются нет-представления. Если данный символ используется в квадратных скобках, это будет означать символ, который не должен присутствовать в вашем шаблоне. Например, выражение на Рисунке 4 ищет все слова за исключением тех, что начинаются с буквы Х:

Соответствия всех слов кроме тех, что начинаются с “x”.
Рисунок 4. Соответствия всех слов кроме тех, что начинаются с “x”.

Представление с обычными скобками и пробелом

Допустим, теперь вам нужно вычленить месяц из даты рождения. Традиционно формат даты выглядит (в американском варианте – прим. переводчика): June 26, 1951. Типовое выражение для поиска соответствия будет выглядеть так, как на Рисунке 5:

Соответствие всех дат формата Месяц ЧЧ, ГГГГ.
Рисунок 5. Соответствие всех дат формата Месяц ЧЧ, ГГГГ.

Появившееся новое представление "\s" соответствует пробелу, т.е. пропуску между символами, включая табуляторы. Как же вычленить поле месяца из строки, которая подошла под наш шаблон? Просто нужно взять поле месяца в круглые скобки, создав группу, а затем получить её значение с помощью ORO API, который мы обсудим в следующем разделе. Само выражение показано на Рисунке 6:

Соответствие всех дат формата Месяц ЧЧ, ГГГГ, с выделенным полем месяца Группа 1.
Рисунок 6. Соответствие всех дат формата Месяц ЧЧ, ГГГГ, с выделенным полем месяца Группа 1.

Остальные представления

Чтобы облегчить рабочий процесс, был создан целый список кратких типовых представлений, перечень которых можно найти в Таблице 2:

Краткое выражение Полное представление
\d [0-9]
\D [^0-9]
\w [A-Z0-9]
\W [^A-Z0-9]
\s [ \t\n\r\f]
\S [^ \t\n\r\f]
Таблица 2. Наиболее популярные выражения.

В качестве иллюстрации мы можем использовать "\d" для производных "[0-9]", рассмотренных нами ранее в примере с номером социального страхования. Новый укороченный вариант показан на Рисунке 7:

Соответствия номеров социального страхования формата 123-12-1234.
Рисунок 7. Соответствия номеров социального страхования формата 123-12-1234.

Библиотека Jakarta-ORO

В настоящее время Java-программистам доступны многие открытые библиотеки типовых выражений, большинство из которых поддерживает Perl 5-совместимый синтаксис. Я пользуюсь библиотекой Jakarta-ORO, поскольку это один из наиболее удобных API, существующих сегодня, и полностью совместимый с типовыми выражениями Perl 5. Кроме того, это наиболее оптимизированный интерфейс.

Сама библиотека была ранее известна под именем OROMatcher, но в последствии безвозмездно передана проекту Jakarta Дэниелом Саварезом (Daniel Savarese). Пакет можно скачать с сайта, указанного в Ресурсах .

Объекты Jakarta-ORO

Я начну с краткого описания объектов, которые вам необходимо создать и иметь к ним доступ, чтобы пользоваться данной библиотекой, а затем я покажу, как пользоваться самой библиотекой.

Объект PatternCompiler

Во-первых, создадим экземпляр класса Perl5Compiler и приставим его к объекту интерфейса PatternCompiler. Perl5Compiler является реализацией интерфейса PatternCompiler и позволяет вам компилировать строку типового выражения в объект Pattern, используемый для поиска соответствий:

PatternCompiler compiler=new Perl5Compiler();

Объект Pattern

Для компиляции типовых выражений в объекте Pattern нужно вызвать метод compile() объекта компилятора, передав в него это выражение. Например, вы можете скомпилировать выражение "t[aeio]n" следующим образом:

Pattern pattern=null;
         try {
                 pattern=compiler.compile("t[aeio]n");
        } catch (MalformedPatternException e) {
                 e.printStackTrace();
        }

По умолчанию компилятор создаёт шаблон, учитывающий регистр букв, поэтому указанным настройкам будут соответствовать только "tin", "tan", "ten" и "ton", но не "Tin" или "taN". Чтобы шаблон не учитывал регистр, нужно вызвать компилятор с дополнительной маской:

pattern=compiler.compile("t[aeio]n",Perl5Compiler.CASE_INSENSITIVE_MASK);

Однажды создав объект Pattern , вы можете пользоваться им для поиска по шаблону с помощью класса PatternMatcher .

Объект PatternMatcher

Объект PatternMatcher осуществляет поиск соответствий на основе объекта Pattern и строки. Вы создаёте класс Perl5Matcher и приписываете его к интерфейсу PatternMatcher . Класс Perl5Matcher является реализацией интерфейса PatternMatcher и выполняет поиск соответствий, руководствуясь синтаксисом типовых выражений Perl 5:

      PatternMatcher matcher=new
Perl5Matcher();

Получить искомое соответствие с помощью объекта PatternMatcher можно одним из приведенных способов, когда строка для осуществления поиска по шаблону передаётся в качестве первого параметра:

  • boolean matches(String input, Pattern pattern) : используется в случае, когда исходная строка и типовое выражение должны быть абсолютно идентичны, т.е. типовое выражение должно полностью описывать строку ввода.
  • boolean matchesPrefix(String input, Pattern pattern) : используется в случае, когда типовое выражение должно соответствовать началу строки ввода.
  • boolean contains(String input, Pattern pattern ): используется в случае, когда типовое выражение должно соответствовать какой-либо части строки ввода (т.е. само является частью строки).

Во всех трёх случаях вместо объекта String можно также передавать и объект PatternMatcherInput . В этом случае вы можете продолжать процесс поиска соответствий с того места, где была завершена предыдущая операция. Этот способ удобен тогда, когда строка у вас состоит из нескольких частей, которые могут отвечать вашему типовому выражению. При работе с объектом PatternMatcherInput вместо String сигнатуры метода будут выглядеть так:

  • boolean matches(PatternMatcherInput input, Pattern pattern)
  • boolean matchesPrefix(PatternMatcherInput input, Pattern pattern)
  • boolean contains(PatternMatcherInput input, Pattern pattern)

Сценарий использования API

А теперь обсудим несколько примеров использования библиотеки Jakarta-ORO.

Обработка log-файла

Ваша задача: проанализировать Web-сервер и определить, сколько времени каждый пользователь проводит на Web-сайте. Регистрационная статья стандартного log-файла из BEA WebLogic выглядит так:

172.26.155.241 — — [26/Feb/2001:10:56:03 -0500] "GET /IsAlive.htm
HTTP/1.0" 200 15

Проанализировав эти данные, вы понимаете, что вам нужно вычленить из log-файла две вещи: IP-адрес и время доступа к странице. Чтобы выделить поле IP-адреса и поле регистрации времени в регистрационной статье, вы можете воспользоваться групповым представлением (круглыми скобками).

Рассмотрит сначала IP-адрес. Он содержит 4 байта, каждый из которых имеет значение в пределах от 0 до 255; каждый байт отделен от другого точкой. Таким образом, в каждом из байтов IP-адреса у вас имеется от одной до трёх цифр. Типовое выражение для такого поля будет выглядеть так, как показано на Рисунке 8:

IP-адрес из 4 байтов, значение каждого из которых находится между 0 и 255.
Рисунок 8. IP-адрес из 4 байтов, значение каждого из которых находится между 0 и 255.

Вам необходимо избегать символа точки, поскольку она вам в буквальном смысле там нужна, но естественно не в том специальном значении синтаксиса типовых выражений, которое я объяснил ранее.

Отметка о времени заполнения log-файла берётся в квадратные скобки. Вы можете вычленить всё, что может находиться в этих скобках, сначала отыскав открывающую скобку ("[") и вычленяя то, что не входит в закрывающую скобку ("]"), до тех пор пока вы не упрётесь в закрывающую скобку. На Рисунке 9 приводится типовое выражение для этой операции:

До обнаружения "]" находится как минимум один символ.
Рисунок 9. До обнаружения "]" находится как минимум один символ.

Теперь вы можете скомбинировать эти два выражения в одно, содержащее групповое представление (круглые скобки) для вычленения IP-адреса и отметки о времени. Обращаю ваше внимание на то, что "\s-\s-\s" поставлено в середине для того, чтобы выполнялся поиск, даже если вам это не совсем нужно. Целиком типовое выражение показано на Рисунке 10:

Определение места нахождения IP-адреса и отметки о времени путём комбинирования двух выражений.
Рисунок 10. Определение места нахождения IP-адреса и отметки о времени путём комбинирования двух выражений.

Теперь, после того как вы составили нужное вам выражение, вы можете приступать к написанию Java-кода, пользуясь библиотекой типовых выражений.

Работа с библиотекой Jakarta-ORO

Чтобы начать работать с библиотекой Jakarta-ORO, вначале создайте строку типового выражения и выберете какую-либо строку для анализа:

      String logEntry="172.26.155.241 — — [26/Feb/2001:10:56:03
-0500] \"GET /IsAlive.htm HTTP/1.0\" 200 15 ";
        String
regexp="([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})\\s-\\s-\\s\\[([^\\]]+)\\]";

Используемое здесь типовое выражение практически идентично тому, что было приведено на Рисунке 10, с одной лишь разницей, что в Java нужно избегать использования символа "\". Поскольку Рисунок 10 не является кодом Java, нам нужно избавиться от этого символа, чтобы избежать ошибки при компиляции. К сожалению, данный процесс достаточно проблематичен, так что постарайтесь быть повнимательнее. Сначала наберите типовое выражение так, как оно есть, а затем каждый символ "\" в строке замените двойным "\\". Для проверки введите получившуюся строку в консоль.

После запуска строк, создайте экземпляр объекта PatternCompiler и с помощью PatternCompiler для компиляции типовых выражений создайте объект Pattern :

      PatternCompiler compiler=new
Perl5Compiler();
         Pattern pattern=compiler.compile(regexp);

Теперь создайте объект PatternMatcher и вызовите метод contain() из интерфейса PatternMatcher , чтобы увидеть, есть ли соответствие:

      PatternMatcher matcher=new
Perl5Matcher();
         if (matcher.contains(logEntry,pattern)) {
             MatchResult result=matcher.getMatch();
             System.out.println("IP: "+result.group(1));
             System.out.println("Timestamp:
"+result.group(2));
        }

Затем распечатайте найденные группы с помощью объекта MatchResult, возвращаемого из интерфейса PatternMatcher. Поскольку строка logEntry содержит шаблон для поиска соответствий, результат может выглядеть следующим образом:

        IP: 172.26.155.241
        Timestamp: 26/Feb/2001:10:56:03 -0500

Обработка HTML

Вашим следующим заданием будет пройти через все HTML-страницы вашей компании и проанализировать все атрибуты шрифтовых меток. Стандартная метка шрифта в HTML выглядит так: <font face="Arial, Serif" size="+2" color="red">

Ваша программа распечатает атрибуты каждой найденной метки в следующем формате:

        face=Arial, Serif
        size=+2
        color=red

В этом случае, я бы предложил вам воспользоваться двумя типовыми выражениями. Первое для вычленения из метки шрифта отрывка "face="Arial, Serif" size="+2" color="red" показано на Рисунке 11:

Отрывок метки со всеми атрибутами.
Рисунок 11. Отрывок метки со всеми атрибутами.

Второе типовое выражение, показанное на Рисунке 12, разбивает каждый атрибут на пару «имя-значение»:

Атрибутивная пара «имя-значение».
Рисунок 12. Атрибутивная пара «имя-значение».

Рисунок 12 выдаёт следующий результат:

        font Arial, Serif
        size +2
        color red

А теперь, обсудим код, необходимый для всего этого. Во-первых, создайте две строки с типовыми выражениями и с помощью Perl5Compiler скомпилируйте их в объект Pattern . Для поиска соответствий не зависимо то регистра используйте опцию Perl5Compiler.CASE_INSENSITIVE_MASK .

Затем создайте объект Perl5Matcher для выполнения поиска соответствий:

        String
regexpForFontTag="<\\s*font\\s+([^>]*)\\s*>";
         String
regexpForFontAttrib="([a-z]+)\\s*=\\s*\"([^\"]+)\"";

         PatternCompiler compiler=new Perl5Compiler();
         Pattern
patternForFontTag=compiler.compile(regexpForFontTag,Perl5Compiler.CASE_INSENSITIVE_MASK);

         Pattern
patternForFontAttrib=compiler.compile(regexpForFontAttrib,Perl5Compiler.CASE_INSENSITIVE_MASK);


         PatternMatcher matcher=new Perl5Matcher();

Допустим, у вас есть переменная с именем html типа String , представляющая строку в HTML-файле. Если эта строка html содержит метку шрифта, результат поиска соответствия вернётся true , а вам для получения первой группы, содержащей все атрибуты шрифта, придётся воспользоваться объектом MatchResult , возвращаемым из шаблонного объекта:

        if
(matcher.contains(html,patternForFontTag)) {
             MatchResult result=matcher.getMatch();
             String attribs=result.group(1);



         PatternMatcherInput input=new
PatternMatcherInput(attribs);
             while (matcher.contains(input,patternForFontAttrib)) {
                 result=matcher.getMatch();
                 System.out.println(result.group(1)+":
"+result.group(2));
            }
        }

Затем создадим объект PatternMatcherInput. Как говорилось ранее, этот объект позволяет осуществлять поиск соответствий с того места, где мы обнаружили последнее соответствие в строке. Поэтому он отлично подходит для вычленения пар «имя-значение» меток шрифта. Создаём объект PatternMatcherInput, содержащий шаблонную строку. Затем с помощью экземпляра шаблона, как описано выше, вычленяем каждый из атрибутов шрифта. Эта операция выполняется путем повторяющегося вызова метода contains() из объекта PatternMatcher и объекта PatternMatcherInput, используемого вместо строки. Каждая последующая итерация объекта PatternMatcherInput будет продвигать указатель вперёд, таким образом, чтобы следующий тест начинался с того места, где завершился предыдущий.

Результат работы нашего примера будет выглядеть так:

        face: Arial, Serif
        size: +1
        color: red

Дальнейшая обработка HTML

Продолжим разбор нашего примера с HTML. На это раз, представим себе, что наш Web-сервер переехал с адреса widgets.acme.com на newserver.acme.com . Нужно заменить ссылки в некоторых из Web-страниц:

<link href="httр://widgets.acme.com/interface.html#How_To_Buy">

<link href="httр://widgets.acme.com/interface.html#How_To_Sell">

etc.

на

<a
href="httр://newserver.acme.com/interface.html#How_To_Buy">
<link href="httр://newserver.acme.com/interface.html#How_To_Sell">

etc.

Типовое выражение для выполнения такого поиска показано на Рисунке 13:

Cсылка "httр://widgets.acme.com/interface.html#(any anchor).
Рисунок 13. Cсылка "httр://widgets.acme.com/interface.html#(any anchor).

С помощью приведенного ниже выражения вы можете заменить ссылку в Примере 13:

<link href="httр://newserver.acme.com/interface.html#$1">

Обратите внимание на $1 после символа # . Синтаксис типовых выражений Perl использует такие комбинации типа $1 , $2 и т.п. для представления групп, которые были обнаружены и вычленены. Выражение на Рисунке 13 может присоединять к ссылке в качестве Группы 1 любой текст.

Теперь вернёмся к Java. Вам снова нужно создать тестовые строки, объект для компиляции типового выражения в объекте Pattern , объект PatternMatcher :

    String link="<a
href=\"httр://widgets.acme.com/interface.html#How_To_Trade\">";

     String regexpForLink=
"<\\s*a\\s+href\\s*=\\s*\"http://widgets.acme.com/interface.html#([^\"]+)\">";


     PatternCompiler compiler=new Perl5Compiler();
     Pattern
patternForLink=compiler.compile(regexpForLink,Perl5Compiler.CASE_INSENSITIVE_MASK);


       PatternMatcher matcher=new Perl5Matcher();

Теперь воспользуемся статичным методом substitute() из класса Util из пакета com.oroinc.text.regex для выполнения замещения и распечатаем получившуюся строку:

        String result=Util.substitute(matcher,
                                      patternForLink,
                                      new Perl5Substitution(
                                        "<a
href=\"httр://newserver.acme.com/interface.html#$1\">"),
                                      link,
                                      Util.SUBSTITUTE_ALL);
        System.out.println(result);

Синтаксис метода Util.substitute() будет выглядеть так:

    public static String substitute(PatternMatcher matcher,
                                    Pattern pattern,
                                    Substitution sub,
                                    String input,
                                    int numSubs)

Первыми двумя параметрами для вызова будут объекты PatternMatcher и Pattern , созданные ранее. Исходным данным для третьего параметра является объект Substitution , определяющий порядок замещения. В данном случае используется объект Perl5Substitution , позволяющий применить замещение характерное для Perl 5. Четвёртый параметр – это сама строка, на которой вы собираетесь провести операцию замещения. И последний параметр позволяет вам определить, будете ли вы выполнять замещение всех обнаруженных соответствий или только определённого числа.

Заключение

В этой статье я показал, как можно использовать способности типовых выражений (regular expressions). При грамотном применении они значительно облегчают работу по поиску строк при редактировании текстов. Я также показал способ внедрения типовых выражений в ваши Java-приложения с помощью открытой библиотеки Jakarta-ORO. Теперь это ваше дело, пользоваться ли старым подходом ( StringTokenizers , charAt , или substring ) для манипулирования строками или типовыми выражениями вроде библиотеки Jakarta-ORO.

Об авторе

Benedict Chng родом из Сингапура, является сертифицированным Sun разработчиком и в настоящее время занимается консалтингом в Бостоне. Его основные интересы заключаются в разработке приложений для Palm-приборов и посещений достопримечательностей Новой Англии.

Ресурсы

Reprinted with permission from the July 2001 edition of JavaWorld magazine. Copyright © ITworld.com, Inc., an IDG Communications company.
View the original article at: http://www.javaworld.com/javaworld/ jw-07-2001/jw-0713-regex.html

Русский перевод опубликован с разрешения Java Журнал © IBA Java Team, 2001. Оригинал статьи: http://java.iba.by/javaweb/ibajavat.nsf/ lnarticlesview/AD1E1D56D6AD1A7042256A8F002C7E3D

[an error occurred while processing this directive]
[an error occurred while processing this directive] Перевод на русский © Андрей Чернухо, 2001
< Вернуться на caйт :: Copyright © 1999 — 2010, IT • archiv.