Qual é a maneira mais eficiente de contar quantos arquivos existem em um diretório?

52

CentOS 5.9

Eu me deparei com um problema no outro dia em que um diretório tinha muitos arquivos. Para contar, eu corri ls -l /foo/foo2/ | wc -l

Acontece que havia mais de 1 milhão de arquivos em um único diretório (longa história - a causa raiz está sendo corrigida).

Minha pergunta é: existe uma maneira mais rápida de fazer a contagem? Qual seria a maneira mais eficiente de obter a contagem?

    
por Mike B 10.09.2013 / 21:33

13 respostas

54

Resposta curta:

\ls -afq | wc -l

(Isso inclui . e .. , então subtraia 2.)

Quando você lista os arquivos em um diretório, três coisas comuns podem acontecer:

  1. Enumerando os nomes dos arquivos no diretório. Isso é inescapável: não há como contar os arquivos em um diretório sem enumerá-los.
  2. Classificando os nomes dos arquivos. Os curingas da shell e o comando ls fazem isso.
  3. Chamando stat para recuperar metadados sobre cada entrada de diretório, como, por exemplo, se é um diretório.

# 3 é o mais caro, de longe, porque requer o carregamento de um inode para cada arquivo. Em comparação, todos os nomes de arquivos necessários para o nº 1 são armazenados de maneira compacta em alguns blocos. # 2 desperdiça algum tempo de CPU, mas muitas vezes não é um disjuntor de negócio.

Se não houver novas linhas em nomes de arquivos, um simples ls -A | wc -l informará quantos arquivos existem no diretório. Tenha em atenção que, se tiver um alias para ls , pode desencadear uma chamada para stat (por exemplo, ls --color ou ls -F precisa de saber o tipo de ficheiro, o que requer uma chamada para stat ). linha de comando, chame command ls -A | wc -l ou \ls -A | wc -l para evitar um alias.

Se houver novas linhas no nome do arquivo, se as novas linhas são listadas ou não, depende da variante Unix. GNU coreutils e BusyBox assumem o padrão de exibir ? para uma nova linha, então eles são seguros.

