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


55

CentOS 5.9

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

Es stellte sich heraus, dass sich über 1 Million Dateien in einem einzigen Verzeichnis befanden (lange Geschichte - die Ursache 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?


5
ls -l|wc -lwäre wegen der Gesamtanzahl der Blöcke in der ersten ls -lAusgabezeile um eins gesperrt
Thomas Nyman

3
@ThomasNyman Wegen der Pseudoeinträge dot und dotdot wäre es tatsächlich um mehrere Stellen versetzt, aber diese können durch Verwendung des -AFlags vermieden werden . -list auch problematisch, weil die Datei Metadaten liest, um das erweiterte Listenformat zu generieren. Das Erzwingen von NOT -ldurch Verwendung \lsist eine viel bessere Option ( -1wird bei der Weiterleitung der Ausgabe vorausgesetzt). Die beste Lösung finden Sie hier in der Antwort von Gilles .
Caleb

2
@Caleb ls -lgibt weder versteckte Dateien noch die Einträge .und aus ... ls -aDie Ausgabe enthält versteckte Dateien, einschließlich . und, ..während die ls -AAusgabe versteckte Dateien ohne . und enthält ... In Gilles 'Antwort bewirkt die Bash- dotglob Shell-Option, dass die Erweiterung versteckte Dateien ohne . und enthält ...
Thomas Nyman

Antworten:


61

Kurze Antwort:

\ls -afq | wc -l

(Dies beinhaltet .und .., also subtrahiere 2)


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

  1. Auflisten der Dateinamen im Verzeichnis. Dies ist unvermeidlich: Es gibt keine Möglichkeit, die Dateien in einem Verzeichnis zu zählen, ohne sie aufzuzählen.
  2. Sortieren der Dateinamen. Shell Wildcards und der lsBefehl machen das.
  3. Aufruf statzum Abrufen von Metadaten zu jedem Verzeichniseintrag, z. B. ob es sich um ein Verzeichnis handelt.

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

Wenn die Dateinamen keine Zeilenumbrüche enthalten, werden Sie durch eine einfache Angabe darüber informiert, ls -A | wc -lwie viele Dateien sich im Verzeichnis befinden. Beachten Sie, dass, wenn Sie einen Alias ​​für haben ls, dies möglicherweise einen Aufruf von auslösen kann stat(z. B. ls --coloroder ls -Fden Dateityp kennen müssen, für den ein Aufruf erforderlich ist stat), also von der Befehlszeile aus, command ls -A | wc -loder \ls -A | wc -lum einen Alias ​​zu vermeiden.

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

Rufen Sie ls -fan, um die Einträge aufzulisten, ohne sie zu sortieren (Nr. 2). Dies schaltet sich automatisch ein -a(zumindest bei modernen Systemen). Die -fOption ist in POSIX, aber mit optionalem Status. Die meisten Implementierungen unterstützen dies, BusyBox jedoch nicht. Die Option -qersetzt nicht druckbare Zeichen einschließlich Zeilenumbrüchen durch ?; Es ist POSIX, wird jedoch von BusyBox nicht unterstützt. Lassen Sie es daher aus, wenn Sie BusyBox-Unterstützung benötigen, und zahlen Sie dabei zu viele Dateien, deren Name ein Zeilenumbruchzeichen enthält.

Wenn das Verzeichnis keine Unterverzeichnisse enthält, findwerden statdie Einträge in den meisten Versionen von nicht abgerufen (Blattverzeichnisoptimierung: Ein Verzeichnis mit einer Linkanzahl von 2 kann keine Unterverzeichnisse enthalten. Daher findmüssen die Metadaten der Einträge nur nachgeschlagen werden, wenn ein Bedingung wie es -typeerforderlich ist). Dies find . | wc -list 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 enthält, die Dateinamen jedoch möglicherweise Zeilenumbrüche enthalten, probieren Sie eines dieser Verzeichnisse aus (das zweite sollte schneller sein, wenn es unterstützt wird, dies ist jedoch möglicherweise nicht erkennbar).

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

Auf der anderen Seite, verwenden Sie nicht , findwenn das Verzeichnis Verzeichnisse hat: auch find . -maxdepth 1Anrufe statauf jedem Eintrag (zumindest mit GNU finden und BusyBox zu 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 zählen set -- *; echo $#. Dadurch werden Punktdateien (Dateien, deren Name mit " .1" beginnt ) übersehen und in einem leeren Verzeichnis wird "1" anstelle von "0" angezeigt. Dies ist der schnellste Weg, um Dateien in kleinen Verzeichnissen zu zählen, da kein externes Programm gestartet werden muss, aber (mit Ausnahme von zsh) Zeit für größere Verzeichnisse aufgrund des Sortierschritts (# 2) 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 haben mark_dirsOption gesetzt, stellen Sie sicher, um sie auszuschalten: 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-Methode.


1
Meine empirischen Tests an> 1 Million Dateien haben gezeigt, dass dies find -maxdepth 1problemlos möglich ist, \ls -Usolange Sie nichts wie eine -typeErklärung hinzufügen , die weitere Überprüfungen erfordert. Sind Sie sicher, dass GNU tatsächlich Aufrufe findet stat? Sogar die Verlangsamung find -typeist nichts im Vergleich dazu, wie viele ls -lFehler Sie machen, wenn Sie Dateidetails zurückgeben. Auf der anderen Seite verwendet der Gewinner der klaren Geschwindigkeit zshdas nicht sortierende Glob. (Sortierte Globs sind 2x langsamer als lsnicht sortierte, 2x schneller). Ich frage mich, ob Dateisystemtypen diese Ergebnisse erheblich beeinflussen würden.
Caleb

@Caleb Ich rannte strace. Dies gilt nur, wenn das Verzeichnis Unterverzeichnisse hat: Andernfalls setzt finddie Blattverzeichnisoptimierung ein (auch ohne -maxdepth 1), das hätte ich erwähnen sollen. Viele Dinge können das Ergebnis beeinflussen, einschließlich des Dateisystemtyps (Aufrufen statist bei Dateisystemen, die Verzeichnisse als lineare Listen darstellen, viel teurer als bei Dateisystemen, die Verzeichnisse als Bäume darstellen), ob die Inodes alle zusammen erstellt wurden und daher in der Nähe sind auf der Festplatte, kalter oder heißer Cache usw.
Gilles 'SO - hör auf, böse zu sein'

1
Historisch gesehen war ls -fdies der zuverlässige Weg, um Anrufe zu verhindern stat- dies wird heute oft einfach als "Ausgabe ist nicht sortiert" (was auch dazu führt) beschrieben und beinhaltet .und ... -Aund -Usind keine Standardoptionen.
Random832

1
Wenn Sie speziell Dateien mit einer gemeinsamen Erweiterung (oder einer anderen Zeichenfolge) zählen möchten, werden durch das Einfügen dieser in den Befehl die zusätzlichen 2 entfernt. Hier ein Beispiel:\ls -afq *[0-9].pdb | wc -l
Steven C. Howell,

Zu Ihrer Information, mit ksh93 version sh (AT&T Research) 93u+ 2012-08-01auf meinem Debian-basierten System, FIGNOREscheint nicht zu funktionieren. Die .und ..Einträge werden in das resultierende Array aufgenommen
Sergiy Kolodyazhnyy

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

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


1
Vielen Dank. Ich bin jedoch gezwungen, eine dumme Frage zu stellen: Warum ist es schneller? Weil es nicht die Mühe macht, Dateiattribute nachzuschlagen?
Mike B

2
Ja das ist mein verständnis Solange Sie den -typeParameter nicht verwenden , findsollte es schneller sein alsls
Joel Taylor

1
Hmmm .... wenn ich die Dokumentation von find gut verstehe , sollte das eigentlich besser sein als meine Antwort. Wer mehr Erfahrung hat, kann das nachprüfen?
Luis Machuca

Fügen Sie ein hinzu -mindepth 1, um das Verzeichnis selbst auszulassen.
Stéphane Chazelas

8

ls -1UBevor die Pipe etwas weniger Ressourcen verbraucht, da sie nicht versucht, die Dateieinträge zu sortieren, liest sie sie nur so, wie sie im Ordner auf der Festplatte sortiert sind. Es produziert auch weniger Output, was etwas weniger Arbeit bedeutet wc.

Sie könnten auch verwenden, ls -fwas mehr oder weniger eine Abkürzung für ist ls -1aU.

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


8
Übrigens wird -1 impliziert, wenn die Ausgabe an eine Pipe geht
enzotib

@enzotib - ist das? Wow ... man lernt jeden Tag etwas Neues!
Luis Machuca

6

Ein weiterer Vergleichspunkt. Dieses C-Programm ist zwar kein Shell-Oneliner, macht aber nichts überflüssiges. Beachten Sie, dass versteckte Dateien ignoriert werden, um mit der Ausgabe von übereinzustimmen ls|wc -l( ls -l|wc -list aufgrund der Gesamtanzahl 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);
}

Die Verwendung der readdir()stdio-API erhöht den Aufwand und gibt Ihnen keine Kontrolle über die Größe des Puffers, der an den zugrunde liegenden Systemaufruf ( getdentsunter Linux)
Stéphane Chazelas,

3

Du könntest es versuchen perl -e 'opendir($dh,".");$i=0;while(readdir $dh){$i++};print "$i\n";'

Es wäre interessant, das Timing mit Ihrer Pfeife zu vergleichen.


Auf meinen Tests, hält diese so ziemlich genau das gleiche Tempo wie die anderen drei schnellsten Lösungen ( find -maxdepth 1 | wc -l, \ls -AU | wc -lund die zshBasis nicht Sortieranlage glob und Array count). Mit anderen Worten, es übertrifft die Optionen mit verschiedenen Ineffizienzen wie dem Sortieren oder Lesen von fremden Dateieigenschaften. Ich würde sagen, da es Ihnen auch nichts einbringt, lohnt es sich nicht, eine einfachere Lösung zu verwenden, es sei denn, Sie befinden sich bereits in Perl :)
Caleb

