Улучшите ваши навыки создания шаблонов регулярных выражений

Михаэль Штутц

Введение

Каждый день UNIX-администраторы создают и используют регулярные выражения (regexps) чтобы сопоставлять образцы текста. Большинство языков поддерживает некоторую реализацию регулярных выражений. Некоторые приложения (типа EMACS) имеют возможности поиска регулярных выражений, регулярные выражения можно использовать с множеством инструментов командной строки. Для любого приложения ключ к созданию хороших регулярных выражений - это распознавание образца, который содержит только ту информацию, которую вам надо проверять, так, чтобы ничто другое от входных значений не мешало.

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

Использование регулярных выражений (regexps)

Замечу, что примеры в этой статье являются регулярными выражениями для Portable Operating System Interface-extended (POSIX-extended). Если вы используете их в командной строке (с утилитой egrep, например), следует заключать их в кавычки, как заключают любые другие регулярные выражения. Помните, что существуют отличия в реализациях регулярных выражений, поэтому, возможно, придется адаптировать их для различных инструментов, приложений или языка программирования, который вы используете.

Полное совпадение

Вы знаете, что метасимвол ^ соответствует началу строки, а метасимвол $ соответствует концу строки. Работая вместе (как ^$), они позволяют выбрать пустые строки. (Зеркальное отображение этого выражения - $^ - является невозможным, для него никогда не будет найдено соответствие среди реальных строк). Этот базовый регулярный оператор является основным для многих сложных регулярных выражений, и следует привыкнуть к его использованию, если это не было сделано ранее. Используйте его для построения шаблонов, которые находят совпадения, основанные на полном содержимом строки.

Это хороший базовый шаблон для поиска в файле словаря пользователя (/usr/dict/words). Некоторые пользователи UNIX перемещают этот файл в /usr/share/dict/words.

Например, скажем вы забыли как пишется слово fuchsia. В нем пишется sh или cs? Все, в чем вы уверены, это то, что оно начинается с fu и заканчивается ia.

Попробуем поискать с помощью следующего шаблона:

          $ egrep -i '^fu.*ia$' /usr/dict/words
                

Флаг -i ищет совпадения независимо от регистра. В этом примере слово fuchsia является единственным возвращаемым значением.

Сопоставление строк, основанное на длине

Используйте метасимвол "фигурные скобки" ({ }) для задания конкретного числа совпадений в регулярном выражении, как показано в таблице 1. Когда вы добавляете их (фигурные скобки) к только что описанному поиску полной строки, вы получаете возможность задать длину строки.

Таблица 1.Значение фигурных скобок

Пример

Описание

{X}  Этот символ сопоставляется регулярным выражением X раз.
{X,}  Этот символ сопоставляется регулярным выражением X или больше раз.
{X,Y}  Этот символ сопоставляется регулярным выражением по меньшей мере X,но не больше чем Y раз.

Не все реализации расширенных регулярных выражений поддерживают фигурные скобки. В зависимости от реализации фигурные скобки могут потребовать экранирования обратной наклонной чертой (backslash) при использовании.

Можно использовать это регулярное выражение для получения набора слов из словаря, упорядоченных по длине. Точное число слов, которое вы получите, зависит от числа слов в словарном файле системы. Результат будет примерно таким, как показано в примере 1. В этом примере самой распространненой длиной слова было девять букв, чему в словаре соответствует 32380 слов. Словарь не содержит слов, в которых 25 или более букв, и самое длинное слово содержит не 21 букву, это не disestablishmentarian , как вы могли бы подумать, потому что есть еще 81 слово такой же длины, например superincomprehensible и phoneticohieroglyphic . Приз за самое длинное слово в словаре UNIX поделен между пятью словами, включая pathologicopsychological .

