Как запустить sed на более чем 10 миллионов файлов в каталоге?

16

У меня есть каталог, в котором есть 10144911 файлов. До сих пор я пробовал следующее:

  • for f in ls; do sed -i -e 's/blah/blee/g' $f; done

Разбился мой снаряд, lsон в тильде, но я не могу понять, как его сделать.

  • ls | xargs -0 sed -i -e 's/blah/blee/g'

Слишком много аргументов для sed

  • find . -name "*.txt" -exec sed -i -e 's/blah/blee/g' {} \;

Не могу больше разветвляться, больше нет памяти

Любые другие идеи о том, как создать такую ​​добрую команду? Файлы не должны общаться друг с другом. ls | wc -lкажется, работает (очень медленно), поэтому это должно быть возможно.

Sandro
источник
1
Было бы быстрее, если бы вы могли избежать вызова sedдля каждого файла. Я не уверен, есть ли способ открывать, редактировать, сохранять и закрывать ряд файлов sed; если скорость важна, вы можете использовать другую программу, например, Perl или Python.
интуитивно
@intuited: было бы еще быстрее вообще ничего не делать с файлами ... серьезно? если вы хотите изменить шаблон в наборе файлов, вы должны посмотреть в каждом файле, чтобы увидеть, есть ли шаблон. если вы заранее знаете, что можете пропустить «некоторые» файлы, то, очевидно, быстрее даже не трогать файлы. и время запуска sed, вероятно, быстрее, чем запуск pythonили perlтакже, за исключением случаев, когда вы делаете все в этом интерпретаторе.
Акира
@akira: Вы говорите, что запуск perl или python один раз для столько файлов, сколько поместится в командной строке, стоит дороже, чем запуск sed один раз для каждого из этих файлов? Я был бы очень удивлен, если бы это было так. —————— Я полагаю, вы не поняли, что мое предложение состоит в том, чтобы вызвать (запустить) программу редактирования один раз (или, по крайней мере, меньшее количество раз - см. Мой ответ), и позволить ей открывать, изменять и сохранять каждый из файлов в свою очередь, вместо того, чтобы вызывать программу редактирования отдельно для каждого из этих файлов.
интуитивно
Ваш первый комментарий не отражает того, что вы на самом деле хотели сказать: «замените sed на python / perl» ... просто сделав это и посмотрев @ командную строку OP, невинный читатель может предположить, что «find. -exec python» быстрее, чем "find. -exec sed" .. что явно не так. в своем собственном ответе вы называете python гораздо чаще, чем это действительно необходимо.
Акира
Я думаю, что Акира неправильно истолковал ваше (интуитивное) предложение. Я полагаю, что вы предлагали собирать файлы вместе. Я попробовал это с моей попыткой xargs, время попробовать еще раз :)
Sandro

Ответы:

19

Попробуйте это:

find -name '*.txt' -print0 | xargs -0 -I {} -P 0 sed -i -e 's/blah/blee/g' {}

Он будет передавать только одно имя файла на каждый вызов sed. Это решит проблему «слишком много аргументов для седа». -PВариант должен позволить нескольким процессам быть раздвоенной одновременно. Если 0 не работает (он должен работать как можно больше), попробуйте другие числа (10? 100? Количество ядер у вас?), Чтобы ограничить число.

Приостановлено до дальнейшего уведомления.
источник
3
Вероятно, необходимо find . -name \*.txt -print0избегать того, чтобы оболочка расширяла глобус и пыталась выделить место для 10 миллионов аргументов для поиска .
Крис Джонсен
@ChrisJohnsen: Да, это правильно. Я поспешил опубликовать свой ответ и пропустил в том числе эти важные части. Я отредактировал свой ответ с этими исправлениями. Благодарю.
Приостановлено до дальнейшего уведомления.
Попытка это сейчас ... скрещивает пальцы
Сандро
7

Я протестировал этот метод (и все остальные) на 10 миллионах (пустых) файлах, с именами «hello 00000001» или «hello 10000000» (14 байтов на имя).

ОБНОВЛЕНИЕ: я теперь включил четырехъядерный запуск 'find |xargs'метода (все еще без 'sed'; просто echo> / dev / null) ..

# Step 1. Build an array for 10 million files
#   * RAM usage approx:  1.5 GiB 
#   * Elapsed Time:  2 min 29 sec 
  names=( hello\ * )

