Изучаем Linux, 101: Поиск в текстовых файлах с помощью регулярных выражений

Источник: IBM

Краткий обзор

Из этой статьи вы узнаете об основных приемах поиска текста в текстовых файлах с использованием регулярных выражений в Linux. Вы научитесь:

  • Строить простые регулярные выражения.
  • Выполнять поиск в файлах и файловой системе с использованием регулярных выражений.
  • Использовать регулярные выражения совместно с sed.

Эта статья поможет вам подготовиться к сдаче экзамена LPI 101 на администратора начального уровня (LPIC-1) и содержит материалы цели 103.7 темы 103. Цель имеет вес 2.

Предварительные требования

Чтобы извлечь наибольшую пользу из наших статей, необходимо обладать базовыми знаниями о Linux и иметь работоспособный компьютер с Linux, на котором можно будет выполнять все встречающиеся команды. Иногда различные версии программ выводят результаты по-разному, поэтому содержимое листингов и рисунков может отличаться от того, что вы увидите на вашем компьютере. В основе этой статьи лежит концепция, описанная в одной из предыдущих статей этой серии "Изучаем Linux 101: текстовые потоки и фильтры".

Подготовка к выполнению примеров

Для выполнения примеров в этой статье мы будем использовать некоторые файлы, созданные ранее в статье "Изучаем Linux, 101: текстовые потоки и фильтры". Если вы не читали эту статью или не сохранили файлы, не расстраивайтесь! Давайте начнем с создания новой директории lpi103-7 и всех необходимых файлов. Для этого откройте текстовое окно и перейдите в вашу домашнюю директорию. Скопируйте содержимое листинга 1 в текстовое окно; в результате выполнения команд в вашей домашней директории будет создана поддиректория lpi103-7 и в ней все необходимые файлы, которые мы и будем использовать в наших примерах.

Листинг 1. Создание файлов, необходимых для примеров этой статьи

                    
mkdir -p lpi103-7 && cd lpi103-7 && {
echo -e "1 apple\n2 pear\n3 banana" > text1
echo -e "9\tplum\n3\tbanana\n10\tapple" > text2
echo "This is a sentence. " !#:* !#:1->text3
split -l 2 text1
split -b 17 text2 y; 
cp text1 text1.bkp
mkdir -p backup
cp text1 backup/text1.bkp.2
}

Ваше окно должно выглядеть так, как показано в листинге 2, а вашей текущей рабочей директорией должна стать вновь созданная директория lpi103-7.

Listing 2. Creating the example files -- output

                    
ian@attic4:~$ mkdir -p lpi103-7 && cd lpi103-7 && {
> echo -e "1 apple\n2 pear\n3 banana" > text1
> echo -e "9\tplum\n3\tbanana\n10\tapple" > text2
> echo "This is a sentence. " !#:* !#:1->text3
echo "This is a sentence. " "This is a sentence. " "This is a sentence. ">text3
> split -l 2 text1
> split -b 17 text2 y; 
> cp text1 text1.bkp
> mkdir -p backup
> cp text1 backup/text1.bkp.2
> }
ian@attic4:~/lpi103-7$ 

Регулярные выражения

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

Регулярное выражение (другое его название - "regex" или "regexp") является способом описания текстовой строки или шаблона , позволяющим программам находить соответствие шаблону в произвольных текстовых строках и обеспечивающим исключительно мощные возможности поиска. Команда grep (акроним от g eneralized r egular e xpression p rocessor) является стандартным инструментом любого разработчика или администратора Linux или UNIX® и позволяет использовать регулярные выражения для поиска в файлах или выводе команд. В статье "Изучаем Linux, 101: текстовые потоки и фильтры" мы рассказали о sed - потоковом редакторе , который является еще одним стандартным инструментом, активно использующим регулярные выражения для поиска и замены текста в файлах или текстовых потоках. Эта статья поможет вам лучше разобраться с регулярными выражениями, с которыми работают команды grep и sed. Еще одной программой, в которой широко применяются регулярные выражения, является программа awk.

