Jaki jest najbardziej zasobooszczędny sposób zliczania liczby plików w katalogu?

55

CentOS 5.9

Pewnego dnia natknąłem się na problem, w którym katalog miał wiele plików. Aby to policzyć, pobiegłemls -l /foo/foo2/ | wc -l

Okazuje się, że w jednym katalogu znajdowało się ponad milion plików (długa historia - główna przyczyna została naprawiona).

Moje pytanie brzmi: czy istnieje szybszy sposób na policzenie? Jaki byłby najskuteczniejszy sposób na policzenie?

Mike B.
źródło
5
ls -l|wc -lbyłby wyłączony o jeden ze względu na całkowitą liczbę bloków w pierwszym wierszu ls -lwyniku
Thomas Nyman
3
@ThomasNyman Byłoby faktycznie wyłączonych przez kilka z powodu pseudo-kropek i kropek, ale można tego uniknąć za pomocą -Aflagi. -ljest również problematyczne z powodu odczytu metadanych pliku w celu wygenerowania rozszerzonego formatu listy. Wymuszanie NIE -lprzy użyciu \lsjest znacznie lepszą opcją ( -1zakłada się przy przesyłaniu danych wyjściowych.) Zobacz najlepsze rozwiązanie tutaj, w odpowiedzi Gillesa .
Caleb,
2
@Caleb ls -lnie wyświetla żadnych ukrytych plików ani wpisów .i ... ls -aWyjście zawiera ukryte pliki, w tym . i ..jednocześnie ls -Awyjście zawiera ukryte pliki bez . i ... W odpowiedzi Gillesa dotglob opcja powłoki bash powoduje, że rozszerzenie zawiera ukryte pliki z wyłączeniem . i ...
Thomas Nyman

Odpowiedzi:

61

Krótka odpowiedź:

\ls -afq | wc -l

(Obejmuje to, .a ..więc odejmij 2.)


Gdy wyświetlasz listę plików w katalogu, mogą się zdarzyć trzy typowe rzeczy:

  1. Wyliczanie nazw plików w katalogu. Jest to nieuniknione: nie można policzyć plików w katalogu bez ich wyliczenia.
  2. Sortowanie nazw plików. Powtarzają to symbole wieloznaczne powłoki i lspolecenie.
  3. Wywoływanie w statcelu pobrania metadanych dotyczących każdej pozycji katalogu, na przykład, czy jest to katalog.

# 3 jest zdecydowanie najdroższym, ponieważ wymaga załadowania i-węzła dla każdego pliku. Dla porównania, wszystkie nazwy plików potrzebne dla nr 1 są przechowywane w zwięzłej formie w kilku blokach. # 2 marnuje trochę czasu procesora, ale często nie przeszkadza.

Jeśli w nazwach plików nie ma nowego wiersza, prosty ls -A | wc -linformuje, ile plików znajduje się w katalogu. Uważaj, że jeśli masz alias ls, może to wywołać wywołanie stat(np. ls --colorLub ls -Fznać typ pliku, który wymaga wywołania stat), a więc z linii poleceń, zadzwoń command ls -A | wc -llub \ls -A | wc -lunikaj aliasu.

Jeśli w nazwie pliku znajdują się znaki nowej linii, to czy nowe linie są na liście, czy nie, zależy od wariantu Uniksa. Coreutils GNU i BusyBox domyślnie wyświetlają się ?dla nowej linii, więc są bezpieczne.

