Apa cara paling efisien sumber daya untuk menghitung berapa banyak file dalam direktori?

55

CentOS 5.9

Saya menemukan masalah beberapa hari yang lalu di mana sebuah direktori memiliki banyak file. Untuk menghitungnya, saya berlarils -l /foo/foo2/ | wc -l

Ternyata ada lebih dari 1 juta file dalam satu direktori (cerita panjang - penyebab utama semakin diperbaiki).

Pertanyaan saya adalah: adakah cara yang lebih cepat untuk melakukan penghitungan? Apa cara paling efisien untuk mendapatkan penghitungan?

Mike B
sumber
5
ls -l|wc -lakan dimatikan satu per satu karena total blok pada baris pertama ls -lkeluaran
Thomas Nyman
3
@ThomasNyman Sebenarnya akan dimatikan oleh beberapa karena entri semu dot dan dotdot, tetapi mereka dapat dihindari dengan menggunakan -Abendera. -ljuga bermasalah karena membaca file meta data untuk menghasilkan format daftar yang diperluas. Memaksa TIDAK -ldengan menggunakan \lsadalah pilihan yang jauh lebih baik ( -1diasumsikan ketika memipis keluaran). Lihat jawaban Gilles untuk solusi terbaik di sini.
Caleb
2
@ Caleb ls -ltidak menghasilkan file tersembunyi maupun entri .dan ... ls -aoutput termasuk file tersembunyi, termasuk . dan ..sementara ls -Aoutput termasuk file tersembunyi tidak termasuk . dan ... Dalam jawaban Gilles, opsi bash dotglob shell menyebabkan ekspansi menyertakan file tersembunyi tidak termasuk . dan ...
Thomas Nyman

Jawaban:

61

Jawaban singkat:

\ls -afq | wc -l

(Ini termasuk .dan .., jadi kurangi 2.)


Ketika Anda mendaftar file dalam direktori, tiga hal umum mungkin terjadi:

  1. Menghitung nama file dalam direktori. Ini tidak bisa dihindari: tidak ada cara untuk menghitung file dalam direktori tanpa menyebutkannya.
  2. Menyortir nama file. Shell wildcard dan lsperintah melakukan itu.
  3. Memanggil statuntuk mengambil metadata tentang setiap entri direktori, seperti apakah itu direktori.

# 3 adalah yang paling mahal sejauh ini, karena membutuhkan memuat inode untuk setiap file. Sebagai perbandingan, semua nama file yang diperlukan untuk # 1 disimpan secara kompak dalam beberapa blok. # 2 membuang-buang waktu CPU tetapi sering kali bukan pemecah kesepakatan.

Jika tidak ada baris baru dalam nama file, sederhana akan ls -A | wc -lmemberi tahu Anda berapa banyak file yang ada di direktori. Hati-hati bahwa jika Anda memiliki alias untuk ls, ini dapat memicu panggilan ke stat(mis. ls --colorAtau ls -Fperlu mengetahui jenis file, yang membutuhkan panggilan ke stat), jadi dari baris perintah, panggil command ls -A | wc -latau \ls -A | wc -luntuk menghindari alias.

Jika ada baris baru dalam nama file, apakah baris baru terdaftar atau tidak tergantung pada varian Unix. GNU coreutils dan BusyBox default untuk ditampilkan ?untuk baris baru, jadi mereka aman.

