Cách hiệu quả nhất về tài nguyên để đếm có bao nhiêu tệp trong một thư mục?


55

CentOS 5.9

Tôi đã gặp một vấn đề vào một ngày khác, nơi một thư mục có rất nhiều tập tin. Để đếm nó, tôi chạyls -l /foo/foo2/ | wc -l

Hóa ra là có hơn 1 triệu tệp trong một thư mục (câu chuyện dài - nguyên nhân gốc đang được khắc phục).

Câu hỏi của tôi là: có cách nào nhanh hơn để đếm không? Điều gì sẽ là cách hiệu quả nhất để có được số đếm?


5
ls -l|wc -lsẽ bị tắt bởi một do tổng số khối trong dòng ls -lđầu ra đầu tiên
Thomas Nyman

3
@ThomasNyman Nó thực sự sẽ bị tắt bởi một số mục nhập giả và dấu chấm, nhưng những mục này có thể tránh được bằng cách sử dụng -Acờ. -lcũng có vấn đề vì dữ liệu meta của tệp đọc để tạo định dạng danh sách mở rộng. Buộc KHÔNG -lbằng cách sử dụng \lslà một tùy chọn tốt hơn nhiều ( -1được giả định khi đầu ra đường ống.) Xem câu trả lời của Gilles để có giải pháp tốt nhất tại đây.
Caleb

2
@Caleb ls -lkhông xuất bất kỳ tệp ẩn nào cũng như các mục ...mục. ls -ađầu ra bao gồm các tệp ẩn, bao gồm ...trong khi ls -Ađầu ra bao gồm các tệp ẩn không bao gồm .... Trong câu trả lời của Gilles, dotglob tùy chọn bash shell khiến việc mở rộng bao gồm các tệp ẩn không bao gồm ....
Thomas Nyman

Câu trả lời:


61

Câu trả lời ngắn:

\ls -afq | wc -l

(Điều này bao gồm ..., vì vậy trừ 2.)


Khi bạn liệt kê các tệp trong một thư mục, ba điều phổ biến có thể xảy ra:

  1. Liệt kê tên tập tin trong thư mục Điều này là không thể bỏ qua: không có cách nào để đếm các tệp trong một thư mục mà không liệt kê chúng.
  2. Sắp xếp tên tập tin. Shell wildcards và lslệnh làm điều đó.
  3. Gọi statđể lấy siêu dữ liệu về mỗi mục nhập thư mục, chẳng hạn như đó có phải là thư mục không.

# 3 là đắt nhất cho đến nay, bởi vì nó yêu cầu tải một nút cho mỗi tệp. Trong so sánh, tất cả các tên tệp cần thiết cho # 1 được lưu trữ gọn trong một vài khối. # 2 lãng phí một số thời gian CPU nhưng nó thường không phải là một bộ ngắt thỏa thuận.

Nếu không có dòng mới trong tên tệp, một đơn giản ls -A | wc -lcho bạn biết có bao nhiêu tệp trong thư mục. Lưu ý rằng nếu bạn có bí danh ls, điều này có thể kích hoạt một cuộc gọi đến stat(ví dụ ls --colorhoặc ls -Fcần biết loại tệp, yêu cầu gọi đến stat), vì vậy từ dòng lệnh, gọi command ls -A | wc -lhoặc \ls -A | wc -lđể tránh bí danh.

Nếu có dòng mới trong tên tệp, liệu dòng mới có được liệt kê hay không phụ thuộc vào biến thể Unix. GNU coreutils và BusyBox mặc định hiển thị ?cho một dòng mới, vì vậy chúng an toàn.