Zadzwoń, ls -faby wyświetlić listę wpisów bez ich sortowania (# 2). To automatycznie się włącza -a(przynajmniej w nowoczesnych systemach). -fOpcja jest w POSIX ale ze statusem opcjonalnego; większość implementacji obsługuje to, ale nie BusyBox. Ta opcja -qzastępuje znaki niedrukowalne, w tym znaki nowego wiersza, przez ?; jest POSIX, ale nie jest obsługiwany przez BusyBox, więc pomiń go, jeśli potrzebujesz obsługi BusyBox kosztem przeliczania plików, których nazwa zawiera znak nowej linii.

Jeśli katalog nie ma podkatalogów, wówczas większość wersji findnie wywoła statswoich wpisów (optymalizacja katalogu liści: katalog z liczbą linków 2 nie może mieć podkatalogów, więc findnie trzeba wyszukiwać metadanych wpisów, chyba że stan taki, jak tego -typewymaga). Więc find . | wc -ljest przenośny, szybki sposób liczyć plików w katalogu, pod warunkiem, że katalog ma podkatalogi, a nie nazwa pliku zawiera znak nowej linii.

Jeśli katalog nie ma podkatalogów, ale nazwy plików mogą zawierać znaki nowej linii, wypróbuj jeden z nich (drugi powinien być szybszy, jeśli jest obsługiwany, ale może nie być zauważalny).

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

Z drugiej strony nie używaj, findjeśli katalog ma podkatalogi: nawet find . -maxdepth 1wywołuje statkażdy wpis (przynajmniej z GNU find i BusyBox find). Unikasz sortowania (# 2), ale płacisz cenę za wyszukiwanie i-węzłów (# 3), które zabija wydajność.

W powłoce bez zewnętrznych narzędzi można uruchomić zliczanie plików w bieżącym katalogu za pomocą set -- *; echo $#. Pomija to pliki kropkowe (pliki, których nazwa zaczyna się od .) i zgłasza 1 zamiast 0 w pustym katalogu. Jest to najszybszy sposób na zliczanie plików w małych katalogach, ponieważ nie wymaga uruchomienia programu zewnętrznego, ale (oprócz zsh) marnuje czas na większe katalogi z powodu kroku sortowania (# 2).

  • W bash jest to niezawodny sposób na policzenie plików w bieżącym katalogu:

    shopt -s dotglob nullglob
    a=(*)
    echo ${#a[@]}
  • W ksh93 jest to niezawodny sposób na zliczanie plików w bieżącym katalogu:

    FIGNORE='@(.|..)'
    a=(~(N)*)
    echo ${#a[@]}
  • W zsh jest to niezawodny sposób na policzenie plików w bieżącym katalogu:

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

    Jeśli masz mark_dirszestaw opcji, należy ją wyłączyć: a=(*(DNoN^M)).

  • W dowolnej powłoce POSIX jest to niezawodny sposób na policzenie plików w bieżącym katalogu:

    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"

Wszystkie te metody sortują nazwy plików, z wyjątkiem jednej zsh.

Gilles „SO- przestań być zły”
źródło
1
Moje testy empiryczne na> 1 milionach plików pokazują, że find -maxdepth 1łatwo dotrzymuje kroku, o \ls -Uile nie dodasz czegoś takiego jak -typedeklaracja, która musi wykonać dalsze kontrole. Czy jesteś pewien, że GNU znajduje wywołania stat? Nawet spowolnienie find -typejest niczym w porównaniu z tym, ile ls -ltorfowisk sprawi, że zwrócą szczegóły pliku. Z drugiej strony wyraźny zwycięzca prędkości zshkorzysta z globu bez sortowania. (posortowane globusy są 2x wolniejsze niż lspodczas gdy nie sortujące globusy są 2x szybsze). Zastanawiam się, czy typy systemów plików znacząco wpłynęłyby na te wyniki.
Caleb,
@Caleb I pobiegł strace. Jest to prawdą tylko wtedy, gdy katalog ma podkatalogi: w przeciwnym razie findrozpocznie się optymalizacja katalogu liści (nawet bez -maxdepth 1), powinienem o tym wspomnieć. Na wynik może wpływać wiele rzeczy, w tym typ systemu plików (wywoływanie statjest znacznie droższe w systemach plików reprezentujących katalogi jako listy liniowe niż w systemach plików reprezentujących katalogi jako drzewa), niezależnie od tego, czy wszystkie i-węzły zostały utworzone razem, a zatem są blisko na dysku, zimnej lub gorącej pamięci podręcznej itp.
Gilles „SO- przestań być zły”
1
Historycznie ls -fjest to niezawodny sposób zapobiegania dzwonieniu stat- często jest to dziś po prostu opisywane jako „wynik nie jest sortowany” (co również powoduje), i obejmuje .i ... -Ai -Unie są standardowymi opcjami.
Random832
1
Jeśli chcesz zliczyć plik ze wspólnym rozszerzeniem (lub innym ciągiem), wstawienie go do polecenia eliminuje dodatkowe 2. Oto przykład:\ls -afq *[0-9].pdb | wc -l
Steven C. Howell
FYI, z ksh93 version sh (AT&T Research) 93u+ 2012-08-01na moim systemie opartym na Debianie, FIGNOREwydaje się nie działać. Te .i ..wpisy są zawarte w otrzymanej tablicy
Sergiy Kolodyazhnyy
17
find /foo/foo2/ -maxdepth 1 | wc -l

Jest znacznie szybszy na moim komputerze, ale .katalog lokalny jest dodawany do liczby.

Joel Taylor
źródło
1
Dzięki. Muszę jednak zadać głupie pytanie: dlaczego jest szybsze? Ponieważ nie przeszkadza wyszukiwanie atrybutów plików?
Mike B
2
Tak, to moje zrozumienie. Tak długo, jak nie użyjesz tego -typeparametru, findpowinno być szybsze niżls
Joel Taylor
1
Hmmm ... jeśli dobrze rozumiem dokumentację znaleziska , to powinno być lepsze niż moja odpowiedź. Ktoś z większym doświadczeniem może zweryfikować?
Luis Machuca,
Dodaj a, -mindepth 1aby pominąć sam katalog.
Stéphane Chazelas
8

ls -1Uzanim potok zużyje nieco mniej zasobów, ponieważ nie próbuje sortować wpisów plików, po prostu odczytuje je podczas sortowania w folderze na dysku. Daje również mniejszą wydajność, co oznacza nieco mniejszą pracę wc.

Możesz również użyć, ls -fktóry jest mniej więcej skrótem ls -1aU.

Nie wiem jednak, czy istnieje efektywny pod względem zasobów sposób, aby to zrobić za pomocą polecenia bez przesyłania potokowego.

Luis Machuca
źródło
8
Btw, -1 jest implikowane, gdy dane wyjściowe trafiają do potoku
enzotib,
@enzotib - to jest? Wow ... każdego dnia uczy się czegoś nowego!
Luis Machuca,
6

Kolejny punkt porównania. Ten program C, choć nie jest narzędziem powłoki, nie robi nic zbędnego. Zauważ, że ukryte pliki są ignorowane, aby dopasować wyjście ls|wc -l( ls -l|wc -ljest wyłączone o jeden ze względu na całkowitą liczbę bloków w pierwszym wierszu wyniku).

#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
źródło
Korzystanie ze readdir()interfejsu API stdio powoduje dodatkowe obciążenie i nie daje kontroli nad rozmiarem bufora przekazywanego do wywołania systemowego ( getdentsw systemie Linux)
Stéphane Chazelas
3

Możesz spróbować perl -e 'opendir($dh,".");$i=0;while(readdir $dh){$i++};print "$i\n";'

Byłoby interesujące porównać czasy z rurą pociskową.

doneal24
źródło
Na moich testów, to zachowuje prawie dokładnie takim samym tempie jak pozostałych trzech najszybszych rozwiązań ( find -maxdepth 1 | wc -l, \ls -AU | wc -la zshnie oparte na glob sortowania i liczenia tablicy). Innymi słowy, przewyższa opcje z różnymi nieefektywnościami, takimi jak sortowanie lub odczytywanie obcych właściwości pliku. Zaryzykowałbym stwierdzenie, ponieważ to też nic nie zarabia, nie warto używać prostszego rozwiązania, chyba że już jesteś w perlu :)
Caleb
Zauważ, że będzie to obejmować pozycje .i ..pozycje katalogu w liczbie, więc musisz odjąć dwa, aby uzyskać rzeczywistą liczbę plików (i podkatalogów). W nowoczesnym Perlu perl -E 'opendir $dh, "."; $i++ while readdir $dh; say $i - 2'zrobiłby to.
Ilmari Karonen,
2

Na podstawie tej odpowiedzi mogę myśleć o tym jako o możliwym rozwiązaniu.

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

Skopiuj powyższy program C do katalogu, w którym pliki muszą być wymienione. Następnie wykonaj następujące polecenia:

gcc getdents.c -o getdents
./getdents | wc -l
Ramesh
źródło
1
Kilka rzeczy: 1) jeśli chcesz użyć do tego niestandardowego programu, równie dobrze możesz policzyć pliki i wydrukować liczbę; 2) do porównania ls -f, wcale nie filtruj d_type, po prostu włącz d->d_ino != 0; 3) odejmij 2 dla .i ...
Matei David
Zobacz połączoną odpowiedź dla przykładu pomiaru czasu, w którym jest to 40 razy szybciej niż zaakceptowano ls -f.
Matei David,
1