Существует множество книг, посвященных регулярным выражениям и теории языков программирования.

Когда вы узнаете о регулярных выражениях, то вы увидите сходство между их синтаксисом и метасимволами (или подстановкой имен), которые рассматриваются в другой статье этой серии "Изучаем Linux, 101: управление файлами и директориями". Имейте в виду, что это сходство всего лишь поверхностное.

Основные блоки построения выражений

В программе GNU grep, входящей в состав большинства дистрибутивов Linux, используются две формы синтаксиса регулярных выражений: основная и расширенная . Функциональность GNU grep не зависит от использования той или иной формы и одинакова в обоих случаях. В этой статье описывается основная форма синтаксиса и приводятся ее отличия от расширенной формы.

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

Конкатенация
В результате конкатенации (объединения) двух регулярных выражений мы получаем более длинное выражение. Например, в строке abcdcba для регулярного выражения a будет найдено два соответствия (первый и последний символы a); так же дело обстоит и с выражением b. Однако для регулярного выражения ab будет найдено только одно соответствие abcdcba, а для выражения ba - только соответствие abcdcba.
Повторение
Оператор Клини *, или оператор повторения соответствует предшествующему регулярному выражению, повторяющемуся 0 или более раз. Так, регулярному выражению a*b будет соответствовать любая строка, содержащая любое количество символов a и оканчивающаяся символом b, в том числе строка, содержащая один только символ b. Оператор Клини * не нужно записывать в виде escape-последовательности, поэтому если вы хотите найти в выражении буквенное значение символа звездочки (*), то этот символ должен быть записан в виде escape-последовательности. Использование символа * в данном случае отличается от его использования при подстановке имен, в которой он соответствует любой строке.
Чередование
Оператор чередования (/) определяет соответствие либо для предшествующего, либо для последующего выражения. При использовании основного синтаксиса он должен быть записан в виде escape-последовательности. Так, регулярному выражению a*\/b*c будет соответствовать любая строка, содержащая любое количество символов a или b (но не обоих одновременно) и оканчивающаяся символом c, в том числе строка, содержащая один только символ c.

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

Поиск в файлах и файловых системах

В этой статье мы будем использовать текстовые файлы, созданные нами ранее (см. раздел Подготовка к выполнению примеров). Изучите простые примеры, приведенные в листинге 3. Обратите внимание на то, что команда grep принимает регулярное выражение в качестве обязательного параметра и список из 0 или более файлов, в которых следует искать заданное выражение. Если файлы не указаны, то команда grep выполняет поиск в потоке stdin, благодаря чему, ее можно использовать в конвейерах в качестве фильтра. Если совпадений не найдено, то grep не выводит никаких результатов, однако вы всегда можете проверить код завершения.

Листинг 3. Простые регулярные выражения

                    
ian@attic4:~/lpi103-7$ grep p text1
1 apple
2 pear
ian@attic4:~/lpi103-7$ grep pea text1
2 pear
ian@attic4:~/lpi103-7$ grep "p*" text1
1 apple
2 pear
3 banana
ian@attic4:~/lpi103-7$ grep "pp*" text1
1 apple
2 pear
ian@attic4:~/lpi103-7$ grep "x" text1; echo $?
1
ian@attic4:~/lpi103-7$ grep "x*" text1; echo $?
1 apple
2 pear
3 banana
0
ian@attic4:~/lpi103-7$ cat text1 / grep "l\/n"
1 apple
3 banana
ian@attic4:~/lpi103-7$ echo -e "find an \ns* here" / grep "s\*"
s* here

Как видно из этих примеров, иногда можно получить неожиданный результат, в особенности при использовании оператора повторения. Вероятно, вы предполагали, что регулярному выражению p* или, по крайней мере, pp* будут соответствовать строки из нескольких символов p, однако выражениям p* и x* соответствуют любые строки файла; это обусловлено тем, что оператор * соответствует предшествующему регулярному выражению, повторяющемуся ноль или более раз.

