Fusionar / ordenar / gran número de archivos de texto de manera eficiente

8

Estoy intentando un ingenuo:

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

que falla con:

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

Entonces, para evitar una solución tonta como (crea un enorme archivo temporal):

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

Pensé que podía procesar archivos uno por uno usando (esto debería reducir el consumo de memoria y estar más cerca de un mecanismo de transmisión):

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

Seguido entonces por:

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

¿Existe un reemplazo más simple de estilo unix para: cat * | sort -ucuando alcanza el número de archivos MAX_ARG? Se siente incómodo escribir un pequeño script de shell para una tarea tan común.

malat
fuente
2
¿Es necesaria la concatenación? sortlo hace automáticamente para la entrada de múltiples archivos ... pero luego sort -u *también fallaría Argument list too long, supongo
Sundeep

Respuestas:

8

Con GNU sort, y un shell donde printfestá incorporado (todos los POSIX como hoy en día excepto algunas variantes de pdksh):

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

Ahora, un problema con eso es que debido a que los dos componentes de esa tubería se ejecutan de manera simultánea e independiente, cuando el izquierdo expande el *globo, el derecho ya puede haber creado el outputarchivo, lo que podría causar un problema (tal vez no -uaquí) como outputsería un archivo de entrada y de salida, por lo que es posible que desee que la salida vaya a otro directorio ( > ../outputpor ejemplo), o asegúrese de que el glob no coincida con el archivo de salida.

Otra forma de abordarlo en este caso es escribirlo:

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

De esa manera, se sortabre outputpara escribir y (en mis pruebas), no lo hará antes de que haya recibido la lista completa de archivos (tanto tiempo después de que el globo se haya expandido). También evitará el clobbering outputsi ninguno de los archivos de entrada es legible.

Otra forma de escribirlo con zshobash

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

Eso está usando la sustitución del proceso (donde <(...)se reemplaza por una ruta de archivo que se refiere al final de la lectura en la que printfse escribe la tubería ). Esa característica proviene ksh, pero kshinsiste en hacer la expansión de <(...)un argumento separado para el comando para que no pueda usarlo con la --option=<(...)sintaxis. Sin embargo, funcionaría con esta sintaxis:

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

Tenga en cuenta que verá una diferencia con respecto a los enfoques que alimentan la salida de catlos archivos en los casos en que hay archivos que no terminan en un carácter de nueva línea:

$ 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

También tenga en cuenta que sortordena usando el algoritmo de intercalación en el locale ( strcollate()), e sort -uinforma uno de cada conjunto de líneas que se clasifican de la misma manera por ese algoritmo, no líneas únicas a nivel de byte. Si solo le importa que las líneas sean únicas a nivel de byte y no le importe tanto el orden en que están ordenadas, es posible que desee fijar la configuración regional en C donde la ordenación se basa en valores de bytes ( memcmp(); eso probablemente aceleraría cosas significativamente):

printf '%s\0' * | LC_ALL=C sort -u --files0-from=- -o output
Stéphane Chazelas
fuente
Se siente más natural al escribir, esto también le da la oportunidad de sortoptimizar su consumo de memoria. Sin embargo, todavía me parece printf '%s\0' *un poco complejo escribir.
Malat
Podría usar en find . -type f -maxdepth 1 -print0lugar de printf '%s\0' *, pero no puedo afirmar que sea más fácil de escribir. ¡Y esto último es más fácil de definir como un alias, por supuesto!
Toby Speight
@TobySpeight echotiene un -n, hubiera preferido algo como printf -0 %sesto parece un nivel un poco menos bajo que'%s\0'
malat
@Toby, -maxdepthy -print0son extensiones GNU (aunque ampliamente compatibles en estos días). Con otros finds (aunque si tiene GNU sort, es probable que GNU encuentre también), puede hacerlo LC_ALL=C find . ! -name . -prune -type f ! -name '.*' -exec printf '%s\0' {} +( LC_ALL=Cpara excluir archivos ocultos que contienen caracteres no válidos, incluso con GNU find), pero eso es un poco excesivo cuando generalmente tener printfincorporado.
Stéphane Chazelas
2
@malat, siempre puedes definir una print0función como print0() { [ "$#" -eq 0 ] || printf '%s\0' "$@";}y luegoprint0 * | sort...
Stéphane Chazelas
11