Gọi ls -fđể liệt kê các mục mà không sắp xếp chúng (# 2). Điều này tự động bật -a(ít nhất là trên các hệ thống hiện đại). Các -ftùy chọn là trong POSIX nhưng với tình trạng bắt buộc; hầu hết các triển khai đều hỗ trợ nó, nhưng BusyBox thì không. Tùy chọn -qthay thế các ký tự không in được, bao gồm cả dòng mới bằng ?; đó là POSIX nhưng không được BusyBox hỗ trợ, vì vậy hãy bỏ qua nếu bạn cần hỗ trợ BusyBox với chi phí vượt qua các tệp có tên chứa ký tự dòng mới.

Nếu thư mục không có thư mục con, thì hầu hết các phiên bản findsẽ không gọi statcác mục của nó (tối ưu hóa thư mục lá: thư mục có số lượng liên kết là 2 không thể có thư mục con, vì vậy findkhông cần phải tìm siêu dữ liệu của các mục trừ khi điều kiện như -typeyêu cầu nó). Vì vậy, find . | wc -lmột cách di động, nhanh chóng để đếm các tệp trong một thư mục với điều kiện thư mục đó không có thư mục con và không có tên tệp nào chứa một dòng mới.

Nếu thư mục không có thư mục con nhưng tên tệp có thể chứa dòng mới, hãy thử một trong những thư mục này (thư mục thứ hai sẽ nhanh hơn nếu được hỗ trợ, nhưng có thể không đáng chú ý như vậy).

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

Mặt khác, không sử dụng findnếu thư mục có thư mục con: thậm chí find . -maxdepth 1gọi stattrên mỗi mục (ít nhất là với GNU find và BusyBox find). Bạn tránh sắp xếp (# 2) nhưng bạn phải trả giá cho việc tra cứu inode (# 3) sẽ giết chết hiệu suất.

Trong shell không có công cụ bên ngoài, bạn có thể chạy đếm các tệp trong thư mục hiện tại với set -- *; echo $#. Điều này bỏ lỡ các tệp chấm (các tệp có tên bắt đầu .) và báo cáo 1 thay vì 0 trong một thư mục trống. Đây là cách nhanh nhất để đếm các tệp trong các thư mục nhỏ vì nó không yêu cầu bắt đầu một chương trình bên ngoài, nhưng (ngoại trừ trong zsh) lãng phí thời gian cho các thư mục lớn hơn do bước sắp xếp (# 2).

  • Trong bash, đây là một cách đáng tin cậy để đếm các tệp trong thư mục hiện tại:

    shopt -s dotglob nullglob
    a=(*)
    echo ${#a[@]}
  • Trong ksh93, đây là một cách đáng tin cậy để đếm các tệp trong thư mục hiện tại:

    FIGNORE='@(.|..)'
    a=(~(N)*)
    echo ${#a[@]}
  • Trong zsh, đây là một cách đáng tin cậy để đếm các tệp trong thư mục hiện tại:

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

    Nếu bạn có mark_dirstùy chọn được đặt, hãy đảm bảo tắt nó : a=(*(DNoN^M)).

  • Trong bất kỳ shell POSIX nào, đây là một cách đáng tin cậy để đếm các tệp trong thư mục hiện tại:

    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"

Tất cả các phương thức này sắp xếp tên tệp, ngoại trừ zsh.


1
Thử nghiệm thực nghiệm của tôi trên> 1 triệu tệp cho thấy find -maxdepth 1dễ dàng theo kịp \ls -Umiễn là bạn không thêm bất cứ điều gì như -typetuyên bố phải kiểm tra thêm. Bạn có chắc chắn GNU tìm thấy các cuộc gọi thực sự stat? Ngay cả việc chậm lại find -typecũng không là gì so với bao nhiêu ls -lbogs nếu bạn làm cho nó trả về chi tiết tập tin. Mặt khác, người chiến thắng tốc độ rõ ràng đang zshsử dụng quả cầu không sắp xếp. (các quả cầu được sắp xếp chậm hơn gấp 2 lần lstrong khi các khối không sắp xếp nhanh hơn gấp 2 lần). Tôi tự hỏi nếu các loại hệ thống tập tin sẽ ảnh hưởng đáng kể những kết quả này.
Caleb

@Caleb tôi chạy strace. Điều này chỉ đúng nếu thư mục có thư mục con: nếu không find, tối ưu hóa thư mục lá sẽ khởi động (ngay cả khi không có -maxdepth 1), tôi nên đề cập đến điều đó. Rất nhiều thứ có thể ảnh hưởng đến kết quả, bao gồm loại hệ thống tệp (việc gọi statđắt hơn rất nhiều trên các hệ thống tệp đại diện cho các thư mục dưới dạng danh sách tuyến tính so với các hệ thống tệp đại diện cho các thư mục dưới dạng cây), cho dù các nút được tạo cùng nhau và do đó gần nhau trên đĩa, bộ đệm lạnh hoặc nóng, v.v.
Gilles 'SO- ngừng trở nên xấu xa'

1
Trong lịch sử, ls -flà cách đáng tin cậy để ngăn chặn cuộc gọi stat- ngày nay thường được mô tả đơn giản là "đầu ra không được sắp xếp" (điều này cũng gây ra), và bao gồm .... -A-Ukhông phải là lựa chọn tiêu chuẩn.
Random832

1
Nếu bạn đặc biệt muốn đếm tệp có phần mở rộng chung (hoặc chuỗi khác), việc chèn tệp đó vào lệnh sẽ loại bỏ phần bổ sung 2. Dưới đây là một ví dụ:\ls -afq *[0-9].pdb | wc -l
Steven C. Howell

FYI, với ksh93 version sh (AT&T Research) 93u+ 2012-08-01trên hệ thống dựa trên Debian của tôi, FIGNOREdường như không hoạt động. Các mục ...được bao gồm trong mảng kết quả
Sergiy Kolodyazhnyy

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

Là nhanh hơn đáng kể trên máy của tôi nhưng .thư mục địa phương được thêm vào số đếm.


1
Cảm ơn. Tôi buộc phải hỏi một câu hỏi ngớ ngẩn: tại sao nó nhanh hơn? Bởi vì nó không bận tâm đến việc tra cứu các thuộc tính tệp?
Mike B

2
Vâng, đó là sự hiểu biết của tôi. Miễn là bạn không sử dụng -typetham số findsẽ nhanh hơnls
Joel Taylor

1
Hmmm .... nếu tôi hiểu tài liệu tìm kiếm tốt, điều này thực sự sẽ tốt hơn câu trả lời của tôi. Bất cứ ai có nhiều kinh nghiệm có thể xác minh?
Luis Machuca

Thêm một -mindepth 1để bỏ qua thư mục chính nó.
Stéphane Chazelas

8

ls -1Utrước khi đường ống chỉ tiêu tốn ít tài nguyên hơn một chút, vì nó không cố gắng sắp xếp các mục nhập tệp, nó chỉ đọc chúng khi chúng được sắp xếp trong thư mục trên đĩa. Nó cũng tạo ra đầu ra ít hơn, có nghĩa là công việc hơi ít wc.

Bạn cũng có thể sử dụng ls -fmột hoặc nhiều hơn một phím tắt cho ls -1aU.

Tôi không biết có cách nào hiệu quả về tài nguyên để thực hiện thông qua lệnh mà không cần đường ống hay không.


8
Btw, -1 được ngụ ý khi đầu ra đi vào đường ống
enzotib

@enzotib - là gì? Wow ... một người học được điều gì đó mới mỗi ngày!
Luis Machuca

6

Một điểm so sánh khác. Mặc dù không phải là một oneliner vỏ, chương trình C này không làm bất cứ điều gì thừa. Lưu ý rằng các tệp ẩn được bỏ qua để khớp với đầu ra của ls|wc -l( ls -l|wc -lbị tắt bởi một do tổng số khối trong dòng đầu ra đầu tiên).

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

Sử dụng readdir()API stdio sẽ thêm một số chi phí và không cho phép bạn kiểm soát kích thước của bộ đệm được chuyển đến lệnh gọi hệ thống cơ bản ( getdentstrên Linux)
Stéphane Chazelas

3

Bạn có thể thử perl -e 'opendir($dh,".");$i=0;while(readdir $dh){$i++};print "$i\n";'

Thật thú vị khi so sánh thời gian với ống vỏ của bạn.


Các bài kiểm tra của tôi, điều này sẽ giúp khá nhiều chính xác tốc độ giống như ba giải pháp nhanh nhất khác ( find -maxdepth 1 | wc -l, \ls -AU | wc -lzshdựa glob phân loại không và đếm mảng). Nói cách khác, nó vượt qua các tùy chọn với sự thiếu hiệu quả khác nhau như sắp xếp hoặc đọc các thuộc tính tệp không liên quan. Tôi muốn nói rằng vì nó cũng không mang lại cho bạn bất cứ điều gì, nó không đáng để sử dụng một giải pháp đơn giản hơn trừ khi bạn đã ở trong tình trạng nguy hiểm :)
Caleb

Lưu ý rằng điều này sẽ bao gồm các mục ...thư mục trong số đếm, vì vậy bạn cần trừ hai để có được số lượng tệp thực tế (và thư mục con). Trong Perl hiện đại, perl -E 'opendir $dh, "."; $i++ while readdir $dh; say $i - 2'sẽ làm điều đó.
Ilmari Karonen

2

Từ câu trả lời này , tôi có thể nghĩ về điều này như một giải pháp khả thi.

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

Sao chép chương trình C ở trên vào thư mục chứa các tệp cần được liệt kê. Sau đó thực hiện các lệnh sau:

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

1
Một vài điều: 1) nếu bạn sẵn sàng sử dụng một chương trình tùy chỉnh cho việc này, bạn cũng có thể chỉ cần đếm các tệp và in số đếm; 2) để so sánh với ls -f, không lọc d_typetất cả, chỉ bật d->d_ino != 0; 3) trừ 2 cho ....
Matei David