Panggil ls -funtuk mendaftar entri tanpa menyortirnya (# 2). Ini secara otomatis menyala -a(setidaknya pada sistem modern). The -fpilihan adalah di POSIX tapi dengan status yang opsional; sebagian besar implementasi mendukungnya, tetapi tidak BusyBox. Opsi ini -qmenggantikan karakter yang tidak dapat dicetak termasuk baris baru dengan ?; itu POSIX tetapi tidak didukung oleh BusyBox, jadi abaikan saja jika Anda memerlukan dukungan BusyBox dengan mengorbankan overcounting file yang namanya berisi karakter baris baru.

Jika direktori tidak memiliki subdirektori, maka sebagian besar versi findtidak akan memanggil statentri-entrinya (optimisasi direktori daun: direktori yang memiliki jumlah tautan 2 tidak dapat memiliki subdirektori, jadi findtidak perlu mencari metadata entri kecuali jika kondisi seperti -typemembutuhkannya). Begitu find . | wc -ljuga cara portabel dan cepat untuk menghitung file dalam direktori asalkan direktori tersebut tidak memiliki subdirektori dan bahwa tidak ada nama file yang mengandung baris baru.

Jika direktori tidak memiliki subdirektori tetapi nama file mungkin mengandung baris baru, coba salah satu dari ini (yang kedua harus lebih cepat jika didukung, tetapi mungkin tidak begitu terlihat).

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

Di sisi lain, jangan gunakan findjika direktori memiliki subdirektori: bahkan find . -maxdepth 1panggilan statpada setiap entri (setidaknya dengan GNU find dan BusyBox find). Anda menghindari penyortiran (# 2) tetapi Anda membayar harga pencarian inode (# 3) yang membunuh kinerja.

Dalam shell tanpa alat eksternal, Anda dapat menjalankan menghitung file dalam direktori saat ini set -- *; echo $#. Ini melewatkan file dot (file yang namanya dimulai dengan .) dan melaporkan 1 bukannya 0 di direktori kosong. Ini adalah cara tercepat untuk menghitung file dalam direktori kecil karena tidak memerlukan memulai program eksternal, tetapi (kecuali dalam zsh) membuang waktu untuk direktori yang lebih besar karena langkah penyortiran (# 2).

  • Dalam bash, ini adalah cara yang dapat diandalkan untuk menghitung file di direktori saat ini:

    shopt -s dotglob nullglob
    a=(*)
    echo ${#a[@]}
    
  • Di ksh93, ini adalah cara yang dapat diandalkan untuk menghitung file di direktori saat ini:

    FIGNORE='@(.|..)'
    a=(~(N)*)
    echo ${#a[@]}
    
  • Di zsh, ini adalah cara yang dapat diandalkan untuk menghitung file di direktori saat ini:

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

    Jika Anda memiliki mark_dirspilihan set, pastikan untuk mematikannya: a=(*(DNoN^M)).

  • Di setiap shell POSIX, ini adalah cara yang dapat diandalkan untuk menghitung file di direktori saat ini:

    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"
    

Semua metode ini mengurutkan nama file, kecuali untuk yang zsh.

Gilles 'SANGAT berhenti menjadi jahat'
sumber
1
Pengujian empiris saya pada> 1 juta file menunjukkan bahwa find -maxdepth 1dengan mudah mengimbangi \ls -Uselama Anda tidak menambahkan sesuatu seperti -typedeklarasi yang harus melakukan pemeriksaan lebih lanjut. Apakah Anda yakin GNU menemukan panggilan sebenarnya stat? Bahkan perlambatan find -typetidak ada artinya dibandingkan dengan berapa banyak ls -lrawa jika Anda membuatnya mengembalikan detail file. Di sisi lain pemenang kecepatan yang jelas adalah zshmenggunakan glob non sorting. (Gumpalan yang disortir lebih lambat 2x daripada lsyang tidak tersortir 2x lebih cepat). Saya ingin tahu apakah tipe sistem file akan secara signifikan mempengaruhi hasil ini.
Caleb
@ Caleb aku berlari strace. Ini hanya benar jika direktori tersebut memiliki subdirektori: jika tidak find, optimasi direktori leaf -maxdepth 1akan menghasilkan (bahkan tanpa ), saya seharusnya menyebutkannya. Banyak hal yang dapat mempengaruhi hasilnya, termasuk tipe sistem file (panggilan statjauh lebih mahal pada sistem file yang mewakili direktori sebagai daftar linier daripada pada sistem file yang mewakili direktori sebagai pohon), apakah inode semuanya dibuat bersama-sama dan dengan demikian dekat oleh pada disk, cache dingin atau panas, dll.
Gilles 'SO- stop being evil'
1
Secara historis, ls -ftelah menjadi cara yang dapat diandalkan untuk mencegah panggilan stat- ini sering hanya dijelaskan hari ini sebagai "output tidak diurutkan" (yang juga menyebabkan), dan tidak termasuk .dan ... -Adan -Ubukan opsi standar.
Random832
1
Jika Anda secara khusus ingin menghitung file dengan ekstensi umum (atau string lain), memasukkannya ke dalam perintah akan menghilangkan tambahan 2. Berikut adalah contohnya:\ls -afq *[0-9].pdb | wc -l
Steven C. Howell
FYI, dengan ksh93 version sh (AT&T Research) 93u+ 2012-08-01di sistem berbasis Debian saya, FIGNOREsepertinya tidak berfungsi. The .dan ..entri yang dimasukkan ke dalam array yang dihasilkan
Sergiy Kolodyazhnyy
17
find /foo/foo2/ -maxdepth 1 | wc -l

Jauh lebih cepat di komputer saya tetapi .direktori lokal ditambahkan ke hitungan.

Joel Taylor
sumber
1
Terima kasih. Namun saya terpaksa mengajukan pertanyaan konyol: mengapa lebih cepat? Karena tidak repot mencari atribut file?
Mike B
2
Ya, itulah pemahaman saya. Selama Anda tidak menggunakan -typeparameter findharus lebih cepat daripadals
Joel Taylor
1
Hmmm .... jika saya memahami dokumentasi menemukan dengan baik, ini sebenarnya harus lebih baik daripada jawaban saya. Adakah yang lebih berpengalaman dapat memverifikasi?
Luis Machuca
Tambahkan -mindepth 1untuk menghilangkan direktori itu sendiri.
Stéphane Chazelas
8

ls -1Usebelum pipa menghabiskan lebih sedikit sumber daya, karena tidak ada upaya untuk mengurutkan entri file, itu hanya membacanya karena mereka diurutkan dalam folder pada disk. Ini juga menghasilkan lebih sedikit output, yang berarti sedikit bekerja untuk wc.

Anda juga bisa menggunakan ls -fpintasan yang kurang lebih seperti itu ls -1aU.

Saya tidak tahu apakah ada cara hemat sumber daya untuk melakukannya melalui perintah tanpa pemipaan.

Luis Machuca
sumber
8
Btw, -1 tersirat ketika output masuk ke pipa
enzotib
@enzotib - benar? Wow ... orang belajar sesuatu yang baru setiap hari!
Luis Machuca
6

Titik perbandingan lain. Meskipun tidak menjadi shell oneliner, program C ini tidak melakukan apa pun yang berlebihan. Perhatikan bahwa file tersembunyi diabaikan agar sesuai dengan output dari ls|wc -l( ls -l|wc -ldimatikan oleh satu karena total blok di baris pertama 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);
}
Thomas Nyman
sumber
Menggunakan readdir()stdio API memang menambah beberapa overhead dan tidak memberi Anda kontrol atas ukuran buffer yang diteruskan ke panggilan sistem yang mendasarinya ( getdentsdi Linux)
Stéphane Chazelas
3

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

Akan menarik untuk membandingkan timing dengan pipa shell Anda.

selesai24
sumber
Pada pengujian saya, ini menjaga kecepatan yang persis sama dengan tiga solusi tercepat lainnya ( find -maxdepth 1 | wc -l, \ls -AU | wc -ldan zshgumpalan non sorting dan array array berdasarkan). Dengan kata lain itu mengalahkan opsi dengan berbagai inefisiensi seperti menyortir atau membaca properti file asing. Saya berani mengatakan karena itu tidak memberi Anda apa-apa juga, tidak layak menggunakan lebih dari solusi yang lebih sederhana kecuali jika Anda sudah dalam perl :):
Caleb
Perhatikan bahwa ini akan memasukkan entri direktori .dan ..dalam hitungan, jadi Anda perlu mengurangi dua untuk mendapatkan jumlah file yang sebenarnya (dan subdirektori). Dalam Perl modern, perl -E 'opendir $dh, "."; $i++ while readdir $dh; say $i - 2'akan melakukannya.
Ilmari Karonen
2

Dari jawaban ini , saya bisa memikirkan yang satu ini sebagai solusi yang memungkinkan.

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

Salin program C di atas ke dalam direktori di mana file harus terdaftar. Kemudian jalankan perintah-perintah ini:

gcc getdents.c -o getdents
./getdents | wc -l
Ramesh
sumber
1
Beberapa hal: 1) jika Anda bersedia menggunakan program kustom untuk ini, Anda mungkin juga cukup menghitung file dan mencetak hitungan; 2) untuk dibandingkan dengan ls -f, jangan filter d_typesama sekali, hanya di d->d_ino != 0; 3) kurangi 2 untuk .dan ...
Matei David
Lihat jawaban tertaut untuk contoh penetapan waktu di mana ini 40x lebih cepat dari yang diterima ls -f.
Matei David
1

