¿Cómo ejecutar sed en más de 10 millones de archivos en un directorio?

16

Tengo un directorio que contiene 10144911 archivos. Hasta ahora he intentado lo siguiente:

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

Se estrelló mi caparazón, lsestá en una tilda pero no puedo entender cómo hacer uno.

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

Demasiados argumentos para sed

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

No podía bifurcar más, no más memoria

¿Alguna otra idea sobre cómo crear este comando amable? Los archivos no necesitan comunicarse entre sí. ls | wc -lparece funcionar (muy lento), por lo que debe ser posible.

Sandro
fuente
1
Sería más rápido si puede evitar invocar sedcada archivo. No estoy seguro de si hay una manera de abrir, editar, guardar y cerrar una serie de archivos sed; Si la velocidad es esencial, es posible que desee utilizar un programa diferente, tal vez Perl o Python.
intuido
@intuited: sería aún más rápido no hacer nada a los archivos ... ¿en serio? si desea cambiar un patrón en un conjunto de archivos, debe ver cada archivo para ver si existe el patrón. si sabe de antemano que puede omitir 'algunos' archivos, entonces es obvio que es más rápido ni siquiera tocar los archivos. y el tiempo de inicio sedes probablemente más rápido que el inicio pythono perltambién, excepto si hace todo en ese intérprete.
akira
@akira: ¿Estás diciendo que lanzar Perl o Python una vez para tantos archivos como caben en una línea de comando es más costoso que lanzar Sed una vez para cada uno de esos archivos? Me sorprendería mucho si ese fuera el caso. —————— Supongo que no entendiste que mi sugerencia es invocar (iniciar) el programa de edición una vez (o al menos menos veces, ver mi respuesta), y hacer que abra, modifique y vuelva a guardar cada uno de los archivos a su vez, en lugar de invocar el programa de edición por separado para cada uno de esos archivos.
intuido
su primer comentario no refleja lo que realmente quería decir: "reemplace sed por python / perl" ... simplemente haciendo eso y mirando @ la línea de comandos que OP ha dado, un lector inocente podría suponer que "find. -exec python" es más rápido que "find. -exec sed" ... que obviamente no es el caso. en su propia respuesta, llama a python con mucha más frecuencia de la que realmente se necesita.
akira
Creo que akira malinterpretó su sugerencia (intuitiva). Creo que estabas sugiriendo agrupar archivos juntos. Lo intenté con mi intento de xargs, es hora de intentarlo de nuevo :)
Sandro

Respuestas:

19

Prueba esto:

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

Solo alimentará un nombre de archivo a cada invocación de sed. Eso resolverá el problema de "demasiados argumentos para sed". La -Popción debe permitir que se bifurquen varios procesos al mismo tiempo. Si 0 no funciona (se supone que debe ejecutar tantos como sea posible), intente con otros números (10? 100? ¿El número de núcleos que tiene?) Para limitar el número.

Pausado hasta nuevo aviso.
fuente
3
Probablemente, tendrá que ser find . -name \*.txt -print0para evitar que la cáscara de ampliar el pegote y tratando de alloc espacio para 10 millones de argumentos a encontrar .
Chris Johnsen
@ChrisJohnsen: Sí, eso es correcto. Me apresuré a publicar mi respuesta y omití incluir esas partes esenciales. He editado mi respuesta con esas correcciones. Gracias.
Pausado hasta nuevo aviso.
Probándolo ahora ... cruza los dedos
Sandro
7

He probado este método (y todos los demás) en 10 millones de archivos (vacíos), llamados "hola 00000001" a "hola 10000000" (14 bytes por nombre).

ACTUALIZACIÓN: ahora he incluido una ejecución de cuatro núcleos en el 'find |xargs'método (todavía sin 'sed'; solo 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  

Aquí hay un resumen de cómo les fue a las respuestas proporcionadas cuando se ejecutaron contra los datos de prueba mencionados anteriormente. Estos resultados involucran solo los gastos generales básicos; es decir, 'sed' no fue llamado. Es casi seguro que el proceso sed requerirá más tiempo, pero pensé que sería interesante ver cómo se comparan los métodos simples.

El 'find |xargs'método de Dennis , usando un solo núcleo, tomó * 4 horas y 21 minutos ** más tiempo que el bash arraymétodo en una no sedejecución ... Sin embargo, la ventaja multinúcleo que ofrece 'find' debería superar las diferencias de tiempo que se muestran cuando se solicita sed procesando los archivos ...

           | 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
fuente
2

Otra oportunidad para el hallazgo completamente seguro :

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
fuente
1

Esto es principalmente fuera de tema, pero podría 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"),
'

El principal beneficio aquí (más ... xargs ... -I {} ... sed ...) es la velocidad: evitas invocar sed10 millones de veces. Sería aún más rápido si pudieras evitar usar Python (ya que Python es un poco lento, relativamente), por lo que Perl podría ser una mejor opción para esta tarea. No estoy seguro de cómo hacer el equivalente convenientemente con Perl.

La forma en que esto funciona es que xargsinvocará a Python con tantos argumentos como pueda caber en una sola línea de comando, y seguirá haciéndolo hasta que se quede sin argumentos (que están siendo suministrados por ls -f *.txt). El número de argumentos para cada invocación dependerá de la longitud de los nombres de archivo y, um, algunas otras cosas. La fileinput.inputfunción produce líneas sucesivas de los archivos nombrados en los argumentos de cada invocación, y la inplaceopción le dice que "atrape" mágicamente la salida y la use para reemplazar cada línea.

Tenga en cuenta que el replacemétodo de cadena de Python no usa expresiones regulares; si los necesita, debe import reusarlos print re.sub(line, "blah", "blee"). Son RegExps compatibles con Perl, que son una especie de versiones fuertemente fortificadas de las que obtienes sed -r.

editar

Como akira menciona en los comentarios, la versión original que usa un glob ( ls -f *.txt) en lugar del findcomando no funcionaría porque los globos son procesados ​​por el propio shell ( bash). Esto significa que incluso antes de ejecutar el comando, se sustituirán 10 millones de nombres de archivo en la línea de comando. Esto está prácticamente garantizado para superar el tamaño máximo de la lista de argumentos de un comando. Puede utilizar xargs --show-limitspara obtener información específica del sistema sobre esto.

También se tiene en cuenta el tamaño máximo de la lista de argumentos xargs, lo que limita el número de argumentos que pasa a cada invocación de python de acuerdo con ese límite. Dado xargsque todavía tendrá que invocar Python varias veces, la sugerencia de Akira para usar os.path.walkpara obtener la lista de archivos probablemente le ahorrará algo de tiempo.

intuido
fuente
1
¿Cuál es el punto de usar el operador glob (que fallará para tantos archivos de todos modos) ... y luego alimentar los archivos a Python que tiene os.path.walk()?
akira
@akira: el operador global debe evitar intentar reemplazar el contenido de .y ... Ciertamente, hay otras formas de hacerlo (es decir find), pero estoy tratando de mantenerme lo más cerca posible de lo que entiende el OP. Esta es también la razón para no usar os.path.walk.
intuido
@akira: Buena sugerencia, sin embargo, eso probablemente sería considerablemente más rápido.
intuido
Creo que OP entenderá con os.path.walkbastante facilidad.
akira
0

Tratar:

ls | while read file; do (something to $file); done
Reuben L.
fuente
2
ls -fseria mejor; ¿realmente quieres esperar stat()y ordenar tantos archivos?
geekosaur
ahora estoy intentando: para f en * .txt; hacer bla hecho. Le daré un golpe si falla. ¡Gracias!
Sandro