В двух примерах был запрошен код завершения команды grep. Значение 0 говорит о том, что совпадение было найдено, а значение 1 - о том, что совпадений найдено не было. Значение, превышающее 1 (в GNU grep оно всегда равно 2) возвращается в случае возникновения ошибки, например, если указанный файл не существует.

Некоторые сокращения

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

+
Оператор + похож на оператор * за исключением того, что он соответствует предшествующему регулярному выражению, повторяющемуся один или более раз. При использовании основного синтаксиса он должен быть записан в виде escape-последовательности.
?
Символ ? означает, что предшествующее регулярное выражение является необязательным, т. е. он соответствует предшествующему регулярному выражению, повторяющемуся 0 или 1 раз. В данном случае значение символа ? отличается от значения соответствующего символа, использующегося при подстановке имен.
.
Символ . (точка) - это метасимвол, соответствующий любому символу. Одним из наиболее распространенных шаблонов является выражение .*, которое соответствует строке произвольной длины, содержащей любые символы (или не содержащей их вообще). Не стоит говорить о том, что такие конструкции используются как часть более сложных выражений. Сравните регулярное выражение, состоящие из одной точки, со знаком ? в подстановке имен, а также регулярное выражение .* с символом * в подстановке имен.

Листинг 4. Другие регулярные выражения

                    
ian@attic4:~/lpi103-7$ grep "pp\+" text1 # at least two p's
1 apple
ian@attic4:~/lpi103-7$ grep "pl\?e" text1
1 apple
2 pear
ian@attic4:~/lpi103-7$ grep "pl\?e" text1 # pe with optional l between
1 apple
2 pear
ian@attic4:~/lpi103-7$ grep "p.*r" text1 # p, some string then r
2 pear
ian@attic4:~/lpi103-7$ grep "a.." text1 # a followed by two other letters
1 apple
3 banana

Соответствие началу или концу строки

Символ вставки ^ означает начало строки, а символ доллара $ - конец строки. Таким образом, регулярное выражение ^..b соответствует двум любым символам в начале строки, после которых следует символ b, а выражение ar$ - любой строке, оканчивающейся на ar. Регулярное выражение ^$ соответствует пустой строке.

Более сложные выражения

До сих пор мы применяли оператор повторения к отдельному символу. Если вам необходимо найти все совпадения для строки из нескольких символов, например, найти все вхождения an в слове banana, то используйте круглые скобки (при использовании основного синтаксиса они должен быть записаны в виде escape-последовательностей). Точно так же, может возникнуть необходимость найти несколько символов, не используя такие общие или длинные конструкции, как . или операторы чередования. В этом случае вы можете заключить все возможные значения в квадратные скобки ([]), которые не нужно оформлять в виде escape-последовательностей при использовании основной формы синтаксиса. Выражения в квадратных скобках образуют класс символов . Квадратные скобки (за исключением отдельных случаев, о которых будет рассказано позже) также избавляют от необходимости использовать escape-последовательности для специальных символов, таких как . и *.

Листинг 5. Круглые скобки и классы символов

                    

ian@attic4:~/lpi103-7$ grep "\(an\)\+" text1 # find at least 1 an
3 banana
ian@attic4:~/lpi103-7$ grep "an\(an\)\+" text1 # find at least 2 an's
3 banana
ian@attic4:~/lpi103-7$ grep "[3p]" text1 # find p or 3
1 apple
2 pear
3 banana
ian@attic4:~/lpi103-7$ echo -e "find an\ns* here\nsomewhere." / grep "s[.*]"
s* here
ian@attic4:~/lpi103-7$ echo -e "find an\n * in position 2." / grep ".[.*]"
 * in position 2.


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

