it-swarm.com.de

Was ist die ressourceneffizienteste Methode, um zu zählen, wie viele Dateien sich in einem Verzeichnis befinden?

CentOS 5.9

Ich bin neulich auf ein Problem gestoßen, bei dem ein Verzeichnis viele Dateien enthielt. Um es zu zählen, lief ich ls -l /foo/foo2/ | wc -l

Es stellt sich heraus, dass sich über 1 Million Dateien in einem einzigen Verzeichnis befanden (lange Geschichte - die Grundursache wird behoben).

Meine Frage ist: Gibt es einen schnelleren Weg, um zu zählen? Was wäre der effizienteste Weg, um die Zählung zu erhalten?

57
Mike B

Kurze Antwort:

\ls -afq | wc -l

(Dies beinhaltet . Und .., Subtrahieren Sie also 2.)


Wenn Sie die Dateien in einem Verzeichnis auflisten, können drei häufige Probleme auftreten:

  1. Auflisten der Dateinamen im Verzeichnis. Dies ist unausweichlich: Es gibt keine Möglichkeit, die Dateien in einem Verzeichnis zu zählen, ohne sie aufzulisten.
  2. Dateinamen sortieren. Shell-Platzhalter und der Befehl ls tun dies.
  3. Rufen Sie stat auf, um Metadaten zu jedem Verzeichniseintrag abzurufen, z. B. ob es sich um ein Verzeichnis handelt.

# 3 ist bei weitem das teuerste, da für jede Datei ein Inode geladen werden muss. Im Vergleich dazu werden alle für # 1 benötigten Dateinamen kompakt in wenigen Blöcken gespeichert. # 2 verschwendet etwas CPU-Zeit, ist aber oft kein Deal Breaker.

Wenn Dateinamen keine Zeilenumbrüche enthalten, gibt ein einfaches ls -A | wc -l An, wie viele Dateien sich im Verzeichnis befinden. Beachten Sie, dass ein Alias ​​für ls möglicherweise einen Aufruf von stat auslöst (z. B. ls --color Oder ls -F Muss der Dateityp bekannt sein, der erfordert einen Aufruf von stat). Rufen Sie daher über die Befehlszeile command ls -A | wc -l oder \ls -A | wc -l auf, um einen Alias ​​zu vermeiden.

Wenn der Dateiname Zeilenumbrüche enthält, hängt es von der Unix-Variante ab, ob Zeilenumbrüche aufgeführt sind oder nicht. GNU coreutils und BusyBox zeigen standardmäßig ? Für eine neue Zeile an, damit sie sicher sind.