Xem câu trả lời được liên kết cho một ví dụ về thời gian trong đó nhanh hơn 40 lần so với chấp nhận ls -f.
Matei David

1

Một giải pháp chỉ bash, không yêu cầu bất kỳ chương trình bên ngoài nào, nhưng không biết hiệu quả đến mức nào:

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

Mở rộng Glob không cần thiết là cách hiệu quả nhất về tài nguyên để làm điều này. Bên cạnh đó, hầu hết các vỏ có giới hạn trên đối với số lượng vật phẩm mà chúng thậm chí sẽ xử lý, vì vậy điều này có thể sẽ đánh bom khi xử lý một triệu vật phẩm cộng, nó cũng sắp xếp đầu ra. Các giải pháp liên quan đến tìm hoặc ls mà không sắp xếp tùy chọn sẽ nhanh hơn.
Caleb

@Caleb, chỉ các phiên bản cũ của ksh có giới hạn như vậy (và không hỗ trợ cú pháp đó) AFAIK. Trong hầu hết các shell khác, giới hạn chỉ là bộ nhớ khả dụng. Bạn đã có một điểm rằng nó sẽ rất kém hiệu quả, đặc biệt là trong bash.
Stéphane Chazelas

1

Có lẽ cách hiệu quả nhất về tài nguyên sẽ không liên quan đến các yêu cầu bên ngoài. Vì vậy, tôi muốn đánh cuộc ...

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
Có số tương đối? cho bao nhiêu tập tin?
smci