Диапазон
Диапазон - это два символа, разделенные дефисом (-), например, 0-9 (десятичные цифры) или 0-9a-fA-F (шестнадцатеричные цифры). Обратите внимание на то, что диапазоны зависят от используемой локали.
Именованные классы
Существует несколько именованных классов для удобного обозначения широко используемых классов. Именованные классы начинаются с [: и оканчиваются на :], и могут быть использованы в выражениях, заключенных в квадратные скобки. Приведем несколько примеров:
[:alnum:]
Все алфавитно-цифровые символы.
[:blank:]
Пробел и символы табуляции.
[:digit:]
Цифры от 0 до 9 (эквивалентно диапазону 0-9).
[:upper:] and [:lower:]
Буквы в верхнем и нижнем регистрах соответственно.
^ (отрицание)
Если класс символов начинается с символа вставки ^ (т. е. символ ^ стоит сразу же после открывающей квадратной скобки), то регулярному выражению будут соответствовать значения, содержащие любые символы (за исключением первого символа ^), кроме перечисленных в классе после символа ^.

Принимая во внимание все вышесказанное, можно сделать следующие выводы: если вы хотите найти совпадение с буквенным значением дефиса (-), содержащегося в классе символов, то вы должны поставить его либо первым, либо последним; если вы хотите найти буквенное значение символа ^, то он не должен стоять первым; символ ] (правая квадратная скобка) закрывает класс, если она не стоит первой.

Классы символов - это единственная область, в которой не существует различий между регулярными символами и подстановкой имен, хотя символы отрицания все-таки различаются (^ и !). В листинге 6 приведены некоторые примеры работы с классами символов.

Листинг 6. Другие классы символов

                    
ian@attic4:~/lpi103-7$ # Match on range 3 through 7
ian@attic4:~/lpi103-7$ echo -e "123\n456\n789\n0" / grep "[3-7]"
123
456
789
ian@attic4:~/lpi103-7$ # Find digit followed by no n or r till end of line
ian@attic4:~/lpi103-7$ grep "[[:digit:]][^nr]*$" text1
1 apple
ian@attic4:~/lpi103-7$ # Find a digit, n, or z followed by no n or r till end of line
ian@attic4:~/lpi103-7$ grep "[[:digit:]nz][^nr]*$" text1
1 apple
3 banana

Вас не удивил последний пример? В этом случае первое выражение в квадратных скобках соответствует любой цифре либо буквам n или z в строке, и после последней буквы n не следует другая буква n или r, поэтому завершающая подстрока na соответствует регулярному выражению.

Какие совпадения были найдены?

Вы можете задать значение переменной окружения GREP_COLORS для выделения цветом найденных совпадений. При использовании значения по умолчанию найденные совпадения выделяются красным жирным шрифтом, как показано на рисунке 1. Из этого рисунка видно, что с регулярным выражением совпала вся первая строка вывода и два последних символа во второй строке.

Рисунок 1. Использование цвета для совпадений grep

Рисунок 1. Использование цвета для совпадений grep

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

Расширенная форма регулярных выражений

Расширенная форма синтаксиса регулярных выражений является расширением GNU. Она избавляет от необходимости записывать некоторые символы (включая круглые скобки, символы '?', '+', '/' и '{') в виде escape-последовательностей, как это требуется при использовании основной формы синтаксиса. Обратная сторона заключается в том, что вы должны записывать их в виде escape-последовательностей в тех случаях, когда вам требуется, чтобы они интерпретировались в вашем регулярном выражении как обычные буквенные символы. Для указания того, что вы используете расширенную форму синтаксиса регулярных выражений, вы можете использовать опцию -E (или --extended-regexp) команды grep. Другой способ переключиться в расширенный режим - использовать команду egrep. В листинге 7 приведен пример, который мы уже видели в этом разделе, но в нем используется расширенная форма синтаксиса и команда egrep.

Листинг 7. Расширенные регулярные выражения

                    
ian@attic4:~/lpi103-7$ # Find b followed by one or more an's and then an a
ian@attic4:~/lpi103-7$ grep "b\(an\)\+a" text1 
3 banana
ian@attic4:~/lpi103-7$ egrep "b(an)+a" text1
3 banana