Rozwiązanie typu bash, nie wymagające żadnego zewnętrznego programu, ale nie wiem, jak wydajne:

list=(*)
echo "${#list[@]}"
enzotib
źródło
Ekspansja globu nie jest koniecznym najbardziej zasobooszczędnym sposobem na to. Oprócz większości pocisków posiadających górną granicę liczby przedmiotów, które nawet przetwarzają, więc prawdopodobnie wybuchnie to, mając do czynienia z milionem przedmiotów, ale także sortuje wynik. Rozwiązania obejmujące wyszukiwanie lub ls bez opcji sortowania będą szybsze.
Caleb,
@Caleb, tylko stare wersje ksh miały takie ograniczenia (i nie obsługiwały tej składni) AFAIK. We wszystkich innych powłokach limitem jest tylko dostępna pamięć. Masz rację, że będzie to bardzo nieefektywne, szczególnie w bash.
Stéphane Chazelas
1

Prawdopodobnie najbardziej zasobooszczędny sposób nie wymagałby zewnętrznych wywołań procesów. Więc postawiłbym na ...

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
źródło
1
Masz liczby względne? dla ilu plików?
smci
0

Po naprawieniu problemu z odpowiedzi @Joela, gdzie został dodany .jako plik:

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

tailpo prostu usuwa pierwszy wiersz, co oznacza, że .nie jest już liczony.

