Comment exécuter sed sur plus de 10 millions de fichiers dans un répertoire?

16

J'ai un répertoire contenant 10144911 fichiers. Jusqu'à présent, j'ai essayé ce qui suit:

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

J'ai cassé ma coquille, lsc'est dans un tilda mais je ne sais pas comment en faire un.

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

Trop d'arguments pour sed

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

Impossible de bifurquer plus de mémoire

Avez-vous d'autres idées sur la façon de créer cette sorte de commande? Les fichiers n'ont pas besoin de communiquer entre eux. ls | wc -lsemble fonctionner (très lentement) donc ça doit être possible.

Sandro
la source
1
Ce serait plus rapide si vous pouvez éviter d'appeler sedpour chaque fichier. Je ne sais pas s'il existe un moyen d'ouvrir, de modifier, d'enregistrer et de fermer une série de fichiers sed; si la vitesse est essentielle, vous voudrez peut-être utiliser un programme différent, peut-être perl ou python.
intuition
@intuited: il serait encore plus rapide de ne rien faire du tout ... sérieusement? si vous voulez changer un motif dans un ensemble de fichiers, vous devez regarder dans chaque fichier pour voir s'il y a un motif. si vous savez à l'avance que vous pouvez ignorer «certains» fichiers, il est évident qu'il est plus rapide de ne même pas toucher les fichiers. et le temps de démarrage de sedest probablement plus rapide que le lancement pythonou perlaussi, sauf si vous faites tout dans cet interpréteur.
akira
@akira: Êtes-vous en train de dire que lancer perl ou python une fois pour autant de fichiers qu'il y en aura sur une ligne de commande est plus cher que de lancer sed une fois pour chacun de ces fichiers? Je serais vraiment surpris si tel était le cas. —————— Je suppose que vous n'avez pas compris que ma suggestion est d' invoquer (démarrer) le programme d'édition une fois (ou au moins moins de fois - voir ma réponse), et de le faire ouvrir, modifier et réenregistrer chacun des fichiers à son tour, plutôt que d'appeler le programme d'édition séparément pour chacun de ces fichiers.
intuition
votre premier commentaire ne reflète pas ce que vous vouliez vraiment dire: "remplacez sed par python / perl". plus rapide que "find. -exec sed" .. ce qui n'est évidemment pas le cas. dans votre propre réponse, vous appelez python beaucoup plus souvent que nécessaire.
akira
Je pense que Akira a mal interprété votre suggestion (intuitive). Je pense que vous proposiez de regrouper les fichiers. J'ai essayé avec ma tentative de xargs, il est temps de réessayer :)
Sandro

Réponses:

19

Essayez ceci:

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

Il ne fournira qu'un seul nom de fichier à chaque invocation de sed. Cela résoudra le problème «trop d'arguments pour sed». L' -Poption doit permettre à plusieurs processus d'être bifurqués en même temps. Si 0 ne fonctionne pas (il est censé en exécuter autant que possible), essayez d'autres nombres (10? 100? Le nombre de cœurs que vous avez?) Pour limiter le nombre.

En pause jusqu'à nouvel ordre.
la source
3
Il faudra probablement find . -name \*.txt -print0éviter que le shell n'élargisse le glob et ne tente d'allouer de l'espace pour 10 millions d'arguments à trouver .
Chris Johnsen
@ChrisJohnsen: Oui, c'est exact. Je me suis précipité pour poster ma réponse et j'ai raté d'inclure ces parties essentielles. J'ai édité ma réponse avec ces corrections. Merci.
pause jusqu'à nouvel ordre.
Essayer maintenant ... croise les doigts
Sandro
7

J'ai testé cette méthode (et toutes les autres) sur 10 millions de fichiers (vides), nommés "hello 00000001" à "hello 10000000" (14 octets par nom).

MISE À JOUR: J'ai maintenant inclus une exécution quad-core sur la 'find |xargs'méthode (toujours sans 'sed'; juste 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  