Поиск в файлах

Теперь, когда вы знакомы с основными командами, давайте посмотрим, как использовать всю мощь команд grep и find для поиска информации в файловой системе. В наших простых примерах мы будем использовать файлы, созданные ранее в статье "Изучаем Linux 101: текстовые потоки и фильтры" или в директории lpi103-7 и ее поддиректориях (см. раздел Подготовка к выполнению примеров). В первом случае у вас будут дополнительные файлы, и, соответственно, вы получите дополнительные результаты.

Во-первых, команда grep может выполнять поиск сразу в нескольких файлах. Если вы добавите опцию -n, то команда покажет вам номера строк, в которых были найдены совпадения. Если вы просто хотите знать количество строк с совпадениями, то используйте опцию -c, а если вам нужен список файлов, содержащих совпадения, используйте опцию -l. В листинге 8 приведены некоторые примеры.

Листинг 8. Применение grep к нескольким файлам

                    
ian@attic4:~/lpi103-7$ grep plum *
text2:9	plum
yaa:9	plum
ian@attic4:~/lpi103-7$ grep -n banana text[1-4]
text1:3:3 banana
text2:2:3	banana
ian@attic4:~/lpi103-7$ grep -c banana text[1-4]
text1:1
text2:1
text3:0
ian@attic4:~/lpi103-7$ grep -l pear *
text1
text1.bkp
xaa

В примере с использованием опции -c в листинге 8 есть строка text3:0. Часто может возникать необходимость узнать, сколько тех или иных совпадений содержится в файле, но при этом не требуется знать о файлах, в которых этих совпадений нет. У команды grep имеется опция -v, которая отображает вывод только для тех строк, которые не содержат совпадений. Таким образом, можно использовать регулярное выражение :0$ для поиска строк, оканчивающихся на :0.

Это мы и сделаем в нашем следующем примере, используя команду find для поиска всех файлов в текущей директории и ее поддиректориях и команду xargs для передачи списка файлов команде grep с целью определения количества найденных слов banana в каждом файле. Наконец, мы сортируем полученный вывод с помощью дополнительного вызова команды grep c опцией -v для поиска всех строк, не оканчивающихся на:0, и получаем количество файлов, содержащих слово banana.

Листинг 9. Поиск файлов, содержащих, как минимум, одно слово banana

                    
ian@attic4:~/lpi103-7$ find . -type f -print0/ xargs -0 grep -c banana/ grep -v ":0$"
./backup/text1.bkp.2:1
./text2:1
./text1:1
./yaa:1
./xab:1
./text1.bkp:1

Регулярные выражения и редактор sed

В статье "Изучаем Linux 101: текстовые потоки и фильтры" мы рассказывали о потоковом редакторе sed и упоминали о том, что sed использует регулярные выражения. Регулярные выражения могут использоваться как в адресных выражениях, так и в выражениях замены.

Если вы просто что-то ищете, то, вероятно, вам подойдет команда grep. Если же вам необходимо извлечь строку поиска (или связанную с ней строку) из списка всех найденных строк для дальнейшей обработки, то для этого можно использовать редактор sed. Итак, давайте выясним, как это работает. Для начала напомним вам, что наши файлы text1 и text2 содержали номера и имена фруктов, разделенные пробелами и символами табуляции, а файл text3 - повторяющееся предложение. Содержимое этих трех файлов представлено в листинге 10.

Листинг 10. Содержимое файлов text1, text2 и text3

ian@attic4:~/lpi103-7$ cat text[1-3]
1 apple
2 pear
3 banana
9	plum
3	banana
10	apple
This is a sentence.  This is a sentence.  This is a sentence. 

