ZaLinux.ru

Как отфильтровать текст, находящийся между двумя определёнными строками

Когда нам нужно найти строку, соответствующую определённому образцу, нам на помощь приходит команда grep и регулярные выражения. А что если нам нужно найти то, что находится между совпадением первого шаблона и между совпадением второго шаблона? То есть нам нужно найти содержимое находящееся между двумя определёнными строками.

Как найти текст, который начинает и заканчивается с определённых строк

Допустим, в HTML коде есть конструкция:

<div class="onp-locker-call…..
	здесь интересующие нас строки
</div>

Строка <div class="onp-locker-call….. может иметь различные варианты например:

<div class="onp-locker-call" style="display: none;" data-lock-id="onpLock443607">

Или так:

<div class="onp-locker-call" style="display: none;" data-lock-id="onpLock781340">

Но суть в любом случае одна — нам нужно найти фрагмент, начинающийся и заканчивающийся с определённых строк. При этом мы не знаем точное количество строк и, возможно, окаймляющие строки также могут различаться (то есть поиск выполняется по регулярному выражению).

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

Если нам нужно найти строки вида:

<div class="onp-locker-call…..
	здесь интересующие нас строки
</div>

Тогда команда sed будет следующей:

sed -n '/<div class="onp-locker-call/,/<\/div>/p' ФАЙЛ

Рассмотрим чуть более простой пример, имется файл со следующим содержимым:

zdk
aaa
b12
cdn
dke
kdn

Мне нужно найти содержимое между любыми произвольными строками.

Допустим, я хочу найти содержимое между строками aaa и cdn, то есть я должен получить:

aaa
b12
cdn

Или я хочу найти содержимое между строками zdk и dke, то есть вывод должен быть таким:

zdk
aaa
b12
cdn
dke

Каким образом добиться этого?

Нужно задействовать команду sed, использующую выражение диапазонов.

Синтаксис запуска:

sed -n '/НАЧАЛО_ДИАПАЗОНА/,/КОНЕЦ_ДИАПАЗОНА/p' ФАЙЛ

Для указанных выше примеров запуск команды такой:

sed -n '/aaa/,/cdn/p' ФАЙЛ
aaa
b12
cdn

Для второго случая:

sed -n '/zdk/,/dke/p' ФАЙЛ
zdk
aaa
b12
cdn
dke

Использование опции -n подавляет автоматический вывод, то есть будут напечатаны только строки, для которых это явно запрошено. То есть это случиться когда будет найден диапазон /aaa/,/cdn/.

Эти выражения диапазонов также доступны в awk, там вы можете сказать:

awk '/zdk/,/dke/' ФАЙЛ

Конечно, все эти условия могут быть развёрнуты в более строгие выражения схожие с регулярными выражениями, к примеру:

sed -n '/^aaa$/,/^cdn$/p' ФАЙЛ

Это позволит проверять, что строки состоят из точных совпадений aaa и cdn и ничего более.

Показанные два примера можно скомпоновать в одну единственную команду sed с более сложным синтаксисом:

sed -n '
    /^aaa$/,/^cdn$/w output1
    /^zdk$/,/^dke$/w output2
    ' ФАЙЛ

Так что там с командой grep?

С командой grep конструкция для данного примера выглядела бы так:

grep -o "aaa.*cdn" <(paste -sd_ ФАЙЛ) | tr '_' '\n'

В grep можно добиться множественного совпадения, но нужно использовать такой вариант grep как perl-regexp (то есть добавить опцию -P, которая поддерживается не на всех платформах, например, на OS X), поэтому в качестве рабочего решения мы заменяем новые строки на символ _ и после grep меняем их обратно.

В качестве альтернативы можно использовать pcregrep, которая поддерживает многостроковые шаблоны (опция -M).

Или используйте ex:

ex +"/aaa/,/cdn/p" -scq! ФАЙЛ

Рассмотрим ещё один пример поиска фрагментов из нескольких строк

Допустим имеется файл со следующим модержимым:

kkkkkkkkkkk
jjjjjjjjjjjjjjjjjj
gggggggggggg/CK
JHGHHHHHHHH
HJKHKKLKLLL
JNBHBHJKJJLKKL
JLKKKLLKJLKJ/D
GGGGGGGGGGGGGG
GGGGGGGGGGGGGG

Мне хочется, чтобы были выбраны строки начиная с CK с конца строки и поиск совпадений был остановлен, когда строка на конце имеет D.

То есть должно быть выведено:

gggggggggggg/CK
JHGHHHHHHHH
HJKHKKLKLLL
JNBHBHJKJJLKKL
JLKKKLLKJLKJ/D