Voici un résumé de la façon dont les réponses fournies se sont comportées lorsqu'elles ont été comparées aux données de test mentionnées ci-dessus. Ces résultats ne concernent que les frais généraux de base; c'est-à-dire que «sed» n'a pas été appelé. Le processus sed sera certainement le plus long, mais j'ai pensé qu'il serait intéressant de voir comment les méthodes nues se comparaient.

La 'find |xargs'méthode de Dennis , utilisant un seul cœur, a pris * 4 heures 21 minutes ** de plus que la bash arrayméthode lors d'une analyse no sed... Cependant, l'avantage multicœur offert par 'find' devrait l'emporter sur les différences de temps indiquées lorsque sed est demandé. traitement des fichiers ...

           | 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
la source
2

Une autre opportunité pour la découverte complètement sûre :

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
la source
1

Ceci est principalement hors sujet, mais vous pouvez utiliser

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

Le principal avantage ici (sur ... xargs ... -I {} ... sed ...) est la vitesse: vous évitez d'invoquersed 10 millions de fois. Ce serait encore plus rapide si vous pouviez éviter d'utiliser Python (car python est un peu lent, relativement), donc perl pourrait être un meilleur choix pour cette tâche. Je ne sais pas comment faire l'équivalent facilement avec perl.

La façon dont cela fonctionne est d' xargsinvoquer Python avec autant d'arguments qu'il peut tenir sur une seule ligne de commande, et de continuer ainsi jusqu'à épuisement des arguments (fournis par ls -f *.txt). Le nombre d'arguments pour chaque invocation dépendra de la longueur des noms de fichiers et, euh, d'autres éléments. La fileinput.inputfonction génère des lignes successives à partir des fichiers nommés dans les arguments de chaque invocation, et l' inplaceoption lui dit de "capturer" par magie la sortie et de l'utiliser pour remplacer chaque ligne.

Notez que la replaceméthode de chaîne de Python n'utilise pas de regexps; si vous en avez besoin, vous devez les import reutiliser print re.sub(line, "blah", "blee"). Ce sont des RegExps compatibles Perl, qui sont en quelque sorte des versions fortement enrichies de celles que vous obtenez sed -r.

Éditer

Comme le mentionne akira dans les commentaires, la version originale utilisant un glob ( ls -f *.txt) à la place de la findcommande ne fonctionnerait pas car les globs sont traités par le shell ( bash) lui-même. Cela signifie qu'avant même l'exécution de la commande, 10 millions de noms de fichiers seront substitués dans la ligne de commande. Il est à peu près garanti de dépasser la taille maximale de la liste d'arguments d'une commande. Vous pouvez utiliser xargs --show-limitspour des informations spécifiques au système à ce sujet.

La taille maximale de la liste d'arguments est également prise en compte par xargs, ce qui limite le nombre d'arguments qu'elle passe à chaque appel de python en fonction de cette limite. Comme il xargsfaudra encore invoquer python plusieurs fois, la suggestion d'Akira d'utiliser os.path.walkpour obtenir la liste des fichiers vous fera probablement gagner du temps.

intuition
la source
1
quel est le point d'utiliser l'opérateur glob (qui échouera pour autant de fichiers de toute façon) ... puis d'alimenter les fichiers en python qui a os.path.walk()?
akira
@akira: l'opérateur glob évite d'essayer de remplacer le contenu de .et ... Il y a certainement d'autres façons de le faire (c'est-à-dire find), mais j'essaie de m'en tenir le plus possible à ce que l'OP comprend. C'est aussi la raison de ne pas utiliser os.path.walk.
intuition
@akira: Bonne suggestion, cependant, ce serait probablement beaucoup plus rapide.
intuition
je pense que OP comprendra os.path.walkassez facilement.
akira
0

Essayer:

ls | while read file; do (something to $file); done
Reuben L.
la source
2
ls -fserait mieux; voulez-vous vraiment attendre stat()et trier autant de fichiers?
geekosaur
en ce moment j'essaye: pour f dans * .txt; faire bof; terminé. Je donnerai un coup de fouet en cas d'échec. Je vous remercie!
Sandro