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 кажется, работает (очень медленно), поэтому это должно быть возможно.

5 ответов5

19

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

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

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

6

Я проверил этот метод (и все остальные) на 10 миллионах (пустых) файлах с именами "привет 00000001" или "привет 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
-----------+---------+---------+----------------------------------------------------- 
2

Еще одна возможность для совершенно безопасного поиска:

while IFS= read -rd $'\0' path
do
    file_path="$(readlink -fn -- "$path"; echo x)"
    file_path="${file_path%x}"
    sed -i -e 's/blah/blee/g' -- "$file_path"
done < <( find "$absolute_dir_path" -type f -print0 )
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 ...) - скорость: вы избегаете вызывать sed 10 миллионов раз. Было бы еще быстрее, если бы вы могли избежать использования Python (так как python довольно медленный), поэтому Perl может быть лучшим выбором для этой задачи. Я не уверен, как сделать эквивалент удобно с Perl.

Это работает так, что xargs будет вызывать Python с таким количеством аргументов, сколько может поместиться в одной командной строке, и продолжит делать это до тех пор, пока у него не закончатся аргументы (которые предоставляются 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 обрабатываются самой оболочкой (bash). Это означает, что перед выполнением команды в командной строке будет подставлено 10 миллионов имен файлов. Это почти наверняка превысит максимальный размер списка аргументов команды. Вы можете использовать xargs --show-limits для системной информации по этому вопросу.

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

0

Пытаться:

ls | while read file; do (something to $file); done

Всё ещё ищете ответ? Посмотрите другие вопросы с метками .