Mesclar / classificar / um grande número eficiente de arquivos de texto com eficiência

8

Estou tentando um ingênuo:

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

que falha com:

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

Portanto, para evitar uma solução boba como (cria um enorme arquivo temporário):

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

Embora eu pudesse processar arquivos um por um usando (isso deve reduzir o consumo de memória e estar mais próximo de um mecanismo de streaming):

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

Seguido então por:

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

Existe uma substituição mais simples no estilo unix para: cat * | sort -uquando o número de arquivos atingir MAX_ARG? Parece estranho escrever um pequeno script de shell para uma tarefa tão comum.

malat
fonte
2
a concatenação é necessária? sortele automaticamente para a entrada de vários arquivos .. mas, em seguida, sort -u *iria falhar com Argument list too longbem suponho
Sundeep

Respostas:

8

Com o GNU sort, e um shell em que printfestá embutido (todos os similares ao POSIX atualmente, exceto algumas variantes de pdksh):

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

Agora, um problema com isso é que, como os dois componentes desse pipeline são executados simultaneamente e de forma independente, no momento em que o esquerdo expande o *globo, o direito pode já ter criado o outputarquivo, o que poderia causar problemas (talvez não -uaqui) como outputseria um arquivo de entrada e saída, você pode querer que a saída vá para outro diretório ( > ../outputpor exemplo) ou verifique se o glob não corresponde ao arquivo de saída.

Outra maneira de resolvê-lo neste caso é escrevê-lo:

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

Dessa forma, está sortabrindo outputpara gravação e (nos meus testes), não o fará antes de receber a lista completa de arquivos (muito tempo depois que a glob foi expandida). Também evitará estroboscópios outputse nenhum dos arquivos de entrada for legível.

Outra maneira de escrevê-lo com zshoubash

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

Isso está usando substituição de processo (onde <(...)é substituído por um caminho de arquivo que se refere ao final de leitura do pipe no qual printfestá sendo gravado). Esse recurso vem ksh, mas kshinsiste em fazer a expansão de <(...)um argumento separado para o comando, para que você não possa usá-lo com a --option=<(...)sintaxe. Porém, ele funcionaria com esta sintaxe:

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

Observe que você verá uma diferença das abordagens que alimentam a saída dos catarquivos nos casos em que existem arquivos que não terminam com um caractere de nova linha:

$ 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

Observe também que sortclassifica usando o algoritmo de intercalação no locale ( strcollate()) e sort -urelata um de cada conjunto de linhas que são iguais por esse algoritmo, não linhas únicas no nível de bytes. Se você se preocupa apenas com as linhas serem únicas no nível de bytes e não se importa muito com a ordem em que elas são classificadas, convém fixar o código do idioma em C, onde a classificação se baseia nos valores de bytes ( memcmp(); isso provavelmente aceleraria coisas significativamente):

printf '%s\0' * | LC_ALL=C sort -u --files0-from=- -o output
Stéphane Chazelas
fonte
Parece mais natural escrever, isso também dá a oportunidade de sortotimizar seu consumo de memória. Ainda acho printf '%s\0' *um pouco complexo para digitar.
Malat 16/05/19
Você poderia usar em find . -type f -maxdepth 1 -print0vez de printf '%s\0' *, mas não posso afirmar que é mais fácil digitar. E o último é mais fácil de definir como um apelido, é claro!
Toby Speight
@TobySpeight echotem um -n, eu teria algo preferido como printf -0 %seste parece um pouco menos do que o baixo nível'%s\0'
malat
@Toby -maxdepthe -print0são extensões GNU (embora amplamente suportadas atualmente). Com outros finds (embora se você tiver a classificação GNU, é provável que o GNU encontre também), é possível LC_ALL=C find . ! -name . -prune -type f ! -name '.*' -exec printf '%s\0' {} +( LC_ALL=Cainda excluir arquivos ocultos que contêm caracteres inválidos, mesmo com o GNU find), mas isso é um pouco exagerado quando você geralmente tem printfembutido.
Stéphane Chazelas
2
@malat, você sempre pode definir uma print0função como print0() { [ "$#" -eq 0 ] || printf '%s\0' "$@";}e depoisprint0 * | sort...
Stéphane Chazelas
11

