?

Log in

デニース [userpic]

Multiple Lines Regexp

Декабрь, 1, 2011 (13:47)
Метки:

Давно интересовал вопрос поиска нескольких строк в текстовом файле по регулярному выражению. Тут надо оговориться: это не поиск "или": "или такой строки, или сякой, или вот такой", с этим вполне справляется

grep -E "^line1$|^line2$|^line3$"
а поиск именно последовательности определенных строк.

Для примера возьмем текстовый файл ./file.txt:
1111
2222
3333
4444
5555
6666
и попробуем удалить оттуда блок
3333
4444
5555
В итоге нашел несколько хороших решений:
  1. Я изначально нацеливался на использование grep. В новых версиях решение находится легко с использованием ключа --perl-regexp, позволяющего вводить выражения, как можно догадатся из названия (и мана), в стиле перл, используя \n в качестве разделителя строк:
    grep -v --perl-regexp "^3333\n4444\n5555$" ./file.txt 
    1111
    2222
    6666
    
  2. Не все версии grep имеют поддержку такого ключа. На некоторых OS или в старых версиях попытки заставить grep понимать "\n", "\n\r" и т.п. оказались безуспешными или требовали установки pcregrep (тут надо сказать, что ставить какие бы то ни было пакеты я не имел возможности). Поэтому мы воспользуемся sed'ом, а точнее его интересным ключом N. Он указывает sed'у не очищать буфер с регулярным выражением, а добавить его к следующему выражению при анализе следующей строки:
    sed -e "/^3333$/ {N; /\n4444$/ {N; /\n5555$/ {d;}}}" ./file.txt 
    1111
    2222
    6666
    
    Таким образом строки 3333, 4444 и 5555 будут удалены, только если они идут друг за другом.

    Тут надо сделать замечание, что скрипт, представленный выше, не всегда будет корректно работать при перекрывающихся многострочных регулярных выражениях, в отличие от первого варианта. Т.е. если исходный файл имеет вид:
    1111
    2222
    3333
    4444
    3333
    4444
    3333
    5555
    6666
    а надо удалить блок,
    3333
    4444
    3333
    5555
    то данная строка на седе оставит файл без изменений.


    Чтобы автоматизировать процесс, надо в отдельном файле иметь строки, которые нужно удалить из исходного файла (например, есть файл ./to_remove.txt следующего содержания):
    3333
    4444
    5555
    Тогда с помощью простенького awk скрипта можно генерить строку для sed'а.

  3. Еще один вариант на awk (с оберткой в bash-скрипт для удобства использования) на основе предложения от Юры. Скрипт позволяет работать как с файлами, так и с stdin, писать регексп прямо в командной строке или читать из файла, поддерживает ключи -n, -v, -i аналогично grep'у:
    ./mlgrep.sh -h
    mlgrep.sh is 'Multiline grep' tool.
     
    Usage: mlgrep.sh [OPTION]... PATTERN [FILE]...
    Search for PATTERN in each FILE or standard input.
    PATTERN is awk-style regular expression ('\n' are
    allowed to represent multiline regexp)
     
    OPTIONS:
      -n       - print line numbers
      -v       - invert match
      -i       - ignore case
      -f       - read pattern from file
     

    Например, если исходный файл file.txt имеет вид:
    1111
    2222
    3333
    4444
    3333
    4444
    3333
    5555
    6666

    , то
    ./mlgrep.sh -n "^3+\n4444\n3333\n5+$" ./file.txt 
    5:3333
    6:4444
    7:3333
    8:5555
     



Comments

Posted by: Юрий Арапов (yuridichesky)
Posted at: Декабрь, 1, 2011 10:17 (UTC)

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

Posted by: デニース (denis_aikidoka)
Posted at: Декабрь, 1, 2011 10:52 (UTC)

Да, я про awk тоже думал, но там свои заморочки: придется сохранять и не печатать каждую строку, попадающую под регулярное выражение, до тех пор, пока условия выполняются. А потом, если N-я строка не подходит под выражение, нужно будет выдавать на печать весь сохраненный блок.

Мне кажется, на awk в данном случае не так очевидно будет.

Posted by: Юрий Арапов (yuridichesky)
Posted at: Декабрь, 1, 2011 11:10 (UTC)

а мне кажется, что как раз наоборот :-)

"протаскиваем" через файл рамку из n строк и сравниваем эту рамку с рамкой-шаблоном. если совпадают, то стираем рамку (и ничего не печатаем), а если не совпадают, то печатаем верхнюю строку рамки и сдвигаем ее на одну строчку вниз.

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

Posted by: デニース (denis_aikidoka)
Posted at: Декабрь, 1, 2011 11:29 (UTC)

Предлагаешь каждый раз перезаписывать файл-рамку из n строк? Тогда придется сделать "кол-во_строк_исходного_файла - n" перезаписываний файла. Если бы в awk можно было перемещаться по редактируемому файлу, тогда было бы проще )

Posted by: Юрий Арапов (yuridichesky)
Posted at: Декабрь, 1, 2011 11:54 (UTC)

сканирующая рамка -- циклический буфер того же размера, что рамка-шаблон. каждая строка из входного файла один раз записывается в сканирующую рамку и больше никуда не двигается. некоторые строки из "верхней" позиции рамки записываются в выходной файл. количество чтений из файла О(N), количество записей в файл О(N), количество сравнений строк O(N*P), P -- размер шаблона. с количеством сравнений можно еще пошаманить.
или нас интересует inplace редактирование? тогда надо все прочитать в память и манипулировать в памяти. все O(...) будут такими же.

Posted by: デニース (denis_aikidoka)
Posted at: Декабрь, 3, 2011 22:18 (UTC)

Написал по твоему предложенному алгоритму скрипт, добавил как третий вариант в пост :)

6 Читать комментарии