0

Sau khi khắc phục sự cố từ câu trả lời của @Joel, nơi nó được thêm .dưới dạng tệp:

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

tailchỉ cần xóa dòng đầu tiên, nghĩa .là không được tính nữa.


1
Thêm một cặp đường ống để bỏ qua một dòng wcđầu vào không hiệu quả lắm vì chi phí tăng tuyến tính liên quan đến kích thước đầu vào. Trong trường hợp này, tại sao không chỉ đơn giản là giảm số đếm cuối cùng để bù cho nó bị tắt bởi một, đó là một hoạt động thời gian không đổi:echo $(( $(find /foo/foo2 -maxdepth 1 | wc -l) - 1))
Thomas Nyman

1
Thay vì cung cấp nhiều dữ liệu đó thông qua một quy trình khác, có lẽ sẽ tốt hơn nếu chỉ thực hiện một số phép toán trên đầu ra cuối cùng. let count = $(find /foo/foo2 -maxdepth 1 | wc -l) - 2
Caleb

0

os.listdir () trong python có thể làm việc cho bạn. Nó đưa ra một mảng các nội dung của thư mục, không bao gồm '.' và các tập tin '..'. Ngoài ra, không cần phải lo lắng các tệp abt có ký tự đặc biệt như '\ n' trong tên.

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

sau đây là thời gian của lệnh python ở trên so với lệnh 'ls -Af'.

~ / kiểm tra $ thời gian ls -Af | wc -l
399144

0m0.300 thực
người dùng 0m0.104s
sys 0m0.240s
~ / test $ time python -c 'nhập os; in len (os.listdir ("."))'
399142

số 0m0.249 thực
người dùng 0m0.064
sys 0m0.180s

0

ls -1 | wc -lđến ngay với tâm trí của tôi Cho dù ls -1Ulà nhanh hơn ls -1là hoàn toàn học tập - sự khác biệt nên không đáng kể nhưng đối với các thư mục rất lớn.


0

Để loại trừ các thư mục con khỏi số đếm, đây là một biến thể của câu trả lời được chấp nhận từ Gilles:

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

Sự $(( ))mở rộng số học bên ngoài trừ đi đầu ra của lớp con thứ hai $( )từ lớp thứ nhất $( ). Đầu tiên $( )là chính xác Gilles 'từ trên cao. Thứ hai $( )xuất ra số lượng thư mục "liên kết" đến mục tiêu. Điều này xuất phát từ ls -od(thay thế ls -ldnếu muốn), trong đó cột liệt kê số lượng liên kết cứng có ý nghĩa đặc biệt cho các thư mục. "Liên kết" đếm bao gồm ., ..và bất kỳ thư mục con.

Tôi đã không kiểm tra hiệu suất, nhưng nó có vẻ tương tự nhau. Nó thêm một chỉ số của thư mục đích và một số chi phí cho đường ống con và đường ống được thêm vào.


-2

Tôi nghĩ rằng echo * sẽ hiệu quả hơn bất kỳ lệnh 'ls' nào:

echo * | wc -w

4
Điều gì về các tập tin với một không gian trong tên của họ? echo 'Hello World'|wc -wsản xuất 2.
Joseph R.

@JosephR. Caveat Emptor
Dan Garthwaite
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.