Uma correção simples funciona pelo menos no Bash, pois printfestá embutida e os limites do argumento da linha de comando não se aplicam a ela:

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

( echo * | xargstambém funcionaria, exceto no tratamento de nomes de arquivos com espaço em branco etc.)

ilkkachu
fonte
Essa parece ser uma resposta melhor do que a aceita, pois não requer um catprocesso separado para cada arquivo.
LarsH 15/05
4
@LarsH, agrupa find -exec {} +vários arquivos por uma execução. Com find -exec \;isso haveria um gato por arquivo.
Ilkkachu
Ah, bom saber. (Padding)
LarsH
9
find . -maxdepth 1 -type f ! -name ".*" -exec cat {} + | sort -u -o /path/to/sorted.txt

Isso concatenará todos os arquivos regulares não ocultos no diretório atual e classificará o conteúdo combinado (ao remover linhas duplicadas) no arquivo /path/to/sorted.txt.

Kusalananda
fonte
Eu estava tentando usar apenas dois arquivos por vez para evitar consumir muita memória (meu número de arquivos é bastante grande). Você acredita |que as operações serão encadeadas corretamente para limitar o uso de memória?
malat 15/05/19
2
O @malat sortfará uma classificação fora do núcleo se os requisitos de memória exigirem. O lado esquerdo do pipeline consumirá muito pouca memória em comparação.
Kusalananda
1

Eficiência é um termo relativo, então você realmente precisa especificar qual fator deseja minimizar; CPU, memória, disco, tempo etc. Por uma questão de argumento, presumirei que você desejasse minimizar o uso de memória e estivesse disposto a gastar mais ciclos de CPU para conseguir isso. Soluções como a fornecida por Stéphane Chazelas funcionam bem

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

mas eles assumem que os arquivos de texto individuais têm um alto grau de exclusividade para começar. Se não o fizerem, ou seja, se depois

sort -u < sample.txt > sample.srt

sample.srt é 10% menor que o sample.txt, e você economiza memória significativa removendo as duplicatas dos arquivos antes de mesclar. Você também economizará ainda mais memória ao não encadear os comandos, o que significa que os resultados de diferentes processos não precisam estar na memória ao mesmo tempo.

find /somedir -maxdepth 1 type f -exec sort -u -o {} {} \;
sort -u --files0-from <(printf '%s\0' *) > ../output
Paul Smith
fonte
1
O uso de memória raramente é uma preocupação, sortpois sortrecorre ao uso de arquivos temporários quando o uso de memória ultrapassa um limite (geralmente relativamente pequeno). base64 /dev/urandom | sort -uencherá seu disco, mas não consumirá muita memória.
Stéphane Chazelas
Bem, pelo menos é o caso da maioria das sortimplementações, incluindo a original no Unix v3 em 1972, mas aparentemente não busybox sort. Presumivelmente, porque esse é destinado a rodar em pequenos sistemas que não possuem armazenamento permanente.
Stéphane Chazelas
Observe que yes | sort -u(todos os dados duplicados) não precisam usar mais do que alguns bytes de memória e muito menos disco. Mas com o GNU e o Solaris, sortpelo menos, vemos gravando muitos arquivos grandes de 2 bytes /tmp( y\npara cada poucos megabytes de entrada), para que acabe preenchendo o disco eventualmente.
Stéphane Chazelas
0

Como @ilkkachu, mas o gato (1) é desnecessário:

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

Além disso, se os dados forem tão longos, talvez você queira usar a opção sort (1) --parallel = N

Quando N é o número de CPUs que o seu computador possui

Udi
fonte