Пример 1. Подсчет слов с числом букв X в словаре

        $ for i in `seq 1 32`
        >  {
        >   
          echo "There are" `egrep '^.{'$i'}$' /usr/dict/words            / wc -l` "$i-letter words in the dictionary."
        
        >  }
        There are 52 1-letter words in the dictionary.
        There are 155 2-letter words in the dictionary.
        There are 1351 3-letter words in the dictionary.
        There are 5110 4-letter words in the dictionary.
        There are 9987 5-letter words in the dictionary.
        There are 17477 6-letter words in the dictionary.
        There are 23734 7-letter words in the dictionary.
        There are 29926 8-letter words in the dictionary.
        There are 32380 9-letter words in the dictionary.
        There are 30867 10-letter words in the dictionary.
        There are 26011 11-letter words in the dictionary.
        There are 20460 12-letter words in the dictionary.
        There are 14938 13-letter words in the dictionary.
        There are 9762 14-letter words in the dictionary.
        There are 5924 15-letter words in the dictionary.
        There are 3377 16-letter words in the dictionary.
        There are 1813 17-letter words in the dictionary.
        There are 842 18-letter words in the dictionary.
        There are 428 19-letter words in the dictionary.
        There are 198 20-letter words in the dictionary.
        There are 82 21-letter words in the dictionary.
        There are 41 22-letter words in the dictionary.
        There are 17 23-letter words in the dictionary.
        There are 5 24-letter words in the dictionary.
        There are 0 25-letter words in the dictionary.
        There are 0 26-letter words in the dictionary.
        There are 0 27-letter words in the dictionary.
        There are 0 28-letter words in the dictionary.
        There are 0 29-letter words in the dictionary.
        There are 0 30-letter words in the dictionary.
        There are 0 31-letter words in the dictionary.
        There are 0 32-letter words in the dictionary.
        $
      

Сравнение слов

Угловые скобки \< и \> очень полезны при проектировании шаблонов: они заключают в себе слово для сравнения целиком . Они не сопоставят заключенный в них образец до тех пор, пока слово, которое рассматривается в данный момент регулярным выражением, не будет точно соответствовать заданному. Слово представляет собой набор символов (числа, буквы, символы подчеркивания), который с обоих концов ограничен несловарными символами, в состав которых входят:

  • начало строки
  • символ пробела
  • знак пунктуации
  • конец строки
  • любой другой символ,исключая буквы, числа и знаки препинания

Угловые скобки могут сильно сэкономить время, но часто их недостаточно эффективно используют, возможно из-за того, что не каждая реализация регулярных выражений поддерживает их. Если реализация, которой пользуетесь вы, поддерживает, сделайте их использование привычкой.

Поместим в угловые скобки слово, чтобы искать сопоставления только для него, например, так:

          \<system\>
        

Регулярное выражение в этом примере не выберет слова ecosystem , systemic или system/70 , не сопоставит оно и строки, где образец system находится где угодно в строке, - оно выведет только строки, состоящие лишь из system .

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

Для сравнения строк, содержащих слова, начинающиеся с pre , используйте:

          \<\(pre\).*\>
        

Предшествующий пример подбирает строки, содержащие слова preface и preposterous , но не spread или Dupre .

Сопоставление двойных слов

