Заменить первое совпадение строк в 1-строчном 2ГБ файле в Linux
Я пытаюсь заменить только первое совпадение одной строки в огромном файле только одной строкой (2,1 ГБ), эта замена произойдет в задании сценария оболочки. Большая проблема в том, что машина, на которой будет запускаться этот скрипт, имеет только 1 ГБ памяти (примерно 300 МБ свободной), поэтому мне нужна буферизованная стратегия, которая не переполняет мою память. Я уже пробовала sed
, perl
и python
подход, но все они возвращают меня из ошибок памяти. Вот мои попытки (обнаруженные в других вопросах):
# With perl
perl -pi -e '!$x && s/FROM_STRING/TO_STRING/ && ($x=1)' file.txt
# With sed
sed '0,/FROM_STRING/s//TO_STRING/' file.txt > file.txt.bak
# With python (in a custom script.py file)
for line in fileinput.input('file.txt', inplace=True):
print line.replace(FROM_STRING, TO_STRING, 1)
break
Один хороший момент заключается в том, что FROM_STRING
то, что я ищу, всегда находится в начале этого огромного файла в одну строку, в первых 100 символов. Еще одна хорошая вещь - время выполнения не является проблемой, оно может занять время без проблем.
РЕДАКТИРОВАТЬ (РЕШЕНИЕ):
Я проверил три решения ответов, все они решили проблему, спасибо всем вам. Я проверил производительность с Linux time
и все они тоже занимают примерно одинаковое время, примерно до 10 секунд... Но я выбираю решение @Miller, потому что оно проще (просто использует perl
).
5 ответов
Если вы уверены, что строка, которую вы пытаетесь заменить, находится только в первых 100 символах, тогда должна работать следующая строка perl:
perl -i -pe 'BEGIN {$/ = \1024} s/FROM_STRING/TO_STRING/ .. undef' file.txt
Объяснение:
Переключатели:
-i
: Редактировать<>
файлы на месте (делает резервную копию, если расширение предоставлено)-p
: Создаетwhile(<>){...; print}
цикл для каждой "строки" во входном файле.-e
: Говоритperl
выполнить код в командной строке.
Код:
BEGIN {$/ = \1024}
: Установите $INPUT_RECORD_SEPARATOR равным количеству символов, которые нужно прочитать для каждой "строки"s/FROM/TO/ .. undef
: Используйте триггер, чтобы выполнить регулярное выражение только один раз. Мог бы также использоватьif $. == 1
,
Поскольку вы знаете, что ваша строка всегда находится в первом фрагменте файла, вы должны использовать dd
за это. Вам также понадобится временный файл для работы, как в tmpfile="$(mktemp)"
Сначала скопируйте первый блок файла в новое временное местоположение:dd bs=32k count=1 if=file.txt of="$tmpfile"
Затем выполните замену в этом блоке:sed -i 's/FROM_STRING/TO_STRING/' "$tmpfile"
Затем объедините новый первый блок с остальной частью старого файла, снова используя dd
:dd bs=32k if=file.txt of="$tmpfile" seek=1 skip=1
РЕДАКТИРОВАТЬ: В соответствии с предложением Марка Сетчелла, я добавил спецификацию bs=32k
на эти команды, чтобы ускорить темп dd
операции. Это настраивается в соответствии с вашими потребностями, но если вы настраиваете отдельные команды по-отдельности, вам, возможно, следует быть осторожным с изменениями семантики между различными размерами входного и выходного блоков.
- Учитывая, что тогда строка для замены находится в первых 100 байтах,
- Учитывая, что Perl IO работает медленно, если вы не начнете использовать
sysread
читать большие блоки, - Предполагая, что подстановка изменяет размер файла[1], и
- При условии, что
binmode
не требуется[2],
Я бы использовал
( head -c 100 | perl -0777pe's/.../.../' && cat ) <file.old >file.new
- Более быстрое решение для этого существует.
- Хотя это легко добавить в случае необходимости.
Не проверено, но я бы сделал:
perl -pi -we 'BEGIN{$/=\65536} s/FROM_STRING/TO_STRING/ if 1..1' file.txt
читать кусками по 64к.
Практичным (не очень компактным, но эффективным) будет разделение файла, поиск-замена и объединение: например:
head -c 100 myfile | sed 's/FROM/TO/' > output.1
tail -c +101 myfile > output.2
cat output.1 output.2 > output && /bin/rm output.1 output.2
Или в одну строку:
( ( head -c 100 myfile | sed 's/FROM/TO/' ) && (tail -c +101 myfile ) ) > output