Beachten Sie, dass dies die Verzeichniseinträge .und ..in die Anzahl einschließt. Sie müssen also zwei davon abziehen, um die tatsächliche Anzahl der Dateien (und Unterverzeichnisse) zu erhalten. In modernen Perl perl -E 'opendir $dh, "."; $i++ while readdir $dh; say $i - 2'würde es tun.
Ilmari Karonen

2

Aus dieser 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 diese Befehle aus:

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

1
Ein paar Dinge: 1) Wenn Sie bereit sind, ein benutzerdefiniertes Programm dafür zu verwenden, können Sie auch einfach die Dateien zählen und die Anzahl ausdrucken. 2) Zum Vergleich: ls -fFiltern Sie überhaupt nicht nach , sondern d_typenur nach d->d_ino != 0. 3) subtrahiere 2 für .und ...
Matei David

In der verknüpften Antwort finden Sie ein Timing-Beispiel, bei dem dies 40-mal schneller ist als das akzeptierte ls -f.
Matei David

1

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

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

Glob-Erweiterung ist nicht die ressourceneffizienteste Möglichkeit, dies zu tun. Abgesehen davon, dass die meisten Shells eine Obergrenze für die Anzahl der Objekte haben, die sie sogar verarbeiten werden, so dass dies bei mehr als einer Million Objekten wahrscheinlich bombardiert wird, sortiert es auch die Ausgabe. Die Lösungen mit find oder ls ohne Sortieroptionen sind schneller.
Caleb