Rufen Sie ls -f Auf, um die Einträge aufzulisten, ohne sie zu sortieren (# 2). Dies schaltet automatisch -a Ein (zumindest auf modernen Systemen). Die Option -f Befindet sich in POSIX, jedoch mit optionalem Status. Die meisten Implementierungen unterstützen dies, BusyBox jedoch nicht. Die Option -q Ersetzt nicht druckbare Zeichen einschließlich Zeilenumbrüchen durch ?. Es ist POSIX, wird jedoch von BusyBox nicht unterstützt. Lassen Sie es daher weg, wenn Sie BusyBox-Unterstützung auf Kosten der Überzählung von Dateien benötigen, deren Name ein Zeilenumbruchzeichen enthält.

Wenn das Verzeichnis keine Unterverzeichnisse hat, rufen die meisten Versionen von findstat für seine Einträge nicht auf (Blattverzeichnisoptimierung: Ein Verzeichnis mit einer Linkanzahl von 2 kann keine Unterverzeichnisse haben, daher find muss die Metadaten der Einträge nicht nachschlagen, es sei denn, eine Bedingung wie -type erfordert dies. find . | wc -l Ist also eine tragbare und schnelle Methode zum Zählen von Dateien in einem Verzeichnis, vorausgesetzt, das Verzeichnis enthält keine Unterverzeichnisse und kein Dateiname enthält eine neue Zeile.

Wenn das Verzeichnis keine Unterverzeichnisse hat, Dateinamen jedoch Zeilenumbrüche enthalten können, versuchen Sie eines davon (das zweite sollte schneller sein, wenn es unterstützt wird, aber möglicherweise nicht merklich).

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

Verwenden Sie andererseits nicht find, wenn das Verzeichnis Unterverzeichnisse enthält: Selbst find . -maxdepth 1 Ruft stat bei jedem Eintrag auf (zumindest mit GNU find und BusyBox finden). Sie vermeiden das Sortieren (Nr. 2), zahlen jedoch den Preis für eine Inode-Suche (Nr. 3), die die Leistung beeinträchtigt.

In der Shell ohne externe Tools können Sie die Dateien im aktuellen Verzeichnis mit set -- *; echo $# Zählen. Dabei fehlen Punktedateien (Dateien, deren Name mit . Beginnt) und es wird 1 anstelle von 0 in einem leeren Verzeichnis gemeldet. Dies ist der schnellste Weg, um Dateien in kleinen Verzeichnissen zu zählen, da kein externes Programm gestartet werden muss, aber (außer in zsh) aufgrund des Sortierschritts (# 2) Zeit für größere Verzeichnisse verschwendet wird.

  • In Bash ist dies eine zuverlässige Methode, um die Dateien im aktuellen Verzeichnis zu zählen:

    shopt -s dotglob nullglob
    a=(*)
    echo ${#a[@]}
    
  • In ksh93 ist dies eine zuverlässige Methode, um die Dateien im aktuellen Verzeichnis zu zählen:

    FIGNORE='@(.|..)'
    a=(~(N)*)
    echo ${#a[@]}
    
  • In zsh ist dies eine zuverlässige Methode, um die Dateien im aktuellen Verzeichnis zu zählen:

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

    Wenn Sie die Option mark_dirs Aktiviert haben, müssen Sie sie deaktivieren: a=(*(DNoN^M)).

  • In jeder POSIX-Shell ist dies eine zuverlässige Methode, um die Dateien im aktuellen Verzeichnis zu zählen:

    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"
    

Alle diese Methoden sortieren die Dateinamen mit Ausnahme der zsh.

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

Ist auf meinem Rechner deutlich schneller aber das lokale . Verzeichnis wird zur Zählung hinzugefügt.

17
Joel Taylor

ls -1U, Bevor die Pipe etwas weniger Ressourcen verbrauchen sollte, da sie nicht versucht, die Dateieinträge zu sortieren, sondern sie nur liest, wenn sie im Ordner auf der Festplatte sortiert sind. Es erzeugt auch weniger Ausgabe, was etwas weniger Arbeit für wc bedeutet.

Sie können auch ls -f Verwenden, was mehr oder weniger eine Verknüpfung für ls -1aU Ist.

Ich weiß nicht, ob es eine ressourceneffiziente Möglichkeit gibt, dies über einen Befehl ohne Piping zu tun.

8
Luis Machuca

Ein weiterer Vergleichspunkt. Dieses C-Programm ist zwar kein Shell-Oneliner, macht aber nichts Überflüssiges. Beachten Sie, dass versteckte Dateien ignoriert werden, um der Ausgabe von ls|wc -l Zu entsprechen (ls -l|wc -l Ist aufgrund der Gesamtzahl der Blöcke in der ersten Ausgabezeile um eins deaktiviert).

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

Sie könnten Perl -e 'opendir($dh,".");$i=0;while(readdir $dh){$i++};print "$i\n";' versuchen

Es wäre interessant, die Timings mit Ihrer Shell-Pipe zu vergleichen.

3
doneal24

Von diese Antwort kann ich mir diese als mögliche Lösung vorstellen.

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

Kopieren Sie das obige C-Programm in das Verzeichnis, in dem die Dateien aufgelistet werden müssen. Führen Sie dann die folgenden Befehle aus:

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

Eine reine Bash-Lösung, die kein externes Programm erfordert, aber nicht weiß, wie effizient sie ist:

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

os.listdir () in python kann die Arbeit für Sie erledigen. Es enthält ein Array des Inhalts des Verzeichnisses mit Ausnahme der speziellen Dateien '.' und '..' Sie müssen sich um Dateien mit Sonderzeichen wie '\ n' im Namen kümmern.

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

es folgt die Zeit, die der obige Befehl python im Vergleich zum Befehl 'ls -Af') benötigt.

 ~/Test $ time ls -Af | wc -l 
 399144 
 
 Real 0m0.300s 
 Benutzer 0m0.104s 
 sys 0m0.240s 
 ~/test $ time python -c 'import os; print len ​​(os.listdir ("."))' 
 399142 
 
 Real 0m0.249s 
 Benutzer 0m0.064s 
 Sys 0m0.180s 
1
indrajeet

Wahrscheinlich würde der ressourceneffiziente Weg am meisten keine externen Prozessaufrufe beinhalten. Also würde ich wetten auf ...

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
mikeserv

Um nterverzeichnisse auszuschließen von der Zählung aus, hier eine Variation der akzeptierten Antwort von Gilles:

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

Die äußere arithmetische Erweiterung $(( )) subtrahiert die Ausgabe der zweiten Unterschale $( ) von der ersten $( ). Die erste $( ) ist genau Gilles 'von oben. Die zweite $( ) gibt die Anzahl der Verzeichnisse aus, die mit dem Ziel "verknüpft" sind. Dies kommt von ls -od (Falls gewünscht durch ls -ld Ersetzen), wobei die Spalte, in der die Anzahl der Hardlinks aufgeführt ist, dies als besondere Bedeutung für Verzeichnisse hat. Die Anzahl der "Links" umfasst ., .. Und alle Unterverzeichnisse.

Ich habe die Leistung nicht getestet, aber es scheint ähnlich zu sein. Es fügt eine Statistik des Zielverzeichnisses und einen gewissen Overhead für die hinzugefügte Subshell und Pipe hinzu.

0
user361782

Nachdem das Problem aus der Antwort von @Joel behoben wurde, wurde . als Datei:

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

tail entfernt einfach die erste Zeile, was bedeutet, dass . wird nicht mehr gezählt.

0
haneefmubarak

ls -1 | wc -l fällt mir sofort ein. Ob ls -1U ist schneller als ls -1 ist rein akademisch - der Unterschied sollte vernachlässigbar sein, aber für sehr große Verzeichnisse.

0
countermode

Ich weiß, dass dies alt ist, aber ich denke, dass awkhas hier erwähnt werden muss. Die Vorschläge, die die Verwendung von wc beinhalten, sind in Bezug auf die Frage von OP: "Der ressourceneffizienteste Weg" einfach nicht korrekt. Ich hatte vor kurzem eine Protokolldatei außer Kontrolle geraten (aufgrund einer schlechten Software) und bin daher auf diesen Beitrag gestoßen. Es gab ungefähr 232 Millionen Einträge! Ich habe es zuerst versucht wc -l und wartete 15 Minuten - es war nicht einmal in der Lage, die Zeilen zu zählen. Die folgende Anweisung awk gab mir in 3 Minuten eine genaue Zeilenanzahl für diese Protokolldatei. Ich habe im Laufe der Jahre gelernt, die Fähigkeit von awk, Standard-Shell-Programme viel effizienter zu simulieren, nie zu unterschätzen.

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

Und wenn Sie einen Befehl wie ls zum Zählen von Dateien in einem Verzeichnis ersetzen müssen:

`#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/)
0
user.friendly

Eine etwas späte Antwort (nach 6 Jahren), aber ...

Der schnellste Weg ist einfach do ls -lim übergeordneten Verzeichnis und überprüfen Sie die Spalte für die Anzahl der Links für das angegebene Unterverzeichnis.

Demo: Angenommen, Sie möchten die Anzahl der Dateien/Verzeichnisse in meinem Verzeichnis /usr/lib Zählen.

Die Eingabe von ls -l /usr Erzeugt also:

total 0
drwxr-xr-x  978 root  wheel  31296 29 apr  2019 bin
drwxr-xr-x  267 root  wheel   8544 30 okt  2018 include
drwxr-xr-x  312 root  wheel   9984 23 jan  2019 lib
drwxr-xr-x  240 root  wheel   7680 29 apr  2019 libexec
drwxr-xr-x   17 root  wheel    544 14 nov  2018 local
drwxr-xr-x  248 root  wheel   7936 23 jan  2019 sbin
drwxr-xr-x   47 root  wheel   1504  4 okt  2018 share
drwxr-xr-x    5 root  wheel    160 25 okt  2017 standalone

Die Nummer direkt nach den Berechtigungen ist link count Der Datei. Bei einem Verzeichnis ist es nur die Anzahl der darin enthaltenen Einträge. Im obigen Beispiel hat /usr/lib12 Einträge.

Lassen Sie überprüfen:

$ ls -1a /usr/lib | wc -l
     312

Verwenden Sie einfach -d, Ohne die anderen Verzeichnisse im übergeordneten Verzeichnis anzuzeigen.

$ ls -ld /usr/lib
drwxr-xr-x  312 root  wheel  9984 23 jan  2019 /usr/lib
#           ^^^ - the number of entries in the /usr/lib (including . and ..)
0
jm666