Используя угловые скобки, можно быстро найти двойные слова: после слова идет пробел и затем снова то же слово. Вы также можете использовать backreference , который сопоставляет часть образца самому себе и является рекурсивной возможностью большинства современных реализаций регулярных выражений. (Заключите часть образца, которую ыы хотите сделать эталонной, в круглые скобки и вызовите backreference с помощью бэкслэша ("\"), после этого запишите число вложений слова-образца: 1 для первой группы круглых скобок, 2 для второй группы круглых скобок и так далее.)

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

          (\<.*\>)( )+\1
        

Этот пример не находит двойные слова, отделенные друг от друга запятой, такие как It's been a long, long time .

Для поиска всех двойных слов, включая те, что разделены пробелами и другими знаками препинания, используйте это:

          (\<.*\>).?( )+\1
        

Для того чтобы сопоставление не зависело от регистра символов, используйте флаг -i .

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

Давайте перейдем к следующей ситуации, с которой вы постоянно сталкиваететсь: время и дата. Ниже представлена пара советов, как заставить регулярные выражения работать с корректными образцами времени и дат.

Вы не можете просто искать двузначные числа, которые определяют минуты и секунды, потому что минуты и секунды отсчитываются от 0 до 59; для их сопоставления заключите в скобки подходящие диапазоны для десятков и единиц для каждой из двух колонок:

  • Для сопоставления часов в 12- или 24-часовом форматах, используйте:
                (([0-1]?[0-9])/([2][0-3])):([0-5][0-9])(:[0-5][0-9])?
              

  • Для сопоставления времени в 12-часовом AM/PM формате с секундами или без них и даже для сопоставления образцов времени, у которых нет замыкающего идентификатора AM или PM в верхнем или нижнем регистре, используйте:
                ([^0-9])([0-1]?[0-9]){1}(((:([0-5]){1}([0-9]){1}){1,2})/(( )
                ?([AP]M)/([ap]m)))?
              

Без выражения отрицания в начале у последнего примера регулярное выражение будет находить время без двоеточия. Такое выражение, в зависимости от ваших входных данных, может иметь отношение к средневолновому радио (известное в Соединенных Штатах как AM-радио), например, 1450 AM.

Сопоставление месяцев

Сопоставление 12 месяцев требует списка, разделенного вертикальной чертой - оператор /, - но иногда названия месяцев сокращаются различными способами.

  • Чтобы найти любой из 12 месяцев по его полному названию или трехбуквенному сокращению, используйте следующее регулярное выражение (оно записывается в одну строчку):
                Jan(uary)?/Feb(uary)?/Mar(ch)?/Apr(il)?/May/Jun(e)?/Jul(y)?/
                Aug(ust)?/Sep(tember)?/Oct(ober)?/Nov(ember)?/Dec(ember)?
              

  • Вы можете сделать особый поиск - полное название или трехбуквенное сокращение - только то название месяца, после которого следует пустое пространство (пробел) или стоит точка (регулярное выражение записывается в одну строчку):
                Jan(uary/ /\.)/Feb(uary/ /\.)/Mar(ch/ /\.)/Apr(il/ /\.)/
                May( /\.)/Jun(e/ /\.)/Jul(y/ /            .)/Aug(ust/ /\.)/Sep(tember/ /\.)/Oct(ober/ /\.)/
                Nov(ember/ /\.)/Dec(ember/ /\.)
              

Заметьте, что для обоих примеров выше May (май) является исключением. Это единственный месяц, у которого полное название совпадает с трехбуквенным сокращением, поэтому правильно выполненное сравнение должно содержать любой из двух вариантов написания, для того чтобы слова типа Mayflower не вызвали ошибочного истинного значения.

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

Для решения этих проблем, делайте следующее:

  • Заключите все регулярное выражение в скобки, и поставьте перед ним спецификатор, позволяющий делать проверку либо в начале строки, либо после пробела, как сделано ниже (записывается в одну строчку):
                (^/ )(Jan(uary/ /\.)/Feb(uary/ /\.)/Mar(ch/ /\.)/Apr(il/ /\.)/
                May( /\.)/Jun(e/ /\.)/Jul(y/ /\.)/Aug(ust/ /\.)/Sep(tember/ /
                \.)/Oct(ober/ /\.)/Nov(ember/ /\.)/Dec(ember/ /\.))
              

  • Другой способ - задать перед регулярным выражением спецификатор для сопоставления неалфавитноцифровых символов, как сделано ниже (в одну строку):
                ([^A-Za-z0-9])(Jan(uary/ /\.)/Feb(uary/ /\.)/Mar(ch/ /\.)/
                Apr(il/ /\.)/May( /\.)/Jun(e/ /\.)/Jul(y/ /\.)/Aug(ust/ /\.)/
                Sep(tember/ /\.)/Oct(ober/ /\.)/Nov(ember/ /\.)/Dec(ember/ /\.))
              

Но все еще осталась потенциальная ошибка: ни один из этих примеров не предназначен для поиска в английской прозе, потому что есть шанс, что они вернут ложное совпадение, со словами типа Janelle или Augury. Чтобы исправить это, следует заключить каждый месяц в оболочку для слова оболочку для слова (слово - подразумевается синтаксическая единица).

Предыдущие примеры хорошо подходят для некоторых случаев без каких-либо дополнительных шагов для их модернизации. В других случаях эти образцы могут быть значительно упрощены: например, если вы осуществляете поиск в лог-файле, содержащем только числовые данные с датами, включающими в себя заглавные буквы, регулярное выражение типа [A-S], вполне вероятно, может являться всем тем, что вам понадобится для нахождения строк, содержащих названия месяцев.

Сопоставление дат

Вы можете комбинировать сопоставления, как описано в таблице 1, для поиска дат.

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

          "[A-Za-z]{3,10}\.? [0-9]{1,2}, ([0-9]{4}/'?[0-9]{2})"
        

Это регулярное выражение ищет соответствия для девяти различных форматов даты:

  1. MONTH [D]D, YY
  2. MONTH [D]D, 'YY
  3. MONTH [D]D, YYYY
  4. MON. [D]D, YY
  5. MON. [D]D, 'YY
  6. MON. [D]D, YYYY
  7. MON [D]D, YY
  8. MON [D]D, 'YY
  9. MON [D]D, YYYY

Неверные истинные значения, не соответствующие действительности, в данном регулярном выражении, вклюают Order 99, 99. Для того чтобы убрать их, можно соединить это регулярное выражение с выражением для месяцев, как описано выше, и в итоге регулярное выражение будет находить только настоящие названия месяцев.Также измените числовые диапазоны, чтобы избежать ложных сопоставлений, но это уже не обязательно, удвойте число возможных форматов до 18, изменяя положение запятой в шаблоне формата даты.

Все эти изменения были сделаны для регулярного выражения выше. Протестируйте его измененную версию:

          "([^A-Za-z0-9])(Jan(uary/ /\.)/Feb(uary/ /\.)/Mar(ch/ /\.)/
          Apr(il/ /\.)/May( /\.)/Jun(e/ /\.)/Jul(y/ /\.)/Aug(ust/ /\.)/
          Sep(tember/ /\.)/Oct(ober/ /\.)/Nov(ember/ /\.)/
          Dec(ember/ /\.)) [0-3]?[0-9]{1}(,)? ([0-9]{4}/'?[0-9]{2})"
        

Измените регулярное выражение так, чтобы оно соответствовало вашим нуждам. Всегда проще сопоставлять образцы, логически связанные со своими входными данными, нежели рассогласованные с ними. Будущие поколения возможно увидят неприменимость предыдущего регулярного выражения из-за бага - Y10K, потому что максимально возможный год, который оно может найти - 9999.

Сопоставление целых чисел

Как видно в нескольких предыдущих примерах, числовой диапазон, заключенный в скобки, - хороший способ находить числа.

Для сопоставления целых чисел любой длины за обозначением числового диапазона поставьте "+"; чтобы исключить отрицательные значения, поставьте перед обозначением диапазона специальный символ "-" (дефис):

          -?[0-9]+
        

Предыдущий пример успешно находит ноль, потому что ноль - одно из возможных значений в заданном промежутке.

Заключение выражений в круглые скобки также хорошо для сопоставления чисел. Чтобы найти любое десятичное число, расширьте предыдущее регулярное выражение специальным вложением, содержащим десятичную точку, поставленную после одного или нескольких чисел:

          -?[0-9]+(\.[0-9]+)?
        

Используйте скобки чтобы определить десятичное число с заданным количеством разрядов. Например, для поиска только положительных чисел с разрядностью 5 знаков до запятой:

          [^-][0-9]+\.([0-9]){5,}
        

Больше сопоставлений для реальных ситуаций

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

  • Для поиска телефонных номеров США используйте:
                ((\([2-9][0-9]{2}\))?\ ?/[2-9][0-9]{2}(?:\-?/\ ?))[2-9][0-9]{2}[- ]?[0-9]{4}
              

    Это регулярное выражение находит соответствия для телефонных номеров, представленных в любом из в 15 форматов:

    1. (NPA) PRE-SUFF;
    2. (NPA) PRE SUFF;
    3. (NPA) PRESUFF;
    4. (NPA)PRE-SUFF;
    5. (NPA)PRE SUFF;
    6. (NPA)PRESUFF;
    7. NPA PRE-SUFF;
    8. NPA PRE SUFF;
    9. NPA PRESUFF;
    10. NPAPRE-SUFF;
    11. NPAPRE SUFF;
    12. NPAPRESUFF;
    13. PRE-SUFF;
    14. PRE SUFF;
    15. PRESUFF;

    Оно также находит телефонные номера для беспошлинных звонков телефонной службы дальней связи (WATS) в США; хотя префикс 1 "1-" номера 1-800 или другого беспошлинного номера не являются частью сопоставляемых образцов, беспошлинный номер сопоставляется по оставшимся 10 цифрам. Точно также для номеров США, которым предшествуют 1 или 1+ и любое число пробелов, префикс для звонков дальней связи не учитывается в сопоставлении, но весь фактический номер, который следует за префиксом, учитывается этим регулярным выражением.

  • Для сопоставления адресов электронной почты, содержащих два или три доменных имени, используйте следующий код:
                \<[^@]+\>@[a-zA-Z_\.]+?\.[a-zA-Z]{2,3}
              

  • Для поиска всех современных URL-адресов, используйте регулярное выражение:
                (((http(s)?/ftp/telnet/news):///mailto:)[^\(\)[:space:]]+)
              

    Это работает достаточно хорошо, но находить соответствия для URL не так просто, как вы, возможно, подумали. Регулярное выражение, определяющее все возможные URL согласно RFC 1738, опубликовано в Regexp for URLs, и оно пугающе огромное. Это регулярное выражение должно быть помещено в класс [:url:] (было бы неплохо иметь все виды новых классов для работы с похожими типами данных, например [:email:]).

Заключение

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


Страница сайта http://www.interface.ru
Оригинал находится по адресу http://www.interface.ru/home.asp?artId=8215