@Caleb, nur alte Versionen von ksh hatten solche Einschränkungen (und unterstützten diese Syntax nicht) AFAIK. In allen anderen Shells ist das Limit nur der verfügbare Speicher. Sie haben den Punkt, dass es sehr ineffizient sein wird, vor allem in der Bash.
Stéphane Chazelas

1

Am ressourcenschonendsten wären wahrscheinlich keine externen Prozessaufrufe. 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
Haben Sie relative Zahlen? für wie viele dateien?
smci

0

Nachdem Sie das Problem in der Antwort von @Joel behoben und .als Datei hinzugefügt haben :

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

tailEntfernt einfach die erste Zeile, was bedeutet, dass diese .nicht mehr gezählt wird.


1
Das Hinzufügen eines Pipepaars zum Weglassen einer wcEingabezeile ist nicht sehr effizient, da der Overhead in Bezug auf die Eingabegröße linear zunimmt . In diesem Fall können Sie die Endzählung einfach verringern, um zu kompensieren, dass sie um eins abweicht. Dies ist eine zeitlich konstante Operation:echo $(( $(find /foo/foo2 -maxdepth 1 | wc -l) - 1))
Thomas Nyman,

1
Anstatt so viele Daten durch einen anderen Prozess zu übertragen, ist es wahrscheinlich besser, nur ein wenig mit der endgültigen Ausgabe zu rechnen. let count = $(find /foo/foo2 -maxdepth 1 | wc -l) - 2
Caleb

0

os.listdir () in Python kann die Arbeit für Sie erledigen. Es gibt ein Array des Inhalts des Verzeichnisses ohne das spezielle '.' und '..' Dateien. Außerdem brauchen Sie sich keine Sorgen um Dateien mit Sonderzeichen wie '\ n' im Namen zu machen.

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

Es folgt die Zeit, die der obige Python-Befehl im Vergleich zum Befehl 'ls -Af' benötigt.

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

echte 0m0.300s
Benutzer 0m0.104s
sys 0m0.240s
~ / test $ time python -c 'importiere os; drucke len (os.listdir ("."))'
399142

echte 0m0.249s
Benutzer 0m0.064s
sys 0m0.180s

0

ls -1 | wc -lfällt mir sofort ein. Ob ls -1Udas schneller ist als ls -1rein akademisch - der Unterschied sollte aber bei sehr großen Verzeichnissen vernachlässigbar sein.


0

Um Unterverzeichnisse von der Zählung auszuschließen , gibt es eine Variation der von Gilles akzeptierten Antwort:

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 $( ). Das 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(auf ls -ldWunsch als Ersatz ), wobei die Spalte, in der die Anzahl der festen Links aufgeführt ist, diese als besondere Bedeutung für Verzeichnisse hat. Der „Link“ count enthält ., ..und alle Unterverzeichnisse.

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


-2

Ich würde denken, dass echo * effizienter ist als jeder 'ls'-Befehl:

echo * | wc -w

4
Was ist mit Dateien mit einem Leerzeichen im Namen? echo 'Hello World'|wc -wproduziert 2.
Joseph R.

@ JosephR. Vorbehalt Emptor
Dan Garthwaite
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.