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/' ФАЙЛ
PAT1
3    - первый блок
4
PAT2
PAT1
7    - второй блок
PAT2
PAT1
10    - третий блок

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

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

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

  • /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' ФАЙЛ
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' ФАЙЛ
PAT1
3    - первый блок
4
PAT1
7    - второй блок
PAT1
10    - третий блок

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

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

awk 'flag; /PAT1/{flag=1} /PAT2/{flag=0}' ФАЙЛ
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}' ФАЙЛ

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

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

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

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

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

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

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

или:

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

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

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

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

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

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

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

sed -n '/PAT1/,/PAT2/{/PAT2/!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    - третий блок

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

4 Комментарии

  1. Александр

    Что-то тут не то с кавычками:
    awk ‘/PAT1/{flag=1} flag; /PAT2/{flag=0}’ p}’ ФАЙЛ

    1. Alexey (Автор записи)

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

  2. Ramil

    Здравствуйте !

    Есть лог файл в которы пишутся состояние контроллеров (обмен данными между контроллером и сервером)

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

    Вот пример лога:

    09.09.2022  16:51:20  CONSOLA:9
    09.09.2022  16:51:23  Init Com Eth(0):10.100.0.10:14101
    09.09.2022  16:51:23  Open communication port
    09.09.2022  16:51:24  Trying to establish communication with the controller
    09.09.2022  16:51:24  KIT: GK7C-3 VERSION: V9.18 CPU NUMBER: CPA102001 
    09.09.2022  16:51:25  Established communication
    09.09.2022  16:51:25  Sending date and hour
    09.09.2022  16:51:26  COMMUNICATION: The controller has been synchronized.
    09.09.2022  16:51:27  Receiving supplies
    09.09.2022  16:52:25  Supplies correctly received
    09.09.2022  16:52:27  Processed
    09.09.2022  16:52:27  Sending users
    09.09.2022  16:52:52  Users correctly sent
    09.09.2022  16:52:52  Sending vehicles
    09.09.2022  16:53:06  Vehicles correctly sent
    09.09.2022  16:53:06  Sending controller configuration
    09.09.2022  16:53:08  Controller configuration correctly sent
    09.09.2022  16:53:08  Sending product configuration
    09.09.2022  16:53:10  Product configuration correctly sent
    09.09.2022  16:53:10  Sending tank configuration
    09.09.2022  16:53:11  Tank configuration correctly sent
    09.09.2022  16:53:11  Sending hose configuration
    09.09.2022  16:53:13  Hose configuration correctly sent
    09.09.2022  16:53:14  Disconnecting the controller
    09.09.2022  16:53:15  Disconnected controller
    09.09.2022  16:53:15  Close communication port

     

    1. Alexey (Автор записи)

      Приветствую! Предположим, что исходные данные следующие:

      • логи содержаться в файле logs.txt
      • начальной строкой нужного фрагмента является «CONSOLA:9» без кавычек
      • конечной строкой нужного фрагмента является «Close communication port»

      Следующая команда выведет последний фрагмент между строками «CONSOLA:9» и «Close communication port»:

      tac logs.txt | sed -n '/Close communication port/,/CONSOLA:9/p; /CONSOLA:9/q' | tac

      Я не знал, как вывести именно последний фрагмент, поэтому командой tac сделан обратный порядок строк в файле. Затем находится и выводится фрагмент между «CONSOLA:9» и «Close communication port». Обратите внимание, что начало и конец фрагмента поменяны местами (поскольку весь файл имеет обратный порядок строк). Затем команда «q» (фрагмент «/CONSOLA:9/q» указывает выйти из команды, после первого совпадения. Наконец, « | tac» возвращает нормальный порядок строк.

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

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