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

16

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

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

Bati minha concha, o lsestá em um tilda, mas não consigo descobrir como fazer um.

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

Args em excesso para sed

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

Não foi possível bifurcar mais, não há mais memória

Alguma outra idéia de como criar esse tipo de comando? Os arquivos não precisam se comunicar. ls | wc -lparece funcionar (muito lento), por isso deve ser possível.

Sandro
fonte
11
Seria mais rápido se você pudesse evitar a chamada sedpara cada arquivo. Não tenho certeza se existe uma maneira de abrir, editar, salvar e fechar uma série de arquivos sed; se a velocidade for essencial, convém usar um programa diferente, talvez perl ou python.
intuído
@ intuited: seria ainda mais rápido não fazer nada com os arquivos ... sério? se você deseja alterar um padrão em um conjunto de arquivos, é necessário procurar em cada arquivo para ver se existe o padrão. se você sabe com antecedência que pode pular arquivos de 'alguns', é óbvio que é mais rápido nem tocar nos arquivos. e o tempo de inicialização sedé provavelmente mais rápido do que o lançamento pythonou perltambém, exceto se você fizer tudo nesse intérprete.
akira
@akira: Você está dizendo que iniciar perl ou python uma vez para tantos arquivos quantos caberem em uma linha de comando é mais caro do que iniciar sed uma vez para cada um desses arquivos? Eu ficaria realmente surpreso se fosse esse o caso. —————— Acho que você não entendeu que minha sugestão é invocar (iniciar) o programa de edição uma vez (ou pelo menos menos vezes - veja minha resposta) e abri-lo, modificar e salvar novamente cada um dos arquivos por sua vez, em vez de chamar o programa de edição separadamente para cada um desses arquivos.
intuited
seu primeiro comentário não reflete o que você realmente queria dizer: "substitua sed por python / perl" .. apenas fazendo isso e observando @ a linha de comando que o OP forneceu, um leitor inocente pode assumir que "find. -exec python" é mais rápido que "find. -exec sed" .. o que obviamente não é o caso. em sua própria resposta, você chama python com muito mais frequência do que é realmente necessário.
akira
Acho que akira interpretou mal sua sugestão (intuição). Acredito que você estava sugerindo agrupar arquivos. Tentei fazer isso com meus xargs tentar, tempo para tentar novamente :)
Sandro

Respostas:

19

Faça uma tentativa:

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

Alimentará apenas um nome de arquivo para cada chamada de sed. Isso resolverá o problema "muitos argumentos para sed". A -Popção deve permitir que vários processos sejam bifurcados ao mesmo tempo. Se 0 não funcionar (deve 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.

Pausado até novo aviso.
fonte
3
Provavelmente, ele terá de ser find . -name \*.txt -print0para evitar que o shell expandir o glob e tentando alloc espaço para 10 milhões de argumentos para encontrar .
precisa
@ ChrisJohnsen: Sim, está correto. Corri postando minha resposta e perdi incluindo as partes essenciais. Eu editei minha resposta com essas correções. Obrigado.
Pausado até novo aviso.
Tentar agora ... cruza os dedos
Sandro
7

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

ATUALIZAÇÃO: Agora incluí uma execução de quatro núcleos no 'find |xargs'método (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 saíram quando executadas nos dados de teste mencionados acima. Esses resultados envolvem apenas as despesas gerais básicas; ou seja, 'sed' não foi chamado. O processo sed certamente será o mais demorado, mas pensei que seria interessante ver como os métodos simples eram comparados.

O 'find |xargs'método de Dennis , usando um único núcleo, levou 4 horas e 21 minutos ** a mais do que o bash arraymétodo em uma no sedexecução ... No entanto, a vantagem de vários núcleos oferecida por 'find' deve superar as diferenças de tempo mostradas quando o sed está sendo chamado. processando 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 $'\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
fonte
2

Outra oportunidade para a descoberta completamente segura :

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 )
l0b0
fonte
1

Isso é principalmente fora de 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 ... xargs ... -I {} ... sed ...) é a velocidade: você evita invocar sed10 milhões de vezes. Seria mais rápido ainda se você pudesse evitar o uso do Python (já que o python é meio lento, relativamente), então o perl pode ser uma escolha melhor para esta tarefa. Não sei como fazer o equivalente convenientemente com o perl.

A maneira como isso funciona é que xargsinvocará o Python com o maior número de argumentos possível em uma única linha de comando e continuará fazendo isso até que fique 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 de outras coisas. A fileinput.inputfunção gera linhas sucessivas dos arquivos nomeados nos argumentos de cada chamada, e a inplaceopção diz para "capturar" magicamente a saída e usá-la para substituir cada linha.

Observe que o replacemétodo de string do Python não usa regexps; se você precisar, precisa import ree usa print re.sub(line, "blah", "blee"). Eles são RegExps compatíveis com Perl, que são uma espécie de versões altamente fortificadas daquelas com as quais você se relaciona sed -r.

editar

Como akira menciona nos comentários, a versão original usando um glob ( ls -f *.txt) no lugar do findcomando 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. É praticamente garantido que exceda o tamanho máximo da lista de argumentos de um comando. Você pode usar xargs --show-limitspara obter informações específicas do sistema sobre isso.

O tamanho máximo da lista de argumentos também é levado em consideração por xargs, o que limita o número de argumentos que ele passa para cada chamada de python de acordo com esse limite. Como xargsainda será necessário chamar o python algumas vezes, a sugestão de akira os.path.walkpara obter a lista de arquivos provavelmente economizará algum tempo.

intuído
fonte
11
qual é o ponto de usar o operador glob (que falhará para tantos arquivos assim mesmo) ... e depois alimentar os arquivos para python que possui os.path.walk()?
akira
@akira: glob operator é para evitar tentar substituir o conteúdo de .e ... Certamente, existem outras maneiras de fazer isso (ou seja find), mas estou tentando manter o mais próximo possível do que o OP entende. Este também é o motivo para não usar os.path.walk.
intuído
@akira: Boa sugestão, porém, que provavelmente seria consideravelmente mais rápida.
intuited
Eu acho que o OP vai entender os.path.walkfacilmente.
akira
0

Experimentar:

ls | while read file; do (something to $file); done
Reuben L.
fonte
2
ls -fseria melhor; Deseja realmente esperar stat()e classificar tantos arquivos?
Geekosaur
agora eu estou tentando: para f em * .txt; faça blá; feito. Eu darei um golpe nisso se falhar. Obrigado!
Sandro