Лучше использовать awk или sed:

awk '/CK$/,/D$/' file.txt

ИЛИ:

sed -n '/CK$/,/D$/p' file.txt

Если хочется именно grep, то для GNU grep это делается следующим образом:

grep -oPz '(?s)(?<=\n)\N+CK\n.*?D(?=\n)' file.txt

Здесь:

  • -P активирует perl-regexp
  • -z устанавливает разделитель строк на NUL. Это принуждает grep видеть весь файл как одну строку
  • -o печать только совпадающей части
  • (?s) активирует PCRE_DOTALL, поэтому . любые символы или newline
  • \N совпадает со всем, кроме newline
  • .*? находит . в режиме nongreedy (не жадный)
  • (?<=..) это look-behind (смотреть после) выражения
  • (?=..) это look-ahead (смотреть до) выражения

Рассмотрим ещё пример.

Как с awk или sed выбрать строки между двумя шаблонами, которые могут встречаться несколько раз

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

Например, допустим есть файл, содержащий:

abc
def1
ghi1
jkl1
mno
abc
def2
ghi2
jkl2
mno
pqr
stu

Начальным паттерном является abc, а конечным паттерном является mno, мне нужно, чтобы вывод был таким:

def1
ghi1
jkl1
def2
ghi2
jkl2

Есть ли способ в sed или awk сделать так, чтобы находилось не единичное совпадение, а чтобы поиск повторялся пока не будет достигнут конец файла?

Решение:

Нужно использовать awk с флагом, который будет запускать вывод когда необходимо:

awk '/abc/{flag=1;next}/mno/{flag=0}flag' ФАЙЛ
def1
ghi1
jkl1
def2
ghi2
jkl2

Как это работает?

  • /abc/ совпадает со строками, имеющими этот текст, также делает /mno/.
  • /abc/{flag=1;next} устанавливает flag, когда найден текст abc. Затем эта строка пропускается.
  • /mno/{flag=0} убирает flag, когда найдено mno.
  • Конечный flag — это шаблон с дефолтным действием, которым является print $0: если флаг равен 1, то печатается строка. Таким образом, он напечатает все строки, появившиеся с момента появления abc и до следующего mno. Это также напечатает строки от последнего совпадения abc до конца файла.

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

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

awk '/abc/{a=1}/mno/{print;a=0}a' ФАЙЛ

Или так:

awk '/abc/{a=1} a; /mno/{a=0}' ФАЙЛ

Или даже так:

awk '/abc/,/mno/' ФАЙЛ

Используя sed:

sed -n -e '/^abc$/,/^mno$/{ /^abc$/d; /^mno$/d; p; }'

Опция -n означает не печатать по умолчанию (эта опция разъяснена выше).

Шаблон ищет строки, содержащие только с abc до только mno, затем выполняет действия в { … }.

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

Поиск фрагментов текста, начинающихся и заканчивающихся с определённых строк, вывод этих фрагментов с маркерами или без

Для систематизации изучим ещё несколько примеров, некоторые из которых пересекаются с уже рассмотренными.

Допустим имеется текстовый файл примерно как показано ниже, и я хочу вывести строки между двумя заданными паттернами, обозначенными как PAT1 и PAT2:

1
2
PAT1
3    - первый блок
4
PAT2
5
6
PAT1
7    - второй блок
PAT2
8
9
PAT1
10    - третий блок

Решения на основе awk

Печать строк между PAT1 и PAT2

awk '/PAT1/,/PAT2/' p}' ФАЙЛ
PAT1
3    - первый блок
4
PAT2
PAT1
7    - второй блок
PAT2
PAT1
10    - третий блок

Или используя переменные:

awk '/PAT1/{flag=1} flag; /PAT2/{flag=0}' p}' ФАЙЛ

Как это работает?

  • /PAT1/ соответствует строкам, имеющим этот текст, также делает /PAT2/.
  • /PAT1/{flag=1} устанавливает flag когда в строке найден текст PAT1.
  • /PAT2/{flag=0} удаляет flag когда в строке найден текст PAT2.
  • flag — это паттерн с дефолтным действием, которым является print $0: если флаг равен 1, то строка печатается. Таким образом, он напечатает все те строки с вхождениями со времён случившихся с PAT1 и вплоть до следующей увиденной PAT2. Также будут напечатаны строки с последнего совпадения PAT1 и вплоть до конца файла.

Напечатать строки между PAT1 и PAT2 — не включая PAT1 и PAT2

awk '/PAT1/{flag=1; next} /PAT2/{flag=0} flag' p}' ФАЙЛ
3    - первый блок
4
7    - второй блок
10    - третий блок