Solusi bash-only, tidak memerlukan program eksternal, tetapi tidak tahu seberapa efisien:

list=(*)
echo "${#list[@]}"
enzotib
sumber
Ekspansi global tidak diperlukan cara paling efisien sumber daya untuk melakukan ini. Selain sebagian besar cangkang memiliki batas atas jumlah item yang akan mereka proses sehingga ini mungkin akan meledak ketika berhadapan dengan jutaan item plus, itu juga mengurutkan output. Solusi yang melibatkan find atau ls tanpa opsi pengurutan akan lebih cepat.
Caleb
@ Caleb, hanya versi lama ksh yang memiliki batasan seperti itu (dan tidak mendukung sintaks itu) AFAIK. Pada kebanyakan shell lainnya, batasnya hanyalah memori yang tersedia. Anda punya poin bahwa itu akan sangat tidak efisien, terutama dalam bash.
Stéphane Chazelas
1

Mungkin cara yang paling efisien sumber daya tidak melibatkan permintaan proses dari luar. Jadi saya bertaruh ...

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
sumber
1
Punya angka relatif? untuk berapa file?
smci
0

Setelah memperbaiki masalah dari jawaban @ Joel, di mana ia ditambahkan .sebagai file:

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

tailcukup menghapus baris pertama, artinya .tidak dihitung lagi.

