Como executar sed em mais de 10 milhões de arquivos em um diretório?

16

Eu tenho um diretório que tem 10144911 arquivos nele. Até agora eu tentei o seguinte:

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

Caiu meu shell, o ls está em um til, mas não consigo descobrir como fazer um.

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

Muitos argumentos para sed

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

Não foi possível bifurcar mais nenhuma memória

Alguma outra ideia sobre como criar esse comando? Os arquivos não precisam se comunicar uns com os outros. ls | wc -l parece funcionar (muito lento), por isso deve ser possível.

    
por Sandro 14.03.2011 / 03:03

5 respostas

19

Experimente:

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

Ele só alimentará um nome de arquivo para cada invocação de sed . Isso resolverá o problema "too args for sed". A opção -P deve permitir que vários processos sejam bifurcados ao mesmo tempo. Se 0 não funcionar (é preciso executar o maior número possível), tente outros números (10? 100? O número de núcleos que você tem?) Para limitar o número.

    
por 14.03.2011 / 05:11
6

Eu testei esse método (e todos os outros) em arquivos 10 milhões (vazios), chamados "hello 00000001" para "hello 10000000" (14 bytes por nome).

UPDATE: Eu incluí uma execução quad-core no método 'find |xargs' (ainda sem 'sed'; apenas 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  

Aqui está um resumo de como as respostas fornecidas se saíram quando comparadas com os dados de teste mencionados acima. Esses resultados envolvem apenas as despesas gerais básicas; ou seja, 'sed' não foi chamado. O processo sed provavelmente será o mais demorado, mas achei que seria interessante ver como os métodos nus eram comparados.

O método 'find |xargs' de Dennis, usando um único núcleo, levou * 4 horas 21 mins ** mais do que o método bash array em uma execução no sed ... No entanto, a vantagem de vários núcleos oferecida por 'encontrar' deve compensar as diferenças de tempo mostradas quando o sed está sendo chamado para processar os arquivos ...

           | 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 $'
# 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  
' 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 -----------+---------+---------+-----------------------------------------------------
    
por 14.03.2011 / 06:18
2

Outra oportunidade para o encontrar completamente seguro :

while IFS= read -rd $'
while IFS= read -rd $'%pre%' 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 )
' 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 )
    
por 14.03.2011 / 10:27
1

Isso é principalmente fora do tópico, mas você pode usar

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

O principal benefício aqui (acima de ... xargs ... -I {} ... sed ... ) é a velocidade: você evita invocar sed 10 milhões de vezes. Seria ainda mais rápido se você pudesse evitar o uso do Python (já que o Python é relativamente lento), então o perl pode ser uma escolha melhor para essa tarefa. Não sei como fazer o equivalente convenientemente com o perl.

A maneira como isso funciona é que xargs invocará o Python com tantos argumentos quantos couberem em uma única linha de comando e continuará fazendo isso até ficar sem argumentos (que estão sendo fornecidos por ls -f *.txt ). O número de argumentos para cada invocação dependerá do tamanho dos nomes dos arquivos e, algumas outras coisas. A função fileinput.input produz linhas sucessivas dos arquivos nomeados nos argumentos de cada invocação, e a opção inplace diz a ela para "pegar" magicamente a saída e usá-la para substituir cada linha.

Observe que o método replace da string do Python não usa regexps; Se você precisar deles, precisará import re e usar print re.sub(line, "blah", "blee") . Eles são RegExps Perl-Compatible, que são versões muito fortificadas daquelas que você obtém com sed -r .

editar

Como akira menciona nos comentários, a versão original usando um glob ( ls -f *.txt ) no lugar do comando find não funcionaria porque os globs são processados pelo próprio shell ( bash ). Isso significa que antes que o comando seja executado, 10 milhões de nomes de arquivos serão substituídos na linha de comando. Isso é praticamente garantido para exceder o tamanho máximo da lista de argumentos de um comando. Você pode usar xargs --show-limits para informações específicas do sistema sobre isso.

O tamanho máximo da lista de argumentos também é levado em conta por xargs , o que limita o número de argumentos que ele passa para cada invocação de python de acordo com esse limite. Como xargs ainda terá que invocar Python algumas vezes, a sugestão de akira de usar os.path.walk para obter a listagem de arquivos provavelmente economizará algum tempo.

    
por 14.03.2011 / 07:07
0

Tente:

ls | while read file; do (something to $file); done
    
por 14.03.2011 / 03:29

Tags