# Step 2. Process the array.
#   * Elapsed Time:  7 min 43 sec
  for (( ix=0, cnt=${#names[@]} ; ix<$cnt; ix++ )) ; do echo "${names[ix]}" >/dev/null ; done  

Вот краткое изложение того, как предоставленные ответы оказались эффективными при выполнении с данными испытаний, упомянутыми выше. Эти результаты включают только основные накладные расходы; т.е. «седь» не назывался. Процесс sed почти наверняка будет самым трудоемким, но я подумал, что было бы интересно посмотреть, как сравниваются голые методы.

Денниса 'find |xargs'Метод , использующий одно ядро, занял * 4 часа 21 минуту ** дольше, чем bash arrayметод на no sedходу ... Однако преимущество многоядерности, предоставляемое 'find', должно перевесить разницу во времени, показанную при вызове sed обработка файлов ...

           | Time    | RAM GiB | Per loop action(s). / The command line. / Notes
-----------+---------+---------+----------------------------------------------------- 
Dennis     | 271 min | 1.7 GiB | * echo FILENAME >/dev/null
Williamson   cores: 1x2.66 MHz | $ time find -name 'hello *' -print0 | xargs -0 -I {} echo >/dev/null {}
                               | Note: I'm very surprised at how long this took to run the 10 million file gauntlet
                               |       It started processing almost immediately (because of xargs I suppose),  
                               |       but it runs **significantly slower** than the only other working answer  
                               |       (again, probably because of xargs) , but if the multi-core feature works  
                               |       and I would think that it does, then it could make up the defecit in a 'sed' run.   
           |  76 min | 1.7 GiB | * echo FILENAME >/dev/null
             cores: 4x2.66 MHz | $ time find -name 'hello *' -print0 | xargs -0 -I {} -P 0 echo >/dev/null {}
                               |  
-----------+---------+---------+----------------------------------------------------- 
fred.bear  | 10m 12s | 1.5 GiB | * echo FILENAME >/dev/null
                               | $ time names=( hello\ * ) ; time for (( ix=0, cnt=${#names[@]} ; ix<$cnt; ix++ )) ; do echo "${names[ix]}" >/dev/null ; done
-----------+---------+---------+----------------------------------------------------- 
l0b0       | ?@#!!#  | 1.7 GiB | * echo FILENAME >/dev/null 
                               | $ time  while IFS= read -rd $'\0' path ; do echo "$path" >/dev/null ; done < <( find "$HOME/junkd" -type f -print0 )
                               | Note: It started processing filenames after 7 minutes.. at this point it  
                               |       started lots of disk thrashing.  'find' was using a lot of memory, 
                               |       but in its basic form, there was no obvious advantage... 
                               |       I pulled the plug after 20 minutes.. (my poor disk drive :(
-----------+---------+---------+----------------------------------------------------- 
intuited   | ?@#!!#  |         | * print line (to see when it actually starts processing, but it never got there!)
                               | $ ls -f hello * | xargs python -c '
                               |   import fileinput
                               |   for line in fileinput.input(inplace=True):
                               |       print line ' 
                               | Note: It failed at 11 min and approx 0.9 Gib
                               |       ERROR message: bash: /bin/ls: Argument list too long  
-----------+---------+---------+----------------------------------------------------- 
Reuben L.  | ?@#!!#  |         | * One var assignment per file
                               | $ ls | while read file; do x="$file" ; done 
                               | Note: It bombed out after 6min 44sec and approx 0.8 GiB
                               |       ERROR message: ls: memory exhausted
-----------+---------+---------+----------------------------------------------------- 
Peter.O
источник
1

Это в основном не по теме, но вы можете использовать

find -maxdepth 1 -type f -name '*.txt' | xargs python -c '
import fileinput
for line in fileinput.input(inplace=True):
    print line.replace("blah", "blee"),
'

Основным преимуществом здесь (сверх ... xargs ... -I {} ... sed ...) является скорость: вы избегаете вызова sed10 миллионов раз. Было бы еще быстрее, если бы вы могли избежать использования Python (поскольку python довольно медленный), поэтому Perl может быть лучшим выбором для этой задачи. Я не уверен, как сделать эквивалент удобно с Perl.

Это работает так, что xargsPython будет вызывать столько аргументов, сколько может поместиться в одной командной строке, и продолжит делать это до тех пор, пока у него не закончатся аргументы (которые предоставляются ls -f *.txt). Количество аргументов для каждого вызова будет зависеть от длины имен файлов и некоторых других вещей. fileinput.inputФункция дает последовательные строки из файлов , названных в качестве аргументов для каждого вызова, в и inplaceопция указывает , что волшебным образом «поймать» выход и использовать его для замены каждой строки.

Обратите внимание, что строковый replaceметод Python не использует регулярные выражения; если вам это нужно, вы должны import reи использовать print re.sub(line, "blah", "blee"). Это Perl-совместимые RegExps, которые являются своего рода сильно укрепленными версиями тех, что вы получаете сsed -r .

редактировать

Как упоминает Акира в комментариях, оригинальная версия с использованием glob ( ls -f *.txt) вместо findкоманды не будет работать, потому что globs обрабатываются самой shell ( bash). Это означает, что перед выполнением команды в командной строке будет подставлено 10 миллионов имен файлов. Это в значительной степени гарантированно превышает максимальный размер списка аргументов команды. Вы можете использовать xargs --show-limitsдля системной информации об этом.

Максимальный размер списка аргументов также принимается во внимание xargs, что ограничивает количество аргументов, которые он передает каждому вызову python в соответствии с этим пределом. посколькуxargs все еще придется вызывать python несколько раз, предложение Акиры использовать его os.path.walkдля получения списка файлов, вероятно, сэкономит вам некоторое время.

созерцаемое
источник
1
какой смысл в использовании оператора glob (который все равно потерпит неудачу для такого количества файлов) ... и затем передать файлы в python, который имеет os.path.walk()?
Акира
@akira: оператор glob не должен пытаться заменить содержимое .и ... Конечно, есть другие способы сделать это (то есть find), но я стараюсь максимально близко придерживаться того, что понимает ОП. Это также причина не использовать os.path.walk.
интуитивно
@akira: Хорошее предложение, однако, это, вероятно, будет значительно быстрее.
интуитивно
Я думаю, что ОП поймет os.path.walkдовольно легко.
Акира
0

Пытаться:

ls | while read file; do (something to $file); done
Рувим Л.
источник
2
ls -fбыло бы лучше; Вы действительно хотите подождать stat()и отсортировать столько файлов?
geekosaur
сейчас я пытаюсь: для f в * .txt; делать бла; сделано. Я ударю, если это не удастся. Спасибо!
Сандро