Это использует next для пропуска строки, которая содержит PAT1, чтобы она не печаталась.

Этот вызов next может быть отброшен перетасовкой блоков:

awk '/PAT2/{flag=0} flag; /PAT1/{flag=1}' ФАЙЛ

Напечатать строки между PAT1 и PAT2 - включая PAT1

awk '/PAT1/{flag=1} /PAT2/{flag=0} flag' p}' ФАЙЛ
PAT1
3    - первый блок
4
PAT1
7    - второй блок
PAT1
10    - третий блок

Помещая флаг в самом конце, он запускает действие, которое было установлено в PAT1 или PAT2: печатать в PAT1, а не печатать в PAT2.

Печать строк между PAT1 и PAT2 - включая PAT2

awk 'flag; /PAT1/{flag=1} /PAT2/{flag=0}' p}' ФАЙЛ
3    - первый блок
4
PAT2
7    - второй блок
PAT2
10    - третий блок

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

Вывести строки между PAT1 и PAT2 — исключая строки с последнего PAT1 до конца файла, если не найдено другого PAT2

Ещё вариант:

awk 'flag{
        if (/PAT2/)
           {printf "%s", buf; flag=0; buf=""}
        else
            buf = buf $0 ORS
     }
     /PAT1/ {flag=1}' p}' ФАЙЛ

В одну строку:

awk 'flag{ if (/PAT2/){printf "%s", buf; flag=0; buf=""} else buf = buf $0 ORS}; /PAT1/{flag=1}' p}' ФАЙЛ
3    - первый блок
4
7    - второй блок

# обратите внимание на отсутствие третьего блока, так как никакой другой PAT2 не происходит после него

Это сохраняет все выбранные строки в буфере, который заполняется с момента обнаружения PAT1. Затем он продолжает заполняться следующими строками, пока не будет найден PAT2. В этот момент он печатает сохранённый контент и очищает буфер.

Решения на основе sed

Печать строк между PAT1 и PAT2

sed -n '/PAT1/,/PAT2/{/PAT1/!{/PAT2/!p}}' p}' ФАЙЛ

или:

sed -n '/PAT1/,/PAT2/{//!p}'

Как и выше, но исключая границы диапазона.

Напечатать строки между PAT1 и PAT2 — включая PAT1 и PAT2

Следующий пример включит границы диапазона, что ещё проще:

sed -n '/PAT1/,/PAT2/p' p}' ФАЙЛ

Напечатать строки между PAT1 и PAT2 — включая PAT1

Следующее включит только начало диапазона:

sed -n '/PAT1/,/PAT2/{/PAT2/!p}' p}' ФАЙЛ

Напечатать строки между PAT1 и PAT2 — включая PAT2

Следующее включит только конец диапазона:

sed -n '/PAT1/,/PAT2/{/PAT1/!p}' ФАЙЛ

Решения на основе grep

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

Использование grep с PCRE (где доступен) для печати маркеров и строк между маркерами:

grep -Pzo "(?s)(PAT1(.*?)(PAT2|\Z))" ФАЙЛ
PAT1
3    - первый блок
4
PAT2
PAT1
7    - второй блок
PAT2
PAT1
10    - третий блок
  • -P использовать perl-regexp, PCRE. Не во всех вариантах grep
  • -z Трактовать ввод как набор строк, каждая из которых заканчивается на нулевой байт, а не на newline
  • -o печатать только совпадения
  • (?s) точка соответствует всему, то есть точка также находит newlines (символ новой строки)
  • (.*?) нежадный поиск
  • \Z Соответствует только концу строки, или перед newline в конце

Печатать строки между маркерами, исключая конечный маркер:

grep -Pzo "(?s)(PAT1(.*?)(?=(\nPAT2|\Z)))" ФАЙЛ
PAT1
3    - первый блок
4
PAT1
7    - второй блок
PAT1
10    - третий блок
  • (.*?)(?=(\nPAT2|\Z)) поиск нежадный поиск с lookahead для \nPAT2 и \Z

Печать строк между маркерами исключая маркеры:

grep -Pzo "(?s)((?<=PAT1\n)(.*?)(?=(\nPAT2|\Z)))" ФАЙЛ
3    - первый блок
4
7    - второй блок
10    - третий блок
  • (?<=PAT1\n) положительный lookbehind для PAT1\n

Печать строк между маркерами, исключая начальный маркер:

grep -Pzo "(?s)((?<=PAT1\n)(.*?)(PAT2|\Z))" ФАЙЛ
3    - первый блок
4
PAT2
7    - второй блок
PAT2
10    - третий блок

Рекомендуемые статьи:

Оставить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *