нелинейный инструмент для замены строк?

13

Недавно я задал вопрос о том, как удалить символ новой строки, если это происходит после другого конкретного символа.

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

Но что мне делать, если я хочу заменить текстовую последовательность в огромном файле, который не содержит новых строк?

Например, заменить <foobar>на \n<foobar>без чтения ввод построчно? (так как есть только одна строка и длина 2.5G символов).

MattBianco
источник
1
Вы открыты для использования perlили python?
iruvar
Perl в порядке. Я только что нашел gsar( home.online.no/~tjaberg ), который я попробую.
MattBianco

Ответы:

12

Первое, что приходит мне в голову, когда я сталкиваюсь с проблемой такого типа, это изменение разделителя записей. В большинстве инструментов это установлено \nпо умолчанию, но это можно изменить. Например:

  1. Perl

    perl -0x3E -pe 's/<foobar>/\n$&/' file
    

    объяснение

    • -0: это устанавливает разделитель входной записи на символ, учитывая его шестнадцатеричное значение . В этом случае я устанавливаю >чье шестнадцатеричное значение 3E. Общий формат есть -0xHEX_VALUE. Это просто хитрость, чтобы разбить линию на управляемые куски.
    • -pe: печатать каждую строку ввода после применения скрипта, заданного -e.
    • s/<foobar>/\n$&/простая замена. Это $&то, что было подобрано, в этом случае <foobar>.
  2. AWK

    awk '{gsub(/foobar>/,"\n<foobar>");printf "%s",$0};' RS="<" file
    

    объяснение

    • RS="<": установите разделитель входной записи на >.
    • gsub(/foobar>/,"\n<foobar>"): заменить все случаи foobar>с \n<foobar>. Обратите внимание, что поскольку RSбыло установлено значение <, все <удаляются из входного файла (вот как это awkработает), поэтому нам нужно сопоставить foobar>(без <) и заменить на \n<foobar>.
    • printf "%s",$0: вывести текущую «строку» после замены. $0текущая запись, awkпоэтому она будет содержать все, что было до <.

Я протестировал их на однострочном файле объемом 2,3 ГБ, созданном с помощью следующих команд:

for i in {1..900000}; do printf "blah blah <foobar>blah blah"; done > file
for i in {1..100}; do cat file >> file1; done
mv file1 file

И то, awkи другое perlиспользовали незначительное количество памяти.

Тердон
источник
Вы когда-нибудь пробовали Tie::File perldoc.perl.org/Tie/File.html . Я думаю, что это лучшая особенность Perlпри работе с огромными файлами.
cuonglm
@ Gnouc Я немного поиграл, да. Но i) OP уже заявил о неприязни к Perl в другом вопросе, поэтому я хотел, чтобы он был простым: ii) я стараюсь избегать использования внешних модулей, если в этом нет крайней необходимости, и iii) использование модуля Tie :: File значительно снизит синтаксис Чисто.
Тердон
Согласен. Небольшое замечание, что Tie::Fileэто основной модуль с тех пор v5.7.3.
cuonglm
9

gsar (общий поиск и замена) - очень полезный инструмент именно для этой цели.

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

Во многих случаях это очень хорошо и даже читабельно. Я как проблемы , которые могут быть легко / эффективно решено с помощью всюду доступными инструментами , такими как awk, tr, sedи оболочка Борна.

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

Некоторые из вас могут подумать, что это обман, но я не понимаю, как использование правильного инструмента для работы может быть неправильным. В этом случае программа C называется , gsarчто под лицензией GPL v2 , так что меня удивляет совсем немного , что нет никакого пакета для этого очень полезного инструмента ни папуасского , RedHat , ни убунту .

gsarиспользует двоичный вариант алгоритма поиска строки Бойера-Мура .

Использование просто:

gsar -F '-s<foobar>' '-r:x0A<foobar>'

где -Fозначает режим «фильтра», то есть чтение, stdinзапись в stdout. Есть методы для работы с файлами. -sуказывает строку поиска и -rзамену. Обозначение двоеточия может использоваться для указания произвольных значений байтов.

Режим без учета регистра поддерживается ( -i), но регулярных выражений не поддерживается, поскольку алгоритм использует длину строки поиска для оптимизации поиска.

Инструмент также может быть использован только для поиска, немного похоже grep. gsar -bвыводит смещения байтов совпадающей строки поиска и gsar -lпечатает имя файла и количество совпадений, если таковые имеются, как в сочетании grep -lс wc.

Инструмент был написан Тормодом Тьябергом (начальный) и Хансом Питером Верном (улучшения).

MattBianco
источник
Если это GPL, рассмотрите возможность упаковки его для дистрибутива :)
Rqomey
1
На самом деле я довольно серьезно думаю о создании Gentoo Ebuild для него. Может быть, обороты тоже. Но я никогда раньше не создавал пакет .deb, поэтому я надеюсь, что кто-то превзойдет меня (потому что это займет у меня некоторое время).
MattBianco
Я сомневаюсь, что это очень утешает, но домашний напиток OS X имеет формулу для gsar.
Crazysim
5

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

Вот пример Python, который заменяет foobarнаXXXXXX

#! /usr/bin/python
import mmap
import contextlib   
with open('test.file', 'r+') as f:
 with contextlib.closing(mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_WRITE)) as m:
   pos = 0
   pos = m.find('foobar', pos)
   while pos > 0:
    m[pos: pos+len('XXXXXX')] = 'XXXXXX'
    pos = m.find('foobar', pos)
Iruvar
источник
4

Для этого есть много инструментов:

ddэто то, что вы хотите использовать, если вы хотите заблокировать файл - надежно читать только определенное количество байтов только определенное количество раз. Портативно обрабатывает блокировку и разблокировку файловых потоков:

tr -dc '[:graph:]' </dev/urandom | dd bs=32 count=1 cbs=8 conv=unblock,sync 2>/dev/null

###OUTPUT###

UI(#Q5\e BKX2?A:Z RAxGm:qv t!;/v!)N

Я также использую trвыше, потому что он может обрабатывать преобразование любого байта ASCII в любой другой (или, в этом случае, удаление любого байта ASCII, который не является печатным символом без пробела). Это то, что я использовал в ответ на ваш другой вопрос сегодня утром, на самом деле, когда я сделал:

tr '>\n' '\n>' | sed 's/^>*//' | tr '\n>' '>\n' 

Есть много похожих . Этот список должен содержать наименьшее подмножество общего знаменателя, с которым вы можете познакомиться.

Но если бы я собирался выполнить обработку текста на 2,5 ГБ двоичного файла, я мог бы начать с od. Он может дать вам один octal dumpили несколько других форматов. Вы можете указать все виды опций - но я просто сделаю один байт на строку в \Cэкранированном формате:

Данные, которые вы получите, odбудут регулярными с любым указанным вами интервалом - как я покажу ниже. Но сначала - вот ответ на ваш вопрос:

printf 'first\nnewline\ttab spacefoobar\0null' |
od -A n -t c -v -w1 |
sed 's/^ \{1,3\}//;s/\\$/&&/;/ /bd
     /\\[0nt]/!{H;$!d};{:d
    x;s/\n//g}'

Это немного выше разграничивает на \newlines, \0нули, \tабс и <spaces>при сохранении \Cсбежавшего строки для разделителя. Обратите внимание на используемые функции Hи x- каждый раз, когда sedвстречается разделитель, он заменяет содержимое своих буферов памяти. Таким образом, sedхранится только столько информации, сколько необходимо для надежного разграничения файла, и он не поддается переполнению буфера - нет, то есть до тех пор, пока он действительно сталкивается со своими разделителями. До тех пор, пока это происходит, sedбудет продолжать обрабатывать свои входные данные и odбудет предоставлять их до тех пор, пока они не встретятся EOF.

Как есть, его вывод выглядит так:

first
\nnewline
\ttab
 spacefoobar
\0null

Так что, если я хочу foobar:

printf ... | od ... | sed ... | 
sed 's/foobar/\
&\
/g'

###OUTPUT###

first
\nnewline
\ttab
 space
foobar

\0null

Теперь, если вы хотите использовать Cescape-коды, это довольно просто - потому sedчто двойная \\обратная косая черта уже экранирована от всех одиночных обратных косых черт ввода, так что printfисключение из не xargsбудет иметь проблем с выводом в вашу спецификацию. Но есть xargs кавычки оболочки, поэтому вам нужно будет снова заключить в кавычки:

printf 'nl\ntab\tspace foobarfoobar\0null' |
PIPELINE |
sed 's/./\\&/g' | 
xargs printf %b | 
cat -A

###OUTPUT###

nl$
tab^Ispace $
foobar$
$
foobar$
^@null%

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

И вот как все это выглядит прежде, чем когда-либо sedовладевает этим:

printf 'nl\ntab\tspace foobarfoobar\0null' |
od -A n -t c -v -w1

   n
   l
  \n
   t
   a
   b
  \t
   s
   p
   a
   c
   e

   f
   o
   o
   b
   a
   r
   f
   o
   o
   b
   a
   r
  \0
   n
   u
   l
   l
mikeserv
источник
2

Awk работает с последовательными записями. Он может использовать любой символ в качестве разделителя записей (кроме нулевого байта во многих реализациях). Некоторые реализации поддерживают произвольные регулярные выражения (не совпадающие с пустой строкой) в качестве разделителя записей, но это может быть громоздким, поскольку разделитель записей усекается с конца каждой записи до того, как он будет помещен в $0(GNU awk устанавливает переменную RTв разделитель записей) это было снято с конца текущей записи). Обратите внимание, что printвывод завершается разделителем выходных записей, ORSкоторый по умолчанию является новой строкой и устанавливается независимо от разделителя входных записей RS.

awk -v RS=, 'NR==1 {printf "input up to the first comma: %s\n", $0}'

Вы можете эффективно выбрать другой символ как разделитель записей для других инструментов ( sort, sed, ...) путем замены новой строки с этим символом с tr.

tr '\n,' ',\n' |
sed 's/foo/bar/' |
sort |
tr '\n,' ',\n'

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

Жиль "ТАК - прекрати быть злым"
источник