¿Cuál es la forma más eficiente en cuanto a recursos para contar cuántos archivos hay en un directorio?

55

CentOS 5.9

Me encontré con un problema el otro día en el que un directorio tenía muchos archivos. Para contarlo, corríls -l /foo/foo2/ | wc -l

Resulta que había más de 1 millón de archivos en un solo directorio (larga historia, la causa raíz se está solucionando).

Mi pregunta es: ¿hay una forma más rápida de hacer el recuento? ¿Cuál sería la forma más eficiente de obtener el recuento?

Mike B
fuente
55
ls -l|wc -lestaría apagado por uno debido a los bloques totales en la primera línea de ls -lsalida
Thomas Nyman
3
@ThomasNyman En realidad, sería desactivado por varios debido a las pseudo entradas dot y dotdot, pero se pueden evitar mediante el uso de la -Abandera. -lTambién es problemático debido a la lectura de metadatos del archivo para generar el formato de lista extendida. Forzar NO -lmediante el uso \lses una opción mucho mejor ( -1se asume cuando se canaliza la salida). Consulte la respuesta de Gilles para obtener la mejor solución aquí.
Caleb
2
@Caleb ls -lno genera ningún archivo oculto ni las entradas .y ... ls -ala salida incluye archivos ocultos, incluidos . y ..mientras que la ls -Asalida incluye archivos ocultos excluyendo . y ... En la respuesta de Gilles, la dotglob opción bash shell hace que la expansión incluya archivos ocultos excluyendo . y ...
Thomas Nyman

Respuestas:

61

Respuesta corta:

\ls -afq | wc -l

(Esto incluye .y .., por lo tanto, reste 2.)


Cuando enumera los archivos en un directorio, pueden suceder tres cosas comunes:

  1. Enumerar los nombres de archivo en el directorio. Esto es ineludible: no hay forma de contar los archivos en un directorio sin enumerarlos.
  2. Ordenar los nombres de los archivos. Los comodines de Shell y el lscomando hacen eso.
  3. Llamar statpara recuperar metadatos sobre cada entrada de directorio, como si es un directorio.

El # 3 es el más caro con diferencia, ya que requiere cargar un inodo para cada archivo. En comparación, todos los nombres de archivo necesarios para el n. ° 1 se almacenan de forma compacta en unos pocos bloques. # 2 desperdicia algo de tiempo de CPU pero a menudo no es un factor decisivo.

Si no hay líneas nuevas en los nombres de archivo, un simple ls -A | wc -lle indica cuántos archivos hay en el directorio. Tenga en cuenta que si tiene un alias para ls, esto puede desencadenar una llamada a stat(por ejemplo, ls --coloro ls -Fnecesita saber el tipo de archivo, que requiere una llamada a stat), así que desde la línea de comando, llame command ls -A | wc -lo \ls -A | wc -lpara evitar un alias.

Si hay nuevas líneas en el nombre del archivo, si las nuevas líneas se enumeran o no depende de la variante de Unix. GNU coreutils y BusyBox se muestran ?de forma predeterminada para una nueva línea, por lo que son seguros.

