Qual è il modo più efficiente in termini di risorse per contare quanti file ci sono in una directory?


55

CentOS 5.9

L'altro giorno ho riscontrato un problema in cui una directory conteneva molti file. Per contarlo, ho corsols -l /foo/foo2/ | wc -l

Si scopre che c'erano oltre 1 milione di file in una singola directory (lunga storia - la causa principale si sta risolvendo).

La mia domanda è: c'è un modo più veloce per fare il conteggio? Quale sarebbe il modo più efficiente per ottenere il conteggio?


5
ls -l|wc -lsarebbe spento di uno a causa dei blocchi totali nella prima riga di ls -loutput
Thomas Nyman,

3
@ThomasNyman In realtà sarebbe disattivato da molti a causa delle pseudo-voci dot e dotdot, ma quelle possono essere evitate usando la -Abandiera. -lè anche problematico a causa dei metadati del file di lettura per generare il formato elenco esteso. Forzare NOT -lusando \lsè un'opzione molto migliore ( -1si presume quando si esegue l'output delle tubazioni). Vedere la risposta di Gilles per la migliore soluzione qui.
Caleb,

2
@Caleb ls -lnon genera file nascosti né voci .e ... ls -al'output include file nascosti, inclusi . e ..mentre l' ls -Aoutput include file nascosti esclusi . e ... Nella risposta di Gilles l' dotglob opzione bash shell fa sì che l'espansione includa i file nascosti escludendo . e ...
Thomas Nyman,

Risposte:


61

Risposta breve:

\ls -afq | wc -l

(Ciò include .e .., quindi, sottrarre 2.)


Quando si elencano i file in una directory, potrebbero accadere tre cose comuni:

  1. Enumerazione dei nomi dei file nella directory. Questo è inevitabile: non c'è modo di contare i file in una directory senza elencarli.
  2. Ordinamento dei nomi dei file. I caratteri jolly Shell e il lscomando lo fanno.
  3. Chiamata statper recuperare metadati su ciascuna voce della directory, ad esempio se si tratta di una directory.

# 3 è di gran lunga il più costoso, perché richiede il caricamento di un inode per ciascun file. In confronto, tutti i nomi di file necessari per il numero 1 sono memorizzati in modo compatto in pochi blocchi. # 2 spreca un po 'di tempo della CPU ma spesso non è un problema.

Se non ci sono newline nei nomi dei file, un semplice ls -A | wc -lti dice quanti file ci sono nella directory. Attenzione che se si dispone di un alias per ls, ciò può attivare una chiamata a stat(ad esempio ls --coloro è ls -Fnecessario conoscere il tipo di file, che richiede una chiamata a stat), quindi dalla riga di comando, chiamare command ls -A | wc -lo \ls -A | wc -lper evitare un alias.

Se ci sono newline nel nome del file, se le newline sono elencate o meno dipende dalla variante Unix. I coreutils GNU e BusyBox vengono visualizzati ?per impostazione predefinita per una nuova riga, quindi sono sicuri.