Chame ls -f para listar as entradas sem classificá-las (# 2). Isso ativa automaticamente -a (pelo menos em sistemas modernos). A opção -f está em POSIX, mas com status opcional; a maioria das implementações suportam, mas não o BusyBox. A opção -q substitui caracteres não imprimíveis, incluindo novas linhas por ? ; é POSIX, mas não é suportado pelo BusyBox, portanto, omita-o se você precisar do suporte do BusyBox às custas de arquivos de contagem excessiva cujo nome contém um caractere de nova linha.

Se o diretório não tiver subdiretórios, a maioria das versões de find não chamará stat em suas entradas (otimização do diretório folha: um diretório com uma contagem de links de 2 não pode ter subdiretórios, portanto find doesn ' Não é necessário pesquisar os metadados das entradas, a menos que uma condição como -type exija isso. Portanto, find . | wc -l é uma maneira rápida e portátil de contar arquivos em um diretório, desde que o diretório não tenha subdiretórios e que nenhum nome de arquivo contenha uma nova linha.

Se o diretório não tiver subdiretórios, mas os nomes dos arquivos contiverem novas linhas, tente um desses (o segundo deve ser mais rápido se for suportado, mas pode não ser notavelmente).

find -print0 | tr -dc \0 | wc -c
find -printf a | wc -c

Por outro lado, não use find se o diretório tiver subdiretórios: mesmo find . -maxdepth 1 chamadas stat em cada entrada (pelo menos com o find do GNU find e BusyBox). Você evita a classificação (# 2), mas paga o preço de uma pesquisa de inode (# 3) que mata o desempenho.

No shell sem ferramentas externas, você pode executar a contagem dos arquivos no diretório atual com set -- *; echo $# . Isso perde os arquivos de ponto (arquivos cujo nome começa com . ) e relata 1 em vez de 0 em um diretório vazio. Esta é a maneira mais rápida de contar arquivos em pequenos diretórios, porque não requer iniciar um programa externo, mas (exceto em zsh) desperdiça tempo para diretórios maiores devido à etapa de ordenação (# 2).

  • No bash, essa é uma maneira confiável de contar os arquivos no diretório atual:

    shopt -s dotglob nullglob
    a=(*)
    echo ${#a[@]}
    
  • No ksh93, esta é uma maneira confiável de contar os arquivos no diretório atual:

    FIGNORE='(.|..)'
    a=(~(N)*)
    echo ${#a[@]}
    
  • No zsh, essa é uma maneira confiável de contar os arquivos no diretório atual:

    a=(*(DNoN))
    echo $#a
    

    Se você tiver a opção mark_dirs definida, certifique-se de desativá-la: a=(*(DNoN^M)) .

  • Em qualquer shell POSIX, essa é uma maneira confiável de contar os arquivos no diretório atual:

    total=0
    set -- *
    if [ $# -ne 1 ] || [ -e "$1" ] || [ -L "$1" ]; then total=$((total+$#)); fi
    set -- .[!.]*
    if [ $# -ne 1 ] || [ -e "$1" ] || [ -L "$1" ]; then total=$((total+$#)); fi
    set -- ..?*
    if [ $# -ne 1 ] || [ -e "$1" ] || [ -L "$1" ]; then total=$((total+$#)); fi
    echo "$total"
    

Todos esses métodos classificam os nomes dos arquivos, exceto o zsh one.

    
por 11.09.2013 / 02:30
15
find /foo/foo2/ -maxdepth 1 | wc -l

É consideravelmente mais rápido na minha máquina, mas o diretório . local é adicionado à contagem.

    
por 10.09.2013 / 22:40
8

ls -1U antes de o pipe gastar um pouco menos de recursos, pois não tenta ordenar as entradas do arquivo, apenas as lê à medida que são classificadas na pasta no disco. Também produz menos saída, o que significa um pouco menos de trabalho para wc .

Você também pode usar ls -f , que é mais ou menos um atalho para ls -1aU .

Não sei se existe uma maneira eficiente de usar isso por meio de um comando sem canalização.

    
por 10.09.2013 / 21:42
6

Outro ponto de comparação. Apesar de não ser um shell oneliner, este programa C não faz nada de superfluido. Observe que os arquivos ocultos são ignorados para corresponder à saída de ls|wc -l ( ls -l|wc -l é desativado em um devido ao total de blocos na primeira linha de saída).

#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
#include <error.h>
#include <errno.h>

int main(int argc, char *argv[])
{
    int file_count = 0;
    DIR * dirp;
    struct dirent * entry;

    if (argc < 2)
        error(EXIT_FAILURE, 0, "missing argument");

    if(!(dirp = opendir(argv[1])))
        error(EXIT_FAILURE, errno, "could not open '%s'", argv[1]);

    while ((entry = readdir(dirp)) != NULL) {
        if (entry->d_name[0] == '.') { /* ignore hidden files */
            continue;
        }
        file_count++;
    }
    closedir(dirp);

    printf("%d\n", file_count);
}
    
por 10.09.2013 / 22:50
3

Você pode tentar perl -e 'opendir($dh,".");$i=0;while(readdir $dh){$i++};print "$i\n";'

Seria interessante comparar as temporizações com o seu pipe de concha.

    
por 10.09.2013 / 22:00
1

Uma solução somente bash, não exigindo nenhum programa externo, mas não sabe o quanto eficiente:

list=(*)
echo "${#list[@]}"
    
por 10.09.2013 / 22:55
1

De esta resposta, posso pensar nisso como uma possível solução.

/*
 * List directories using getdents() because ls, find and Python libraries
 * use readdir() which is slower (but uses getdents() underneath.
 *
 * Compile with 
 * ]$ gcc  getdents.c -o getdents
 */
#define _GNU_SOURCE
#include <dirent.h>     /* Defines DT_* constants */
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/syscall.h>

#define handle_error(msg) \
       do { perror(msg); exit(EXIT_FAILURE); } while (0)

struct linux_dirent {
   long           d_ino;
   off_t          d_off;
   unsigned short d_reclen;
   char           d_name[];
};

#define BUF_SIZE 1024*1024*5

int
main(int argc, char *argv[])
{
   int fd, nread;
   char buf[BUF_SIZE];
   struct linux_dirent *d;
   int bpos;
   char d_type;

   fd = open(argc > 1 ? argv[1] : ".", O_RDONLY | O_DIRECTORY);
   if (fd == -1)
       handle_error("open");

   for ( ; ; ) {
       nread = syscall(SYS_getdents, fd, buf, BUF_SIZE);
       if (nread == -1)
           handle_error("getdents");

       if (nread == 0)
           break;

       for (bpos = 0; bpos < nread;) {
           d = (struct linux_dirent *) (buf + bpos);
           d_type = *(buf + bpos + d->d_reclen - 1);
           if( d->d_ino != 0 && d_type == DT_REG ) {
              printf("%s\n", (char *)d->d_name );
           }
           bpos += d->d_reclen;
       }
   }

   exit(EXIT_SUCCESS);
}

Copie o programa C acima no diretório em que os arquivos precisam ser listados. Em seguida, execute os comandos abaixo.

gcc  getdents.c -o getdents
./getdents | wc -l
    
por 08.08.2014 / 01:02
1

Provavelmente, a maneira mais eficiente de recursos não envolveria invocações de processo externas. Então eu apostaria em ...

cglb() ( c=0 ; set --
    tglb() { [ -e "$2" ] || [ -L "$2" ] &&
       c=$(($c+$#-1))
    }
    for glb in '.?*' \*
    do  tglb $1 ${glb##.*} ${glb#\*}
        set -- ..
    done
    echo $c
)
    
por 08.08.2014 / 00:42
0

Depois de corrigir o problema da resposta do @Joel, onde ele adicionou . como um arquivo:

find /foo/foo2 -maxdepth 1 | tail -n +2 | wc -l

tail simplesmente remove a primeira linha, o que significa que . não é mais contado.

    
por 11.09.2013 / 06:23
0

os.listdir () em python pode fazer o trabalho para você. Ele fornece uma matriz do conteúdo do diretório, excluindo o especial '.' e arquivos '..'. Além disso, não é necessário se preocupar com arquivos abt com caracteres especiais como '\ n' no nome.

python -c 'import os;print len(os.listdir("."))'

a seguir é o tempo gasto pelo comando python acima comparado com o comando 'ls -Af'.

~/test$ time ls -Af |wc -l
399144

real    0m0.300s
user    0m0.104s
sys     0m0.240s
~/test$ time python -c 'import os;print len(os.listdir("."))'
399142

real    0m0.249s
user    0m0.064s
sys     0m0.180s
    
por 16.09.2013 / 22:47
0

ls -1 | wc -l vem imediatamente à minha mente. Se ls -1U é mais rápido que ls -1 é puramente acadêmico - a diferença deve ser insignificante, mas para diretórios muito grandes.

    
por 08.08.2014 / 00:58
0

Eu sei que isso é antigo, mas sinto que awk tem para ser mencionado aqui. As sugestões que incluem o uso de wc simplesmente não estão corretas em relação à pergunta do OP: "a maneira mais eficiente de recursos". Eu recentemente tive um arquivo de log ficar fora de controle (devido a algum software ruim) e, portanto, tropeçou neste post. Houve cerca de 232 milhões de entradas! Eu tentei pela primeira vez wc -l e esperei 15 minutos - nem sequer consegui terminar de contar as linhas. A instrução awk a seguir me deu uma contagem de linha precisa em 3 minutos nesse arquivo de log. Eu aprendi ao longo dos anos a nunca subestimar a capacidade do awk de simular programas shell padrão de uma forma muito mais eficiente. Espero que ajude alguém como eu. Hacker feliz!

awk 'BEGIN{i=0} {i++} END{print i}' /foo/foo2

E se você precisar substituir um comando como ls para contar arquivos em um diretório:

'#Normal:' awk 'BEGIN{i=0} {i++} END{print i}' <(ls /foo/foo2/)
'#Hidden:' awk 'BEGIN{i=0} {i++} END{print (i-2)}' <(ls -f /foo/foo2/)
    
por 23.12.2015 / 05:53
-2

Eu acho que o echo * seria mais eficiente do que qualquer comando 'ls':

echo * | wc -w
    
por 11.09.2013 / 22:33