Llame ls -fpara enumerar las entradas sin ordenarlas (# 2). Esto se activa automáticamente -a(al menos en los sistemas modernos). La -fopción está en POSIX pero con estado opcional; la mayoría de las implementaciones lo admiten, pero no BusyBox. La opción -qreemplaza los caracteres no imprimibles, incluidas las nuevas líneas por ?; es POSIX pero no es compatible con BusyBox, así que omítalo si necesita soporte de BusyBox a expensas de contar en exceso los archivos cuyo nombre contiene un carácter de nueva línea.

Si el directorio no tiene subdirectorios, entonces la mayoría de las versiones de findno invocarán statsus entradas (optimización del directorio hoja: un directorio que tiene un recuento de enlaces de 2 no puede tener subdirectorios, por lo findque no necesita buscar los metadatos de las entradas a menos que condición como lo -typerequiere). Entonces, find . | wc -les una forma portátil y rápida de contar archivos en un directorio, siempre que el directorio no tenga subdirectorios y que ningún nombre de archivo contenga una nueva línea.

Si el directorio no tiene subdirectorios pero los nombres de los archivos pueden contener líneas nuevas, pruebe con uno de estos (el segundo debería ser más rápido si es compatible, pero puede que no sea notablemente).

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

Por otro lado, no use findsi el directorio tiene subdirectorios: incluso find . -maxdepth 1llamadas staten cada entrada (al menos con GNU find y BusyBox find). Evita la clasificación (# 2) pero paga el precio de una búsqueda de inodo (# 3) que mata el rendimiento.

En el shell sin herramientas externas, puede ejecutar contar los archivos en el directorio actual con set -- *; echo $#. Esto pierde archivos de puntos (archivos cuyo nombre comienza con .) e informa 1 en lugar de 0 en un directorio vacío. Esta es la forma más rápida de contar archivos en directorios pequeños porque no requiere iniciar un programa externo, pero (excepto en zsh) pierde tiempo para directorios más grandes debido al paso de clasificación (# 2).

  • En bash, esta es una forma confiable de contar los archivos en el directorio actual:

    shopt -s dotglob nullglob
    a=(*)
    echo ${#a[@]}
    
  • En ksh93, esta es una forma confiable de contar los archivos en el directorio actual:

    FIGNORE='@(.|..)'
    a=(~(N)*)
    echo ${#a[@]}
    
  • En zsh, esta es una forma confiable de contar los archivos en el directorio actual:

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

    Si usted tiene el mark_dirsconjunto de opciones, asegúrese de apagarlo: a=(*(DNoN^M)).

  • En cualquier shell POSIX, esta es una forma confiable de contar los archivos en el directorio actual:

    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 estos métodos ordenan los nombres de los archivos, excepto el zsh.

Gilles 'SO- deja de ser malvado'
fuente
1
Mi prueba empírica en> 1 millón de archivos muestra que find -maxdepth 1se mantiene fácilmente siempre \ls -Uque no agregue nada como una -typedeclaración que tiene que hacer más comprobaciones. ¿Estás seguro de que GNU encuentra realmente llamadas stat? Incluso la desaceleración find -typeno es nada en comparación con la cantidad de ls -lpantanos si hace que devuelva los detalles del archivo. Por otro lado, el ganador de velocidad clara está zshusando el globo no clasificador. (los globos clasificados son 2 veces más lentos que lsmientras que los no clasificados son 2 veces más rápidos). Me pregunto si los tipos de sistemas de archivos afectarían significativamente estos resultados.
Caleb
@Caleb corrí strace. Esto solo es cierto si el directorio tiene subdirectorios: de lo contrario find, la optimización del directorio hoja se activa (incluso sin -maxdepth 1), debería haber mencionado eso. Muchas cosas pueden afectar el resultado, incluido el tipo de sistema de archivos (la llamada states mucho más costosa en sistemas de archivos que representan directorios como listas lineales que en sistemas de archivos que representan directorios como árboles), si los inodos se crearon todos juntos y, por lo tanto, están cerca en el disco, caché frío o caliente, etc.
Gilles 'SO- deja de ser malo'
1
Históricamente, ls -fha sido la forma confiable de evitar llamadas stat: esto a menudo se describe simplemente hoy como "la salida no está ordenada" (que también causa), e incluye .y ... -Ay -Uno son opciones estándar.
Random832
1
Si desea contar específicamente un archivo con una extensión común (u otra cadena), insertar eso en el comando elimina el 2. extra. Aquí hay un ejemplo:\ls -afq *[0-9].pdb | wc -l
Steven C. Howell
Para su información, con ksh93 version sh (AT&T Research) 93u+ 2012-08-01en mi sistema basado en Debian, FIGNOREno parece funcionar. Las entradas .y ..se incluyen en la matriz resultante
Sergiy Kolodyazhnyy
17
find /foo/foo2/ -maxdepth 1 | wc -l

Es considerablemente más rápido en mi máquina, pero el .directorio local se agrega al recuento.

Joel Taylor
fuente
1
Gracias. Sin embargo, me veo obligado a hacer una pregunta tonta: ¿por qué es más rápido? ¿Porque no es molesto buscar atributos de archivo?
Mike B
2
Sí, ese es mi entendimiento. Siempre y cuando no esté utilizando el -typeparámetro finddebería ser más rápido quels
Joel Taylor
1
Hmmm ... si entiendo bien la documentación de find , esto debería ser mejor que mi respuesta. ¿Alguien con más experiencia puede verificar?
Luis Machuca
Agregue a -mindepth 1para omitir el directorio en sí.
Stéphane Chazelas
8

ls -1Uantes de que la tubería gaste un poco menos de recursos, ya que no intenta ordenar las entradas del archivo, solo las lee a medida que se ordenan en la carpeta del disco. También produce menos salida, lo que significa un poco menos de trabajo wc.

También podría usar ls -fcuál es más o menos un atajo para ls -1aU.

Sin embargo, no sé si hay una forma eficiente de recursos para hacerlo a través de un comando sin tuberías.

Luis Machuca
fuente
8
Por cierto, -1 está implícito cuando la salida va a una tubería
enzotib
@enzotib: ¿lo es? Wow ... ¡uno aprende algo nuevo todos los días!
Luis Machuca
6

Otro punto de comparación. Si bien no es un shell oneliner, este programa C no hace nada superfluo. Tenga en cuenta que los archivos ocultos se ignoran para que coincidan con la salida de ls|wc -l( ls -l|wc -lestá desactivado en uno debido a los bloques totales en la primera línea de salida).

#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);
}
Thomas Nyman
fuente
El uso de la readdir()API stdio agrega cierta sobrecarga y no le da control sobre el tamaño del búfer pasado a la llamada del sistema subyacente ( getdentsen Linux)
Stéphane Chazelas
3

Tu podrías intentar perl -e 'opendir($dh,".");$i=0;while(readdir $dh){$i++};print "$i\n";'

Sería interesante comparar los tiempos con su tubería de shell.

doneal24
fuente
En mis pruebas, esto mantiene casi exactamente al mismo ritmo que los otros tres soluciones más rápidas ( find -maxdepth 1 | wc -l, \ls -AU | wc -ly el zshpegote no clasificación y recuento de matriz basada). En otras palabras, supera las opciones con varias ineficiencias, como ordenar o leer propiedades de archivos extraños. Me atrevería a decir que ya que tampoco te da nada, no vale la pena usar una solución más simple a menos que ya estés en Perl :)
Caleb
Tenga en cuenta que esto incluirá las entradas del directorio .y ..en el recuento, por lo que debe restar dos para obtener el número real de archivos (y subdirectorios). En Perl moderno, perl -E 'opendir $dh, "."; $i++ while readdir $dh; say $i - 2'lo haría.
Ilmari Karonen
2

A partir de esta respuesta , puedo pensar en esta como una posible solución.

/*
 * 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 el programa C anterior en el directorio en el que deben enumerarse los archivos. Luego ejecute estos comandos:

gcc getdents.c -o getdents
./getdents | wc -l
Ramesh
fuente
1
Algunas cosas: 1) si está dispuesto a usar un programa personalizado para esto, también podría contar los archivos e imprimir el recuento; 2) para comparar ls -f, no filtre d_typeen absoluto, solo en d->d_ino != 0; 3) restar 2 para .y ...
Matei David
Consulte la respuesta vinculada para ver un ejemplo de tiempos en el que es 40 veces más rápido que el aceptado ls -f.
Matei David
1

Una solución solo para bash, que no requiere ningún programa externo, pero no sabe cuánto eficiente:

list=(*)
echo "${#list[@]}"
enzotib
fuente
La expansión global no es necesariamente la forma más eficiente de recursos para hacer esto. Además de la mayoría de los proyectiles que tienen un límite superior para la cantidad de elementos que incluso procesarán, por lo que probablemente bombardeará cuando se trate de más de un millón de elementos, también clasifica la salida. Las soluciones que involucran find o ls sin opciones de clasificación serán más rápidas.
Caleb
@Caleb, solo las versiones antiguas de ksh tenían tales límites (y no admitían esa sintaxis) AFAIK. En todos los demás shells, el límite es solo la memoria disponible. Tienes un punto de que será muy ineficiente, especialmente en bash.
Stéphane Chazelas
1

Probablemente, la forma más eficiente de recursos no implicaría invocaciones de procesos externos. Entonces apostaría por ...

cglb() ( c=0 ; set --
    tglb() { [ -e "$2" ] || [ -L "$2" ] &&
       c=$(($c+$#-1))
    }
    for glb in '.?*' \*
    do  tglb $1 ${glb##.*} ${glb#\*}
        set -- ..
    done
    echo $c
)
mikeserv
fuente
1
¿Tienes números relativos? para cuantos archivos
smci
0

Después de solucionar el problema de la respuesta de @Joel, donde se agregó .como un archivo:

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

tailsimplemente elimina la primera línea, lo que significa que .ya no se cuenta.

haneefmubarak
fuente
1
Agregar un par de tuberías para omitir una línea de wcentrada no es muy eficiente ya que la sobrecarga aumenta linealmente con respecto al tamaño de entrada. En este caso, ¿por qué no simplemente disminuir el recuento final a compensar por ello estar fuera por uno, que es una operación de tiempo constante:echo $(( $(find /foo/foo2 -maxdepth 1 | wc -l) - 1))
Thomas Nyman
1
En lugar de alimentar esa cantidad de datos a través de otro proceso, probablemente sería mejor simplemente hacer algunos cálculos matemáticos en el resultado final. let count = $(find /foo/foo2 -maxdepth 1 | wc -l) - 2
Caleb
0

os.listdir () en python puede hacer el trabajo por usted. Proporciona una matriz de los contenidos del directorio, excluyendo el especial '.' y '...' archivos. Además, no es necesario preocuparse por los archivos abt con caracteres especiales como '\ n' en el nombre.

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

El siguiente es el tiempo que tarda el comando anterior de python en comparación con el comando 'ls -Af'.

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

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

0m0.249s reales
usuario 0m0.064s
sys 0m0.180s
indrajeet
fuente
0

ls -1 | wc -lviene inmediatamente a mi mente Si ls -1Ues más rápido que ls -1puramente académico, la diferencia debería ser insignificante, pero para directorios muy grandes.

contramodo
fuente
0

Para excluir subdirectorios del conteo, aquí hay una variación en la respuesta aceptada de Gilles:

echo $(( $( \ls -afq target | wc -l ) - $( \ls -od target | cut -f2 -d' ') ))

La $(( ))expansión aritmética externa resta la salida del segundo $( )subshell del primero $( ). El primero $( )es exactamente el de Gilles desde arriba. El segundo $( )genera el recuento de directorios "vinculados" al destino. Esto viene de ls -od(sustitúyalo ls -ldsi lo desea), donde la columna que enumera el recuento de enlaces duros tiene un significado especial para los directorios. El recuento de "enlace" incluye ., ..y todos los subdirectorios.

No probé el rendimiento, pero parece ser similar. Agrega una estadística del directorio de destino y algo de sobrecarga para la subshell y la tubería agregadas.

usuario361782
fuente
-2

Creo que echo * sería más eficiente que cualquier comando 'ls':

echo * | wc -w
Dan Garthwaite
fuente
44
¿Qué pasa con los archivos con un espacio en su nombre? echo 'Hello World'|wc -wproduce 2.
Joseph R.
@JosephR. Advertencia Emptor
Dan Garthwaite