Chiama ls -fper elencare le voci senza ordinarle (# 2). Questo si accende automaticamente -a(almeno sui sistemi moderni). L' -fopzione è in POSIX ma con stato opzionale; la maggior parte delle implementazioni lo supportano, ma non BusyBox. L'opzione -qsostituisce i caratteri non stampabili incluse le nuove righe con ?; è POSIX ma non è supportato da BusyBox, quindi omettilo se hai bisogno del supporto BusyBox a spese di un numero eccessivo di file il cui nome contiene un carattere di nuova riga.

Se la directory non ha sottodirectory, la maggior parte delle versioni findnon chiamerà le statsue voci (ottimizzazione delle directory foglia: una directory che ha un numero di collegamenti pari a 2 non può avere sottodirectory, quindi findnon è necessario cercare i metadati delle voci a meno che un condizione come lo -typerichiede). Quindi find . | wc -lè un modo portatile e veloce per contare i file in una directory a condizione che la directory non abbia sottodirectory e che nessun nome di file contenga una nuova riga.

Se la directory non ha sottodirectory ma i nomi dei file possono contenere newline, prova una di queste (la seconda dovrebbe essere più veloce se supportata, ma potrebbe non esserlo in modo evidente).

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

D'altra parte, non usare findse la directory ha delle sottodirectory: persino find . -maxdepth 1chiamate statsu ogni voce (almeno con GNU find e BusyBox find). Eviti l'ordinamento (n. 2) ma paghi il prezzo di una ricerca inode (n. 3) che uccide le prestazioni.

Nella shell senza strumenti esterni, è possibile eseguire contare i file nella directory corrente con set -- *; echo $#. Questo manca i file punto (file il cui nome inizia con .) e riporta 1 anziché 0 in una directory vuota. Questo è il modo più veloce per contare i file in piccole directory perché non richiede l'avvio di un programma esterno, ma (tranne in zsh) fa perdere tempo per directory più grandi a causa della fase di ordinamento (# 2).

  • In bash, questo è un modo affidabile per contare i file nella directory corrente:

    shopt -s dotglob nullglob
    a=(*)
    echo ${#a[@]}
    
  • In ksh93, questo è un modo affidabile per contare i file nella directory corrente:

    FIGNORE='@(.|..)'
    a=(~(N)*)
    echo ${#a[@]}
    
  • In zsh, questo è un modo affidabile per contare i file nella directory corrente:

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

    Se avete il mark_dirsset di opzione, assicurarsi di spegnerlo: a=(*(DNoN^M)).

  • In qualsiasi shell POSIX, questo è un modo affidabile per contare i file nella directory corrente:

    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"
    

Tutti questi metodi ordinano i nomi dei file, tranne quello zsh.


1
I miei test empirici su> 1 milione di file mostrano che si find -maxdepth 1tiene facilmente al passo \ls -Ufintanto che non si aggiunge nulla come una -typedichiarazione che deve fare ulteriori controlli. Sei sicuro che GNU trovi effettivamente le chiamate stat? Anche il rallentamento find -typeè nulla rispetto a quante ls -ltorbiere se lo fai restituire i dettagli del file. D'altra parte il vincitore della chiara velocità sta zshusando il glob non di smistamento. (i globs ordinati sono 2 volte più lenti rispetto a lsquelli non ordinati 2 volte più veloci). Mi chiedo se i tipi di file system influirebbero significativamente su questi risultati.
Caleb,

@Caleb ho corso strace. Questo è vero solo se la directory ha delle sottodirectory: altrimenti findl'ottimizzazione della directory foglia entra in azione (anche senza -maxdepth 1), avrei dovuto menzionarlo. Molte cose possono influenzare il risultato, incluso il tipo di filesystem (la chiamata statè molto più costosa su filesystem che rappresentano directory come elenchi lineari che su filesystem che rappresentano directory come alberi), indipendentemente dal fatto che gli inode siano stati tutti creati insieme e quindi vicini sul disco, cache fredda o calda, ecc.
Gilles 'SO- smetti di essere malvagio' l'

1
Storicamente, ls -fè stato il modo affidabile per impedire la chiamata stat- questo è spesso semplicemente descritto oggi come "l'output non è ordinato" (che causa anche), e include .e ... -Ae -Unon sono opzioni standard.
Casuale 832

1
Se si desidera specificamente contare il file con un'estensione comune (o altra stringa), inserendolo nel comando si elimina il supplemento 2. Ecco un esempio:\ls -afq *[0-9].pdb | wc -l
Steven C. Howell,

FYI, con ksh93 version sh (AT&T Research) 93u+ 2012-08-01sul mio sistema basato su Debian, FIGNOREnon sembra funzionare. Le voci .e ..sono incluse nell'array risultante
Sergiy Kolodyazhnyy il

17
find /foo/foo2/ -maxdepth 1 | wc -l

È considerevolmente più veloce sulla mia macchina ma la .directory locale viene aggiunta al conteggio.


1
Grazie. Sono costretto a fare una domanda stupida: perché è più veloce? Perché non preoccuparsi di cercare gli attributi dei file?
Mike B,

2
Sì, questa è la mia comprensione. Finché non usi il -typeparametro finddovrebbe essere più veloce dils
Joel Taylor

1
Hmmm .... se sto capendo la documentazione di find , questo dovrebbe effettivamente essere migliore della mia risposta. Chiunque abbia più esperienza può verificare?
Luis Machuca,

Aggiungi a -mindepth 1per omettere la directory stessa.
Stéphane Chazelas il

8

ls -1Uprima che la pipe dovrebbe spendere un po 'meno risorse, poiché non tenta di ordinare le voci del file, le legge semplicemente mentre vengono ordinate nella cartella su disco. Produce anche meno output, il che significa un po 'meno lavoro per wc.

È inoltre possibile utilizzare ls -fche è più o meno una scorciatoia per ls -1aU.

Non so se esiste un modo efficiente in termini di risorse per farlo tramite un comando senza piping.


8
A proposito, -1 è implicito quando l'uscita va in una pipe
enzotib

@enzotib - lo è? Wow ... uno impara qualcosa di nuovo ogni giorno!
Luis Machuca,

6

Un altro punto di confronto. Pur non essendo un shell oneliner, questo programma C non fa nulla di superfluo. Si noti che i file nascosti vengono ignorati per corrispondere all'output di ls|wc -l( ls -l|wc -lè disattivato di uno a causa dei blocchi totali nella prima riga di output).

#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);
}

L'uso readdir()dell'API stdio aggiunge un certo sovraccarico e non ti dà il controllo sulla dimensione del buffer passato alla chiamata di sistema sottostante ( getdentssu Linux)
Stéphane Chazelas

3

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

Sarebbe interessante confrontare i tempi con il tuo tubo shell.


Sul mio test, questo mantiene più o meno esattamente lo stesso ritmo delle altre tre soluzioni più veloci ( find -maxdepth 1 | wc -l, \ls -AU | wc -le il zshnon smistamento glob e la matrice conteggio basato). In altre parole, batte le opzioni con varie inefficienze come l'ordinamento o la lettura di proprietà di file estranee. Mi azzarderei a dire dal momento che non ti guadagna nulla, non vale la pena usare su una soluzione più semplice a meno che non ti trovi già in Perù :)
Caleb

Si noti che questo includerà le voci di directory .e ..nel conteggio, quindi è necessario sottrarre due per ottenere il numero effettivo di file (e sottodirectory). Nel moderno Perl, perl -E 'opendir $dh, "."; $i++ while readdir $dh; say $i - 2'lo farebbe.
Ilmari Karonen,

2

Da questa risposta , posso pensare a questa come una possibile soluzione.

/*
 * 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);
}

Copia il programma C sopra nella directory in cui devono essere elencati i file. Quindi eseguire questi comandi:

gcc getdents.c -o getdents
./getdents | wc -l

1
Alcune cose: 1) se sei disposto a utilizzare un programma personalizzato per questo, potresti anche semplicemente contare i file e stampare il conteggio; 2) per confrontare ls -f, non filtrare d_typeaffatto, solo su d->d_ino != 0; 3) sottrai 2 per .e ...
Matei David,

Vedi la risposta collegata per un esempio di tempistiche in cui è 40 volte più veloce di quello accettato ls -f.
Matei David,

1

Una soluzione solo bash, che non richiede alcun programma esterno, ma non so quanto sia efficiente:

list=(*)
echo "${#list[@]}"

L'espansione globale non è necessaria il modo più efficiente in termini di risorse per farlo. Oltre alla maggior parte delle conchiglie che hanno un limite superiore al numero di oggetti che elaboreranno, quindi questo probabilmente bombarderà quando si tratta di un milione di oggetti in più, ordina anche l'output. Le soluzioni che coinvolgono find or ls senza opzioni di ordinamento saranno più veloci.
Caleb,

@Caleb, solo le vecchie versioni di ksh avevano tali limiti (e non supportavano quella sintassi) AFAIK. Nella maggior parte delle altre shell, il limite è solo la memoria disponibile. Hai un punto che sarà molto inefficiente, specialmente in bash.
Stéphane Chazelas il

1

Probabilmente il modo più efficiente in termini di risorse non implicherebbe invocazioni di processi esterni. Quindi scommetterei su ...

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

1
Hai numeri relativi? per quanti file?
smci,

0

Dopo aver risolto il problema dalla risposta di @Joel, dove è stato aggiunto .come file:

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

tailrimuove semplicemente la prima riga, il che significa che .non viene più conteggiato.


1
L'aggiunta di una coppia di tubi al fine di omettere una linea di wcinput non è molto efficiente poiché l' overhead aumenta linearmente per quanto riguarda le dimensioni di input. In questo caso, perché non semplicemente decrementare il conteggio finale per compensare la sua disattivazione di uno, che è un'operazione a tempo costante:echo $(( $(find /foo/foo2 -maxdepth 1 | wc -l) - 1))
Thomas Nyman,

1
Piuttosto che alimentare molti dati attraverso un altro processo, probabilmente sarebbe meglio fare un po 'di matematica sull'output finale. let count = $(find /foo/foo2 -maxdepth 1 | wc -l) - 2
Caleb,

0

os.listdir () in python può fare il lavoro per te. Fornisce una matrice del contenuto della directory, escluso lo speciale "." e '..' file. Inoltre, non è necessario preoccuparsi di file con caratteri speciali come '\ n' nel nome.

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

di seguito è riportato il tempo impiegato dal comando python sopra rispetto al comando 'ls -Af'.

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

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

0m0.249s reali
utente 0m0.064s
sys 0m0.180s

0

ls -1 | wc -lmi viene subito in mente. Che ls -1Usia più veloce di quanto ls -1sia puramente accademico, la differenza dovrebbe essere trascurabile ma per directory molto grandi.


0

Per escludere le sottodirectory dal conteggio, ecco una variazione sulla risposta accettata da Gilles:

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

L' $(( ))espansione aritmetica esterna sottrae l'output della seconda $( )subshell dalla prima $( ). Il primo $( )è esattamente Gilles 'dall'alto. Il secondo $( )genera il conteggio delle directory che "collegano" alla destinazione. Questo deriva da ls -od(sostituire ls -ldse lo si desidera), dove la colonna che elenca il conteggio dei collegamenti reali ha questo significato speciale per le directory. Il conteggio "link" comprende ., ..e tutte le sottodirectory.

Non ho testato le prestazioni, ma sembrerebbe simile. Aggiunge una stat della directory di destinazione e un certo sovraccarico per la subshell e la pipe aggiunte.


-2

Penserei che l'eco * sia più efficiente di qualsiasi comando 'ls':

echo * | wc -w

4
Che dire dei file con uno spazio nel loro nome? echo 'Hello World'|wc -wproduce 2.
Joseph R.,

@JosephR. Caveat Emptor
Dan Garthwaite,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.