haneefmubarak
źródło
1
Dodanie pary rur w celu pominięcia jednej linii wcwejściowej nie jest bardzo wydajne, ponieważ narzut zwiększa się liniowo względem wielkości wejściowej. W takim przypadku może po prostu zmniejszyć ostateczną liczbę, aby zrekompensować jej wyłączenie o jeden, co jest operacją o stałym czasie:echo $(( $(find /foo/foo2 -maxdepth 1 | wc -l) - 1))
Thomas Nyman
1
Zamiast podawać tyle danych w innym procesie, lepiej byłoby po prostu zrobić matematykę na końcowym wyjściu. let count = $(find /foo/foo2 -maxdepth 1 | wc -l) - 2
Caleb,
0

os.listdir () w python może wykonać pracę za Ciebie. Daje tablicę zawartości katalogu, z wyłączeniem specjalnego „.” i pliki „..”. Nie musisz też martwić się plikami abt ze znakami specjalnymi, takimi jak „\ n” w nazwie.

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

poniżej jest czas potrzebny na wykonanie powyższej komendy python w porównaniu z komendą „ls -Af”.

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

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

prawdziwe 0m0,249s
użytkownik 0m0,064s
sys 0m0.180s
indrajeet
źródło
0

ls -1 | wc -lprzychodzi mi od razu do głowy. Czy ls -1Ujest szybszy niż ls -1czysto akademicki - różnica powinna być znikoma, ale w przypadku bardzo dużych katalogów.

przeciwdziałanie
źródło
0

Aby wykluczyć podkatalogi z liczby, oto odmiana zaakceptowanej odpowiedzi od Gillesa:

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

Zewnętrzna $(( ))interpretacja arytmetyczna odejmuje wynik drugiej $( )podpowłoki od pierwszej $( ). Pierwszy $( )to dokładnie Gilles z góry. Drugi $( )wyświetla liczbę katalogów „łączących” się z celem. Pochodzi z ls -od(w ls -ldrazie potrzeby zamień), gdzie kolumna z liczbą twardych linków ma to szczególne znaczenie dla katalogów. „Link” count obejmuje ., ..oraz wszystkie podkatalogi.

Nie testowałem wydajności, ale wyglądałoby to podobnie. Dodaje statystykę katalogu docelowego i trochę narzutu dla dodanej podpowłoki i potoku.

użytkownik361782
źródło
-2

Sądzę, że echo * byłoby bardziej wydajne niż jakiekolwiek polecenie „ls”:

echo * | wc -w
Dan Garthwaite
źródło
4
Co z plikami ze spacją w nazwie? echo 'Hello World'|wc -wprodukuje 2.
Joseph R.
@JosephR. Caveat Emptor
Dan Garthwaite