haneefmubarak
sumber
1
Menambahkan sepasang pipa untuk menghilangkan satu jalur wcinput tidak terlalu efisien karena overhead meningkat secara linier berkenaan dengan ukuran input. Dalam hal ini, mengapa tidak hanya mengurangi jumlah akhir untuk mengimbanginya dengan satu, yang merupakan operasi waktu yang konstan:echo $(( $(find /foo/foo2 -maxdepth 1 | wc -l) - 1))
Thomas Nyman
1
Daripada memberi makan banyak data melalui proses lain, mungkin akan lebih baik untuk hanya melakukan beberapa matematika pada hasil akhir. let count = $(find /foo/foo2 -maxdepth 1 | wc -l) - 2
Caleb
0

os.listdir () dalam python dapat melakukan pekerjaan untuk Anda. Ini memberikan array dari isi direktori, tidak termasuk '.' dan file '..'. Juga, tidak perlu khawatir tentang file dengan karakter khusus seperti '\ n' dalam namanya.

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

berikut ini adalah waktu yang diambil oleh perintah python di atas dibandingkan dengan perintah 'ls -Af'.

~ / uji $ waktu ls -Af | wc -l
399144

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

0m0.249 nyata
pengguna 0m0.064s
sys 0m0.180s
indrajeet
sumber
0

ls -1 | wc -lsegera muncul di benak saya. Apakah ls -1Ulebih cepat daripada ls -1murni akademis - perbedaannya harus diabaikan tetapi untuk direktori yang sangat besar.

countermode
sumber
0

Untuk mengecualikan subdirektori dari hitungan, berikut adalah variasi pada jawaban yang diterima dari Gilles:

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

$(( ))Ekspansi aritmatika luar mengurangi output dari $( )subkulit kedua dari yang pertama $( ). Yang pertama $( )persis Gilles dari atas. Yang kedua $( )menampilkan jumlah direktori "yang menghubungkan" ke target. Ini berasal dari ls -od(gantikan ls -ldjika diinginkan), di mana kolom yang mencantumkan jumlah tautan keras memiliki itu sebagai makna khusus untuk direktori. The "link" count meliputi ., ..dan subdirektori apapun.

Saya tidak menguji kinerja, tetapi tampaknya akan serupa. Ia menambahkan stat dari direktori target, dan beberapa overhead untuk subkulit dan pipa yang ditambahkan.

pengguna361782
sumber
-2

Saya akan berpikir echo * akan lebih efisien daripada perintah 'ls':

echo * | wc -w
Dan Garthwaite
sumber
4
Bagaimana dengan file dengan spasi di namanya? echo 'Hello World'|wc -wmenghasilkan 2.
Joseph R.
@ JosephephR. Caveat Emptor
Dan Garthwaite