Una solución simple, funciona al menos en Bash, ya que printfestá integrada, y los límites del argumento de la línea de comando no se aplican a ella:

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

( echo * | xargstambién funcionaría, excepto para el manejo de nombres de archivos con espacios en blanco, etc.)

ilkkachu
fuente
Esta parece una mejor respuesta que la aceptada, ya que no requiere generar un catproceso separado para cada archivo.
LarsH
44
@LarsH, agrupa find -exec {} +varios archivos por una ejecución. Con find -exec \;él sería un gato por archivo.
ilkkachu
Ah, es bueno saberlo. (
Relleno
9
find . -maxdepth 1 -type f ! -name ".*" -exec cat {} + | sort -u -o /path/to/sorted.txt

Esto concatenará todos los archivos regulares no ocultos en el directorio actual y ordenará sus contenidos combinados (mientras elimina las líneas duplicadas) en el archivo /path/to/sorted.txt.

Kusalananda
fuente
Intenté usar solo dos archivos a la vez para evitar consumir mucha memoria (mi número de archivos es bastante grande). ¿Crees |que encadenará correctamente las operaciones para limitar el uso de memoria?
Malat
2
@malat sorthará una ordenación fuera de núcleo si los requisitos de memoria lo requieren. El lado izquierdo de la tubería consumirá muy poca memoria en comparación.
Kusalananda
1

La eficiencia es un término relativo, por lo que realmente debe especificar qué factor desea minimizar; CPU, memoria, disco, tiempo, etc. En aras de la discusión, voy a suponer que desea minimizar el uso de memoria y está dispuesto a gastar más ciclos de CPU para lograrlo. Soluciones como la que da Stéphane Chazelas funcionan bien

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

pero suponen que los archivos de texto individuales tienen un alto grado de unicidad para comenzar. Si no lo hacen, es decir, si después

sort -u < sample.txt > sample.srt

sample.srt es más de un 10% más pequeño que sample.txt, entonces ahorrará una cantidad considerable de memoria al eliminar los duplicados dentro de los archivos antes de fusionarlos. También ahorrará aún más memoria al no encadenar los comandos, lo que significa que los resultados de diferentes procesos no necesitan estar en la memoria al mismo tiempo.

find /somedir -maxdepth 1 type f -exec sort -u -o {} {} \;
sort -u --files0-from <(printf '%s\0' *) > ../output
Paul Smith
fuente
1
El uso de la memoria rara vez es una preocupación, sortya que sortrecurre al uso de archivos temporales cuando el uso de la memoria va más allá de un umbral (generalmente relativamente pequeño). base64 /dev/urandom | sort -ullenará su disco pero no usará mucha memoria.
Stéphane Chazelas
Bueno, al menos es el caso de la mayoría de las sortimplementaciones, incluida la original en Unix v3 en 1972, pero aparentemente no de busybox sort. Presumiblemente porque está destinado a ejecutarse en sistemas pequeños que no tienen almacenamiento permanente.
Stéphane Chazelas
Tenga en cuenta que yes | sort -u(todos los datos duplicados) no tiene que usar más de unos pocos bytes de memoria y mucho menos el disco. Pero con GNU y Solaris sortal menos, vemos que escribe una gran cantidad de archivos grandes de 2 bytes /tmp( y\npor cada pocos megabytes de entrada) para que finalmente termine llenando el disco.
Stéphane Chazelas
0

Como @ilkkachu, pero el gato (1) es innecesario:

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

Además, si los datos son tan largos, quizás desee utilizar la opción sort (1) --parallel = N

Cuando N es el número de CPU que tiene tu computadora

Udi
fuente