Типовые выражения для осуществления поиска
( Бенедикт Ченг )
Удобство типовых выражений для анализа и обработки текста.
Обзор
Существует большое количество приложений для обработки конфигурационных или регистрационных файлов путем вычленения необходимых данных из моря дополнительной информации или текста.В настоящее время вместо стандартных классов String или StringTokenizer с их рудиментарными способностями обрабатывать строки используются более совершенные инструменты для обработки и анализа текстовых файлов с помощью сравнительных шаблонов (pattern matching). (2700 слов)
- Типовые выражения 101
- Библиотека Jakarta-ORO
- Объекты Jakarta-ORO
- Сценарий использования API
- Работа с библиотекой Jakarta-ORO
- Обработка HTML
- Дальнейшая обработка HTML
- Заключение
- Об авторе
- Ресурсы
Существует большое количество приложений для обработки конфигурационных или регистрационных файлов путем вычленения необходимых данных из моря дополнительной информации или текста. Также в них применяются парсеры для анализа простых выражений. В настоящее время вместо стандартных классов 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 раз |
Предположим, вам нужно найти в текстовом файле номер социального страхования. Формат номера социального страхования в США выглядит так: 999-99-9999. Типовое выражение, которым вы будете пользоваться, показано на Рисунке 1. В типовых выражениях дефисные представления ("-") имеют особое значение. Они определяют радиус соответствия чисел от 0 до 9. В результате вам нужно отказаться от комбинации символа дефиса со стоящи перед ним символом "\" если мы используем обычный дефис, разделяющий цифры в номере социального страхования.

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

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

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

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

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

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

Библиотека 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:

Вам необходимо избегать символа точки, поскольку она вам в буквальном смысле там нужна, но естественно не в том специальном значении синтаксиса типовых выражений, которое я объяснил ранее.
Отметка о времени заполнения log-файла берётся в квадратные скобки. Вы можете вычленить всё, что может находиться в этих скобках, сначала отыскав открывающую скобку ("[") и вычленяя то, что не входит в закрывающую скобку ("]"), до тех пор пока вы не упрётесь в закрывающую скобку. На Рисунке 9 приводится типовое выражение для этой операции:
![До обнаружения "]" находится как минимум один символ.](/javaworld/legacy/jw-07-2001/images/jw-07-reg9.gif)
Теперь вы можете скомбинировать эти два выражения в одно, содержащее групповое представление (круглые скобки) для вычленения IP-адреса и отметки о времени. Обращаю ваше внимание на то, что "\s-\s-\s" поставлено в середине для того, чтобы выполнялся поиск, даже если вам это не совсем нужно. Целиком типовое выражение показано на Рисунке 10:

Теперь, после того как вы составили нужное вам выражение, вы можете приступать к написанию 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:

Второе типовое выражение, показанное на Рисунке 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:

С помощью приведенного ниже выражения вы можете заменить ссылку в Примере 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