Сначала мы используем команды grep и sed для извлечения строк, начинающихся с одной или нескольких цифр, после которых следует символ-разделитель (пробел или табуляция). В обычном режиме sed выводит все строки, поэтому мы используем его опцию -n, чтобы избежать этого, а затем используем команду p редактора sed для вывода только тех строк, которые соответствуют нашему регулярному выражению. Чтобы быть уверенными, что мы используем в обоих случаях одно и то же регулярное выражение, мы присвоили его переменной.

Листинг 11. Поиск с использованием grep и sed

ian@attic4:~/lpi103-7$ grep "$oursearch" text[1-3]
text1:1 apple
text1:2 pear
text1:3 banana
text2:9	plum
text2:3	banana
text2:10	apple
ian@attic4:~/lpi103-7$ cat text[1-3] / sed -ne "/$oursearch/p"
1 apple
2 pear
3 banana
9	plum
3	banana
10	apple

Заметим, что команда grep отображает имена файлов только в тех случаях, когда поиск выполняется в нескольких файлах. Поскольку для передачи входных данных редактору sed мы использовали команду cat, то sed ничего не знает об именах исходных файлов. Тем не менее, совпадения строк идентичны в обоих случаях, как мы и ожидали.

Теперь предположим, что нас интересует только первое слово из каждой найденной строки. В нашем случае этим словом является название фрукта, но мы могли бы искать URL-адреса, имена файлов или что-нибудь еще. В нашем примере нам будет достаточно удалить из строк те фрагменты, которую будут соответствовать нашему поиску. Давайте сделаем это, как показано в листинге 12.

Листинг 12. Удаление нумерации строк с помощью sed

ian@attic4:~/lpi103-7$ cat text[1-3] / sed -ne "/$oursearch/s/$oursearch//p"
apple
pear
banana
plum
banana
apple
            

В нашем последнем примере предположим, что после имени фрукта в наших строках могут содержаться другие данные. Мы добавим строку "lemon pie" (лимонный пирог) к нашим данным и посмотрим, как можно избавиться от лимона. Также мы отсортируем вывод и избавимся от повторяющихся значений. В итоге мы получим список найденных фруктов, не содержащий повторений.

В листинге 13 представлены два способа решения этой задачи. В первом из них мы удалили из каждой строки нумерацию и символы-разделители, а затем удалили все данные после первого пробела (или символа табуляции) и напечатали все, что осталось. Во втором примере мы использовали круглые скобки, чтобы разбить всю строку на три части (число и следующий за ним знак-разделитель, второе слово и все остальное). Затем мы используем команду s для замены всей строки одним только вторым словом и выводим результат. Вы можете попробовать самостоятельно выполнить этот пример, опустив третью часть, \(.*\), и попытаться объяснить полученный результат.

Листинг 13. Окончательный список фруктов

                    
ian@attic4:~/lpi103-7$ echo "7 lemon pie" / cat - text[1-3] /
> sed -ne "/$oursearch/s/\($oursearch\)\([^[:blank:]]*\)\(.*\)/\2/p" /
> sort / uniq
apple
banana
lemon
pear

Некоторые более ранние версии sed не поддерживают использование расширенной формы синтаксиса регулярных выражений. Если вы работаете с такой версией, то используйте опцию -r, чтобы указать sed на то, что вы используете расширенную форму синтаксиса. В листинге 14 показано, что необходимо изменить в переменной oursearch и команде sed для выполнения тех же действий с расширенными регулярными выражениями, что были выполнены в листинге 13 с основными регулярными выражениями.

Листинг 14. Использование расширенной формы синтаксиса регулярных выражений в sed

                    
ian@attic4:~/lpi103-7$ echo "7 lemon pie" / cat - text[1-3] /
> sed -nre "/$oursearchx/s/($oursearchx)([^[:blank:]]*)(.*)/\2/p" /
> sort / uniq
apple
banana
lemon
pear
plum
            

Эта статья лишь частично затронула вопросы работы с регулярными выражениями в командной строке Linux с использованием grep и sed. Если вы хотите узнать больше об этих неоценимых инструментах, обратитесь к соответствующим man-страницам.


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