Fusionner / trier efficacement / un grand nombre unique de fichiers texte

8

J'essaie un naïf:

$ cat * | sort -u > /tmp/bla.txt

qui échoue avec:

-bash: /bin/cat: Argument list too long

Donc, pour éviter une solution idiote comme (crée un énorme fichier temporaire):

$ find . -type f -exec cat {} >> /tmp/unsorted.txt \;
$ cat /tmp/unsorted.txt | sort -u > /tmp/bla.txt

Je pensais que je pouvais traiter les fichiers un par un en utilisant (cela devrait réduire la consommation de mémoire et être plus proche d'un mécanisme de streaming):

$ cat proc.sh
#!/bin/sh
old=/tmp/old.txt
tmp=/tmp/tmp.txt
cat $old "$1" | sort -u > $tmp
mv $tmp $old

Suivi ensuite par:

$ touch /tmp/old.txt
$ find . -type f -exec /tmp/proc.sh {} \;

Existe-t-il un remplacement plus simple de style Unix pour: cat * | sort -ulorsque le nombre de fichiers atteint MAX_ARG? Il est difficile d'écrire un petit script shell pour une tâche aussi courante.

malat
la source
2
la concaténation est-elle nécessaire? sortle fait automatiquement pour plusieurs entrées de fichiers .. mais sort -u *échouerait Argument list too longaussi avec je suppose
Sundeep

Réponses:

8

Avec GNU sort, et un shell où printfest intégré (tous ceux de type POSIX de nos jours sauf quelques variantes de pdksh):

printf '%s\0' * | sort -u --files0-from=- > output

Maintenant, un problème avec cela est que parce que les deux composants de ce pipeline sont exécutés simultanément et indépendamment, au moment où celui de gauche développe le *glob, le droit peut avoir outputdéjà créé le fichier, ce qui pourrait poser problème (peut-être pas avec -uici) comme ce outputserait à la fois un fichier d'entrée et de sortie, vous voudrez peut-être que la sortie aille dans un autre répertoire ( > ../outputpar exemple), ou assurez-vous que le glob ne correspond pas au fichier de sortie.

Une autre façon de le résoudre dans ce cas est de l'écrire:

printf '%s\0' * | sort -u --files0-from=- -o output

De cette façon, il sorts'ouvre outputpour l'écriture et (dans mes tests), il ne le fera pas avant d'avoir reçu la liste complète des fichiers (tant de temps après que le glob ait été développé). Cela évitera également de vous encombrer outputsi aucun des fichiers d'entrée n'est lisible.

Une autre façon de l'écrire avec zshoubash

sort -u --files0-from=<(printf '%s\0' *) -o output

Cela utilise la substitution de processus (où <(...)est remplacé par un chemin de fichier qui se réfère à la fin de lecture du canal d' printfécriture). Cette fonctionnalité vient de ksh, mais kshinsiste pour rendre l'expansion d' <(...)un argument séparé à la commande afin que vous ne puissiez pas l'utiliser avec la --option=<(...)syntaxe. Cela fonctionnerait cependant avec cette syntaxe:

sort -u --files0-from <(printf '%s\0' *) -o output

Notez que vous verrez une différence avec les approches qui alimentent la sortie de catsur les fichiers dans les cas où il y a des fichiers qui ne se terminent pas par un caractère de nouvelle ligne:

$ printf a > a
$ printf b > b
$ printf '%s\0' a b | sort -u --files0-from=-
a
b
$ printf '%s\0' a b | xargs -r0 cat | sort -u
ab

Notez également que sorttrie en utilisant l'algorithme de classement dans les paramètres régionaux ( strcollate()), et sort -usignale une de chaque ensemble de lignes qui trient la même chose par cet algorithme, pas des lignes uniques au niveau de l'octet. Si vous ne vous souciez que des lignes uniques au niveau de l'octet et ne vous souciez pas tellement de l'ordre dans lequel elles sont triées, vous souhaiterez peut-être fixer les paramètres régionaux à C où le tri est basé sur les valeurs d'octets ( memcmp(); cela accélérerait probablement les choses de manière significative):

printf '%s\0' * | LC_ALL=C sort -u --files0-from=- -o output
Stéphane Chazelas
la source
Se sent plus naturel à écrire, cela donne également l'occasion sortd'optimiser sa consommation de mémoire. Je trouve quand même printf '%s\0' *un peu complexe à taper.
malat
Vous pouvez utiliser à la find . -type f -maxdepth 1 -print0place de printf '%s\0' *, mais je ne peux pas affirmer que c'est plus facile à taper. Et ce dernier est plus facile à définir comme un alias, bien sûr!
Toby Speight
@TobySpeight echoa un -n, j'aurais préféré quelque chose comme printf -0 %sça semble un niveau un peu moins bas que'%s\0'
malat
@Toby, -maxdepthet -print0sont des extensions GNU (bien que largement prises en charge de nos jours). Avec d'autres finds (bien que si vous avez un tri GNU, vous aurez probablement aussi GNU find), vous pouvez le faire LC_ALL=C find . ! -name . -prune -type f ! -name '.*' -exec printf '%s\0' {} +( LC_ALL=Cpour exclure toujours les fichiers cachés qui contiennent des caractères invalides, même avec GNU find), mais c'est un peu exagéré quand vous en général ont printfintégré.
Stéphane Chazelas
2
@malat, vous pourriez toujours définir une print0fonction comme print0() { [ "$#" -eq 0 ] || printf '%s\0' "$@";}puisprint0 * | sort...
Stéphane Chazelas
11

Un correctif simple, fonctionne au moins dans Bash, car il printfest intégré, et les limites des arguments de la ligne de commande ne s'appliquent pas:

printf "%s\0" * | xargs -0 cat | sort -u > /tmp/bla.txt

( echo * | xargsfonctionnerait également, sauf pour la gestion des noms de fichiers avec des espaces blancs, etc.)

ilkkachu
la source
Cela semble être une meilleure réponse que celle acceptée, car elle ne nécessite pas la création d'un catprocessus distinct pour chaque fichier.
LarsH
4
@LarsH, find -exec {} +regroupe plusieurs fichiers par exécution. Avec find -exec \;ce serait un chat par fichier.
ilkkachu
Ah, bon à savoir. (Rembourrage)
LarsH
9
find . -maxdepth 1 -type f ! -name ".*" -exec cat {} + | sort -u -o /path/to/sorted.txt

Cela va concaténer tous les fichiers réguliers non cachés dans le répertoire courant et trier leur contenu combiné (tout en supprimant les lignes dupliquées) dans le fichier /path/to/sorted.txt.

Kusalananda
la source
J'essayais d'utiliser seulement deux fichiers à la fois pour éviter de consommer beaucoup de mémoire (mon nombre de fichiers est assez important). Pensez-vous qu'il |enchaînera correctement les opérations pour limiter l'utilisation de la mémoire?
malat
2
@malat sorteffectuera un tri hors cœur si les besoins en mémoire l'exigent. Le côté gauche du pipeline consommera très peu de mémoire en comparaison.
Kusalananda
1

L'efficacité est un terme relatif, vous devez donc vraiment spécifier quel facteur vous souhaitez minimiser; cpu, mémoire, disque, temps, etc. Pour les besoins de l'argument, je vais supposer que vous vouliez minimiser l'utilisation de la mémoire et êtes prêt à passer plus de cycles de cpu pour y parvenir. Des solutions comme celle de Stéphane Chazelas fonctionnent bien

sort -u --files0-from <(printf '%s\0' *) > ../output

mais ils supposent que les fichiers texte individuels ont un haut degré d'unicité pour commencer. Si ce n'est pas le cas, c'est-à-dire si après

sort -u < sample.txt > sample.srt

sample.srt est plus de 10% plus petit que sample.txt, vous économiserez alors de la mémoire en supprimant les doublons dans les fichiers avant de fusionner. Vous économiserez également davantage de mémoire en ne chaînant pas les commandes, ce qui signifie que les résultats de différents processus n'ont pas besoin d'être en mémoire en même temps.

find /somedir -maxdepth 1 type f -exec sort -u -o {} {} \;
sort -u --files0-from <(printf '%s\0' *) > ../output
Paul Smith
la source
1
L'utilisation de la mémoire est rarement un problème avec sortle sortrecours à des fichiers temporaires lorsque l'utilisation de la mémoire dépasse un seuil (généralement relativement faible). base64 /dev/urandom | sort -uremplira votre disque mais n'utilisera pas beaucoup de mémoire.
Stéphane Chazelas
Eh bien, au moins c'est le cas de la plupart des sortimplémentations, y compris celle d'origine dans Unix v3 en 1972, mais apparemment pas de busybox sort. Vraisemblablement parce que celui-ci est destiné à fonctionner sur de petits systèmes qui n'ont pas de stockage permanent.
Stéphane Chazelas
Notez que yes | sort -u(toutes les données dupliquées) ne doivent pas utiliser plus de quelques octets de mémoire sans parler du disque. Mais avec GNU et Solaris sortau moins, nous le voyons écrire de gros fichiers de 2 octets /tmp( y\npour chaque mégaoctet d'entrée), il finira donc par remplir le disque.
Stéphane Chazelas
0

Comme @ilkkachu, mais le chat (1) n'est pas nécessaire:

printf "%s\0" * | xargs -0 sort -u

De plus, si les données sont si longues, vous voudrez peut-être utiliser l'option sort (1) --parallel = N

Lorsque N est le nombre de CPU que votre ordinateur possède

Udi
la source