- Подписка на печатную версию:
- Подписка на электронную версию:
- Подшивки старых номеров журнала (печатные версии)
LXF80:Регулярные выражения
Материал из Linuxformat.
HARDCORE LINUX УЧЕБНИК ДЛЯ ПРОДВИНУТЫХ ПОЛЬЗОВАТЕЛЕЙ
Содержание |
Обработка текста: регулярные выражения
Освоив регулярные выражения, принимайтесь за изучение геномов с д-ром Крисом Брауном (Chris Brown). А может, и без него.
Доброе утро. Милости просим на мастер-класс по регулярным выражениям. От вас потребуется внимание, так что не болтайте там сзади, не валяйте дурака и повремените с чтением почты. Прежде чем начать, я хочу убедиться, что все могут найти обратную косую черту на клавиатуре. Все? И фигурные скобки тоже? Хорошо. Итак, приступим.
Регулярные выражения (также известные как регексы – от англ. regular expressions) – это способ написания определенных текстовых шаблонов. Они существуют в Linux (а до этого в Unix) уже давно. Самое раннее упоминание о них мне удалось найти в man-странице древнего редактора Ed, входившей в справочник по шестой редакции Unix. Этот документ датирован 15 января 1973 года, так что сами регулярные выражения и того древнее. Регекспы используются во многих Linux-утилитах: они служат для поиска и замены в редакторах, как типа Vi, так и посолиднее, OpenOffice.org; их можно найти в фильтрах вроде grep, sed и awk, они применяются в Apache для перезаписи URL и обеспечивают мощь языков Perl и PHP. И есть множество библиотек для использования регулярных выражений в разных языках программирования, вплоть до ASP.NET.
Регулярные выражения приспосабливаются под самые разные вещи. С ними вы сможете:
• Удалять комментарии из файлов конфигурации.
• Находить пустые параграфы в документах OpenOffice.org.
• Проверять, что указанная строка есть корректный IP-адрес.
• Извлекать адреса электронной почты из текстовых файлов.
• Выделять год из строки с датой.
• Искать палиндромы в списке слов.
• Искать специфические участки ДНК в геноме.
Насчет последнего примера: существует целая индустрия, применяющая Perl и регулярные выражения для обработки биоинформации.
(К сожалению, с генетикой я вам не помогу – я доктор в области теоретической физики. Но у O'Reilly есть три книги по этому вопросу!) Для начала следует осознать, что регулярные выражения отнюдь не то же самое, что и безразличные символы (wildcards), распознаваемые командными оболочками. Как пример последних, приведу такую строку:
rm tutorial*.v[0-2]
Оболочка будет искать файлы, соответствующие данному шаблону – их имена начинаются на tutorial и заканчиваются на v0, v1 или v2.
Регулярные выражения и универсальные (безразличные) знаки записываются в том числе и общими метасимволами (например, * и []), но смысл их различен, как и контекст их применения. Универсальные замещающие знаки обычно используются для поиска имен файлов, а регулярные выражения – для выбора текста в файле или строке, обрабатываемой программой.
Впрочем, перейдем к мастер-классу. Буду считать, что вы хорошо знакомы хотя бы с утилитой grep. По умолчанию она выводит строки, соответствующие некому выражению. Например, команда
grep xen ~/book/chapter5.txt
выведет все строки файла chapter5.txt, где обнаружится слово xen. Буквы здесь не имеют особого значения – это просто набор литер x, e и n. Регулярное выражение правильное, но очень простенькое: искать фиксированные строки таким способом нет смысла. Допустим теперь, что вы хотите проверить несколько файлов, чтобы узнать, в каких содержится слово xen. Достаточно будет следующей команды:
grep -l xen ~/book/chapter[0-9].txt
Опция -l говорит «не надо мне строк – покажите имена файлов, содержащих данную строку». Учтите, что строка ~/book/chapter[0- 9].txt здесь – не регулярное выражение grep, а шаблон файлов, обрабатываемых оболочкой.
Возьмемся за метасимволы
Пора познакомиться с настоящими регулярными выражениями – вы должны были уже проснуться. Приведенная ниже таблица содержит элементы регулярных выражений, которыми мы займемся. Рассмотрим выражение:
grep -l '[Xx]en' ~/book/chapter[0-9].txt
Заметьте, я поместил регулярное выражение в одинарные кавычки для того, чтобы оболочка не интерпретировала [Xx] как замещение имени файла. Мы ведь хотим передать аргумент [Xx]en в grep, чтобы именно grep обработал [].
Употребление метасимволов, интерпретируемых как надо – почти искусство, и без него в мире синтаксиса программных оболочек и регулярных выражений не прожить. Можно составить выражение, соответствующее любой заглавной гласной букве ([AUOIE]) или цифре ([0123456789] – охватить все цифры можно и короче - [0-9]; аналогично описываются все малые латинские буквы – [a-z]).
Имейте в виду: что бы ни было помещено внутрь квадратных скобок, соответствует оно только одному символу в тексте. Добавление ^ в начале списка символов инвертирует смысл выражения, то есть регексу [^0-9] соответствует любой символ, кроме цифры.
Символы ^ и $ привязывают искомое значение к началу и концу строки соответственно. Например, выражению ^login соответствует любая строка, начинающаяся со слова login, а [0-9]$ – любая строка, оканчивающаяся цифрой.
Поясню на примере. Есть множество файлов конфигурации, где строки комментариев начинаются с символа #. Иногда в гуще комментариев трудно отыскать нужные параметры. Регулярное выражение отберет для вас незакомментированные строки командой:
grep -v '^#'
Опция -v велит grep печатать только те строки, которые НЕ соответствуют указанному регулярному выражению. То есть мы сказали grep: "не печатай строки, начинающиеся с #". Имеется и альтернативное решение:
grep '^[^#]' /etc/ssh/ssh_config
Оно говорит "печатай строки, не начинающиеся с #". Попробовав воспроизвести оба примера, вы увидите, что они не эквивалентны. Отличие – в отношении к пустым строкам. Ни одно из приведенных регулярных выражений не соответствует пустой строке, однако в первом варианте пустые строки отображаются, а во втором – нет. Обратите внимание, что во втором варианте символы ^ обозначают совершенно разные вещи.
Кстати о пустых строках. Им соответствует регекс ^$. Так, команда
grep -v '^$' somefile
выведет все непустые строки файла.
Расширьте свои знания
Одна из вещей, отличающих любителя от профессионала, заключается в умении отличить простое регулярное выражение от расширенного. Когда-то grep распознавал лишь простые регексы, а для расширенных имелась отдельная утилита, egrep. Зато GNU-версия grep распознает оба типа; по умолчанию она ищет только простые регексы, но если указать ключ -E, то grep заработает и с составными выражениями. Например, обычные круглые скобки ( и ) не имеют какого-то особого смысла в простых регексах – они обозначают просто самих себя. В расширенных же регексах круглые скобки используются для группировки частей регулярного выражения, как в арифметике – (a+b)*c.
Чтобы расширенный регекс понимал символы круглых скобок как таковые, перед ними ставится обратный слэш – \( будет соответствовать открывающей скобке. Если вы еще не совсем запутались, добавлю, что некоторые программы (включая GNU-версию grep) по умолчанию используют простые регексы, где обратный слэш как раз включает особое значение символа. Так что приходится постоянно держать в голове: включает обратный слэш специальное значение какого-либо символа или отключает его.
Далее в таблице идут модификаторы-повторители – внимание! Сам по себе такой модификатор ничему не соответствует, он лишь указывает, сколько раз предшествующее регулярное выражение должно повторяться. Например, [0-9]* соответствует нулю или более цифр, а [A-Z]+ – одной или более заглавной латинской букве. Очень часто встречающийся пример: .* обозначает «любое количество чего угодно». Так, выражение login.*failed соответствует любой строке, содержащей слово login, за которым через любое количество вторгающихся символов следует слово failed [в предыдущей фразе содержится распространенная неточность. Вместо «...слово failed» следует читать «последовательность символов failed», то есть строки «login failed», «login abcfailed» и даже «login_not_failed» одинаково удовлетворяют шаблону, – прим.ред.].
Регекспы шаг за шагом
Для упражнения в использовании повторяющих модификаторов создадим регулярное выражение, распознающее IP-адреса, типа 192.168.0.42. Правильный IP-адрес описывается как «четыре десятичных числа от 0 до 255, разделенные точками». К сожалению, мы не можем напрямую перевести это описание на язык регулярных выражений, так как в них невозможны арифметические сравнения. В первом приближении скажем так: «IP-адрес – это четыре десятичных числа длиной от одной до трех цифр, разделенных точками».
Это действительно соответствует реальным IP-адресам, а заодно и несуществующим, вроде 123.456.789.0. Что ж, давайте построим регекс шаг за шагом. Во-первых, [0-9]{1,3} соответствует числу из одной-трех цифр. Для добавления литерала . (точка) нужно предварить его косой чертой, чтобы отключить его специальное значение: [0-9]{1,3}\.
Далее, нужно сказать: «Повторить всю группу три раза». Приставим еще один повторитель: [0-9]{1,3}\.{3}. Однако это выражение не будет работать так, как нам надо, поскольку повторитель применяется только к последнему символу (здесь это .). Значит, надо окружить повторяющееся выражение круглыми скобками: ([0-9]{1,3}\.){3}. Помните, что используя grep без ключа -E (то есть в режиме простых регексов), вы должны включать специальное значение скобок, предварив каждую из них обратным слэшем: \([0-9]\{1,3\}\.\)\{3\}. Наконец, завершим выражение, приставив описание последней группы цифр IP-адреса, следующим образом: ([0-9]{1,3}\.){3}[0-9]{1,3}. Головка не болит?
Извлечение адресов e-mail
Мы созрели для примера посложнее. Вот задача: у меня есть множество текстовых файлов, содержащих адреса электронной почты. Сам адрес может находиться внутри другого текста, например так: «Спроси Андрея (andy@example.net), знает ли он...» Я хочу извлечь все уникальные адреса из этих файлов. Разобьем задачу на четыре шага:
- Шаг 1: Опишем обычным человеческим языком, как выглядит адрес электронной почты.
- Шаг 2: Создадим регекс, соответствующий описанию адреса.
- Шаг 3: Воспользуемся grep с этим регексом для получения списка адресов.
- Шаг 4: Напишем небольшую обертку для представления законченного решения.
Сложнее всего первый шаг. Вот простые попытки формализовать адрес электронной почты:
- Строка из одного или более заглавного или строчного литерала (имя пользователя)
- Литерал @
- Один или более доменов. Каждый домен – это строка литералов, оканчивающаяся точкой
- Домен первого уровня, содержащий две или три буквы
Теперь перейдем ко второму шагу – поочередному переводу этого описания на язык регулярных выражений. Первая часть адреса переводится как [a-zA-Z]+. Вторая – как @ (это не метасимвол, он просто обозначает сам себя).
Над третьей частью придется призадуматься. Регекс, соответствующий одному домену, может быть таким: [a-zA-Z]+\. – сюда попадают все слова с точкой, например, example. (здесь \ используется для описания точки). Однако доменов бывает и несколько, поэтому нужно добавить сюда +, после чего получится ([a-zA-Z]+\.)+. Этому выражению соответствует и example, и foo.example, и foo.bar.example.
Четвертая часть требует повторителей (см. таблицу) и может быть записана как [a-zA-Z]{2,3}.
Соединив все части, получим [a-zA-Z]+@([a-zA-Z]+\.)+[a-zA-Z]{2,3}.
Конечно, вариант у нас упрощенный, ведь имена и домены могут содержать цифры и другие символы. Кроме того, наше выражение включает адреса с несуществующими доменами вроде xyz: andy@example.xyz. [Более того, строго поставленная задача поиска адресов электронной почты в принципе не разрешима с помощью регулярных выражений. Однако в практических целях этим можно пренебречь, – прим.ред.]
Обычный подход – начать с более или менее работающего регекса, а затем немного его подправить. Как правило, первые 10% усилий затрачиваются на выражение, распознающее 90% случаев. Остальные усилия тратятся на оставшиеся 10%.
Согласно шагу 3 мы должны подсунуть наше выражение утилите grep и обработать все текстовые файлы (оканчивающиеся на .txt):
grep '[a-zA-Z]+@([a-zA-Z]+\.)+[a-zA-Z]{2,3}' *.txt
Однако это еще далеко не все. Во-первых, необходимо добавить опцию -E, поскольку мы писали регекс в расширенном формате. Во-вторых, grep обычно выводит строки, содержащие участки, удовлетворяющие шаблону; чтобы показывался только адрес (то есть часть, соответствующая регексу), нужно передать ему параметр -o. В-третьих, при обработке множества файлов grep будет печатать имя файла перед каждым совпадением. Для отключения вывода имени файла есть опция -h.
Вот что получится в итоге:
grep -E -o -h '[a-zA-Z]+@([a-zA-Z]+\.)+[a-zA-Z]{2,3}' *.txt
Эта команда выдаст список вроде следующего:
santa@northpole.com isaac@trinity.cam.ac.uk jrrtolkien@exeter.ox.ac.uk bill@millisoft.uk.com isaac@trinity.cam.ac.uk bill@MILLISOFT.UK.COM
Я умышленно составил такой список, чтобы показать ряд возможных проблем. Во-первых, домены регистронезависимы, и иногда их пишут заглавными буквами. Нельзя считать два адреса разными только потому, что они отличаются регистром. Во-вторых, адрес может встретиться в списке не один раз.
Небольшая пост-обработка
Перейдем к последнему шагу 4. Регулярных выражений он не касается: просто приведем полученные результаты к удобочитаемому виду. Для начала переведем все адреса в нижний регистр. К счастью, это легко делается с помощью tr:
grep -E -o -h '[a-zA-Z]+@([a-zA-Z]+\.)+[a-zA-Z]{2,3}' *.txt | tr A-Z a-z
Хотя легко предположить, что параметры tr – регулярные выражения, это не так. Мы просто приказываем tr преобразовать символы A-Z в a-z, то есть привести их к нижнему регистру.
Следующий шаг – сортировка. Направим вывод в sort:
grep -E -o -h '[a-zA-Z]+@([a-zA-Z]+\.)+[a-zA-Z]{2,3}' *.txt | tr A-Z a-z | sort
Теперь наш список выглядит так:
bill@millisoft.uk.com bill@millisoft.uk.com isaac@trinity.cam.ac.uk isaac@trinity.cam.ac.uk jrrtolkien@exeter.ox.ac.uk santa@northpole.com
Наконец, воспользуемся uniq для удаления повторяющихся строк. Вот окончательный ответ:
grep -E -o -h '[a-zA-Z]+@([a-zA-Z]+\.)+[a-zA-Z]{2,3}' *.txt | tr A-Z a-z | sort | uniq
А вот и результат:
bill@millisoft.uk.com isaac@trinity.cam.ac.uk jrrtolkien@exeter.ox.ac.uk santa@northpole.com
Теперь вы знаете, как пользоваться регулярными выражениями в grep и делать простую обработку результатов. Если вы все поняли – поздравляем! Вы заслужили погоны специалиста по регексам.
БЕЗУПРЕЧНАЯ ЛОГИКА РЕГЕКСОВ
Не очевидно, почему [^#] не соответствует пустой строке. Но ведь пустые строки не начинаются с #, верно? Фактически, после начала строки должен быть хотя бы один символ, чтобы сравнить его с #.
ЖАДНЫЕ РЕГУЛЯРНЫЕ ВЫРАЖЕНИЯ
Да, регексы очень жадные – они употребляют строку до последнего символа. Точнее, они прикладывают шаблон к самому левому вхождению и прихватывают по максимуму.
Рассмотрим действие регекса t+ на строку 'aatttaatttttaa'. t+ соответствует 't', 'tt', 'ttt' и так далее. В соответствии с правилами, сравнение начнется с первого t в строке и продолжится сколько сможет. Таким образом, результат – первая подстрока 'ttt', хотя дальше есть более длинная последовательность из t.
Если нас устраивает обычное поведение grep, то есть вывод строк, содержащих совпадения, то никаких проблем не возникает, нам важен только факт наличия. Однако если требуется заместить подстроку, соответствующую регексу, все усложняется.
Например, нужно добыть имена пользователей из файла /etc/passwd. Каждая строка этого файла отвечает за одну учетную запись; строки имеют следующий вид:
chris:x:1000:100:Chris Brown:/home/chris:/bin/bash
Наша задача – ликвидировать все, что после двоеточия. Полагаясь на алчность регекспов, можно сделать это командой
sed 's/:.*//' /etc/passwd
Здесь старый шаблон – :.*, а новый пуст. Таким образом, удалится все, что
соответствует регекспу, то есть от первого двоеточия до конца строки.
13 САМЫХ ИСПОЛЬЗУЕМЫХ РЕГУЛЯРНЫХ ВЫРАЖЕНИЙ
Это список наиболее привычных выражений, которые мы использовали в наших уроках
Выражение | Описание | Пример | Что означает пример |
---|---|---|---|
a | Обычные символы обозначают сами себя | apple | Строка «apple» |
[...] | Любой символ, заключенный в [] | [02468] | Любая из цифр 0,2,6,8 |
[^...] | Любой символ, не заключенный в [] | [^13579] | Любой символ кроме нечетных цифр |
[x-x] | Диапазон символов | [A-Z] | Любая большая латинская буква |
. | Любой одиночный символ | c.t | cut, cat, c9t и т.д. |
^ | Начало строки | ^[0-9] | Строки, начинающаяся с цифры |
$ | Конец строки | /bin/sh$ | Строки, кончающиеся /bin/sh |
* | Ноль или больше предшествующих символов | [a-z]* | Любая последовательность символов в нижнем регистре или ничего |
? | Ноль или один предшествующий символ | https?:// | http:// и https:// |
+ | Один или больше предшествующих символов | T+ | T, TT, TTT, TTTT и так далее |
{n} | n повторов предыдущих символов | [0-9]{3} | Последовательность из трех цифр, например 124, 111, 743 |
{n,} | n или больше предыдущих символов | 0{3,} | 000, 0000, 00000 и т.д. |
{n,m} | От n до m предыдущих символов | [A-Z]{2,3} | Строки вроде AB, ABC, YY, ZZZ |