大量文件的快速Linux文件计数


136

当大量文件(> 100,000)时,我试图找出在特定目录中查找文件数的最佳方法。

当文件太多时,执行ls | wc -l将花费很长时间。我相信这是因为它正在返回所有文件的名称。我正在尝试占用尽可能少的磁盘IO。

我已经尝试了一些shell和Perl脚本,但无济于事。有任何想法吗?


2
确保您的“ ls”是/ usr / bin / ls,而不是幻想的别名。
格伦·杰克曼(09年

:有趣的答案,这里类似的问题serverfault.com/questions/205071/...
艾丹

值得指出的是,针对这个问题提出的大多数(如果不是全部)解决方案并非特定于Linux,而是对所有类似* NIX的系统通用。也许删除“ Linux”标签是适当的。
Christopher Schultz '18年

Answers:


188

默认情况下,ls对名称进行排序,如果名称很多,则可能需要一段时间。在所有名称都被读取和排序之前,也不会有输出。使用该ls -f选项可以关闭排序。

ls -f | wc -l

请注意,这也将让-a,所以...开始和其他文件.将被计算在内。


11
+1我以为我知道所有的事情ls
暴徒

5
ZOMG。10万行的排序是没有用的-与每个文件的stat()调用相比ls。因此,它find不能stat()更快地工作。
Dummy00001 2010年

12
ls -f也没有stat()。但是当然两者都可以,ls并且在使用某些选项(例如或)时find调用。stat()ls -lfind -mtime
mark4o 2010年

7
就上下文而言,这需要1-2分钟才能在一个小小的Slicehost盒子上计算250万个jpg。
philfreo 2011年

6
如果您想将子目录添加到计数中,请执行ls -fR | wc -l
Ryan Walls

62

最快的方法是专用程序,如下所示:

#include <stdio.h>
#include <dirent.h>

int main(int argc, char *argv[]) {
    DIR *dir;
    struct dirent *ent;
    long count = 0;

    dir = opendir(argv[1]);

    while((ent = readdir(dir)))
            ++count;

    closedir(dir);

    printf("%s contains %ld files\n", argv[1], count);

    return 0;
}

在不考虑缓存的情况下进行测试,为了避免基于缓存的数据偏斜,我一次又一次地对同一个目录运行了每种大约50次,并且获得了大致以下的性能数字(实时时钟):

ls -1  | wc - 0:01.67
ls -f1 | wc - 0:00.14
find   | wc - 0:00.22
dircnt | wc - 0:00.04

最后一个dircnt是从上述源代码编译的程序。

编辑2016-09-26

由于受欢迎的需求,我将该程序重写为递归程序,因此它将放到子目录中并继续分别计算文件和目录。

既然很明显有些人想知道如何做所有这些事情,所以代码中有很多注释,试图使事情变得显而易见。我编写了此代码,并在64位Linux上对其进行了测试,但是它可以在任何POSIX兼容系统上运行,包括Microsoft Windows。欢迎报告错误;如果您无法在AIX或OS / 400或任何版本上使用它,我很乐意对此进行更新。

正如你所看到的,它的很多比原来的和必然如此复杂:至少一个功能必须存在递归的,除非你想要的代码变得非常复杂,被称为(如管理一个子目录栈和处理,在一个循环中)。由于我们必须检查文件类型,因此会影响不同操作系统,标准库等之间的差异,因此,我编写了一个程序,试图在将要编译的任何系统上使用。

几乎没有错误检查,并且count函数本身并没有真正报告错误。唯一可能真正失败的调用是opendirstat(如果您不走运,并且拥有一个dirent已经包含文件类型的系统)。我对检查subdir路径名的总长度并不抱有幻想,但是从理论上讲,系统不应允许任何长度大于的路径名PATH_MAX。如果有问题,我可以解决,但是只是更多的代码需要向学习编写C的人解释。此程序旨在作为如何递归进入子目录的示例。

#include <stdio.h>
#include <dirent.h>
#include <string.h>
#include <stdlib.h>
#include <limits.h>
#include <sys/stat.h>

#if defined(WIN32) || defined(_WIN32) 
#define PATH_SEPARATOR '\\' 
#else
#define PATH_SEPARATOR '/' 
#endif

/* A custom structure to hold separate file and directory counts */
struct filecount {
  long dirs;
  long files;
};

/*
 * counts the number of files and directories in the specified directory.
 *
 * path - relative pathname of a directory whose files should be counted
 * counts - pointer to struct containing file/dir counts
 */
void count(char *path, struct filecount *counts) {
    DIR *dir;                /* dir structure we are reading */
    struct dirent *ent;      /* directory entry currently being processed */
    char subpath[PATH_MAX];  /* buffer for building complete subdir and file names */
    /* Some systems don't have dirent.d_type field; we'll have to use stat() instead */
#if !defined ( _DIRENT_HAVE_D_TYPE )
    struct stat statbuf;     /* buffer for stat() info */
#endif

/* fprintf(stderr, "Opening dir %s\n", path); */
    dir = opendir(path);

    /* opendir failed... file likely doesn't exist or isn't a directory */
    if(NULL == dir) {
        perror(path);
        return;
    }

    while((ent = readdir(dir))) {
      if (strlen(path) + 1 + strlen(ent->d_name) > PATH_MAX) {
          fprintf(stdout, "path too long (%ld) %s%c%s", (strlen(path) + 1 + strlen(ent->d_name)), path, PATH_SEPARATOR, ent->d_name);
          return;
      }

/* Use dirent.d_type if present, otherwise use stat() */
#if defined ( _DIRENT_HAVE_D_TYPE )
/* fprintf(stderr, "Using dirent.d_type\n"); */
      if(DT_DIR == ent->d_type) {
#else
/* fprintf(stderr, "Don't have dirent.d_type, falling back to using stat()\n"); */
      sprintf(subpath, "%s%c%s", path, PATH_SEPARATOR, ent->d_name);
      if(lstat(subpath, &statbuf)) {
          perror(subpath);
          return;
      }

      if(S_ISDIR(statbuf.st_mode)) {
#endif
          /* Skip "." and ".." directory entries... they are not "real" directories */
          if(0 == strcmp("..", ent->d_name) || 0 == strcmp(".", ent->d_name)) {
/*              fprintf(stderr, "This is %s, skipping\n", ent->d_name); */
          } else {
              sprintf(subpath, "%s%c%s", path, PATH_SEPARATOR, ent->d_name);
              counts->dirs++;
              count(subpath, counts);
          }
      } else {
          counts->files++;
      }
    }

/* fprintf(stderr, "Closing dir %s\n", path); */
    closedir(dir);
}

int main(int argc, char *argv[]) {
    struct filecount counts;
    counts.files = 0;
    counts.dirs = 0;
    count(argv[1], &counts);

    /* If we found nothing, this is probably an error which has already been printed */
    if(0 < counts.files || 0 < counts.dirs) {
        printf("%s contains %ld files and %ld directories\n", argv[1], counts.files, counts.dirs);
    }

    return 0;
}

编辑2017-01-17

我合并了@FlyingCodeMonkey建议的两个更改:

  1. 使用lstat代替stat。如果您正在扫描的目录中具有符号链接目录,则这将更改程序的行为。以前的行为是(链接的)子目录将其文件计数添加到总体计数中;新的行为是链接目录将被视为一个文件,并且其内容将不被计数。
  2. 如果文件路径太长,将发出错误消息,并且程序将停止。

编辑2017-06-29

运气好的话,这将是这个答案的最后编辑:)

我已将此代码复制到GitHub存储库中,以使其变得更容易获得代码(而不是复制/粘贴,您只需下载源代码即可),并且它使任何人都可以通过提交请求来更轻松地提出修改建议-来自GitHub的请求。

该源可在Apache License 2.0下获得。补丁* 欢迎您!


  • “补丁”就是像我这样的老人所说的“请求”。

2
太好了!谢谢!对于那些没有意识到的人:您可以在终端中编译上面的代码:gcc -o dircnt dircnt.c就像这样使用./dircnt some_dir
aesede 2015年

有没有简单的方法可以使此递归?
ck_

@ck_当然,可以很容易地使其递归。您是否需要有关解决方案的帮助,还是要我写整本书?
Christopher Schultz

1
@ChristopherSchultz,您在上面发布的基准-问题目录有多大?
Dom Vinyard

1
我真的很想在Python中使用它,所以我将其打包为ffcount包。感谢您将代码提供给@ChristopherSchultz!
GjjvdBurg

35

您尝试找到了吗?例如:

find . -name "*.ext" | wc -l

1
这将以递归方式在当前目录下查找文件。
mark4o

在我的系统上,find /usr/share | wc -l(〜137,000个文件)比ls -R /usr/share | wc -l每次(第一次)运行(〜160,000行,包括目录名称,目录总数和空行)快25%,比后续(缓存)运行的速度快至少两倍。
暂停,直到另行通知。

11
如果他只需要当前目录,而不是整个树,则可以添加-maxdepth 1选项来查找。
igustin

3
看来原因findls使用方法要快ls。如果停止排序,lsfind具有类似的性能。
Christopher Schultz

17

查找,对40 000个文件测试了ls和perl:相同的速度(尽管我没有尝试清除缓存):

[user@server logs]$ time find . | wc -l
42917

real    0m0.054s
user    0m0.018s
sys     0m0.040s
[user@server logs]$ time /bin/ls -f | wc -l
42918

real    0m0.059s
user    0m0.027s
sys     0m0.037s

并同时使用perl opendir / readdir:

[user@server logs]$ time perl -e 'opendir D, "."; @files = readdir D; closedir D; print scalar(@files)."\n"'
42918

real    0m0.057s
user    0m0.024s
sys     0m0.033s

注意:我使用/ bin / ls -f来确保绕过alias选项,这可能会降低速度,而-f则避免文件排序。不带-f的ls比find / perl慢两倍,除非ls与-f一起使用,这似乎是同时发生的:

[user@server logs]$ time /bin/ls . | wc -l
42916

real    0m0.109s
user    0m0.070s
sys     0m0.044s

我还希望有一些脚本直接询问文件系统,而无需所有不必要的信息。

根据Peter van der Heijden,glenn jackman和mark4o的答案进行测试。

汤玛士


5
您绝对应该清除两次测试之间的缓存。我第一次ls -l | wc -l在带有1M文件的外部2.5英寸硬盘驱动器上的文件夹上运行,大约需要3分钟才能完成操作。第二次IIRC需要12秒。这也可能取决于您的文件系统。用Btrfs
Behrang Saeedzadeh

谢谢,Perl片段对我来说是解决方案。 $ time perl -e 'opendir D, "."; @files = readdir D; closedir D; print scalar(@files)."\n"' 1315029 real 0m0.580s user 0m0.302s sys 0m0.275s
Pažout

5

您可以根据自己的要求更改输出,但这是我写的一个bash单行代码,用于递归计数和报告一系列数字命名目录中的文件数。

dir=/tmp/count_these/ ; for i in $(ls -1 ${dir} | sort -n) ; { echo "$i => $(find ${dir}${i} -type f | wc -l),"; }

递归查找给定目录中的所有文件(而非目录),并以类似哈希的格式返回结果。对find命令的简单调整可以使您希望计数的文件种类更加具体,等等。

结果如下:

1 => 38,
65 => 95052,
66 => 12823,
67 => 10572,
69 => 67275,
70 => 8105,
71 => 42052,
72 => 1184,

1
我发现该示例有些混乱。我想知道为什么左边没有数字,而不是目录名。不过,谢谢您,我最终做了一些小调整。(计算目录并删除基本文件夹名称。对于$(ls -1。| sort -n)中的i; {echo“ $ i => $(find $ {i} | wc -l)”;}
TheJacobTaylor

左边的数字是我的示例数据中的目录名称。抱歉,这令人困惑。
mayyybs 2014年

1
ls -1 ${dir}没有更多空间将无法正常工作。此外,也不能保证可以将返回的名称ls传递给find,因为ls转义了无法打印的字符以供人类使用。(mkdir $'oddly\nnamed\ndirectory'如果您想要一个特别有趣的测试用例)。请参阅为什么您不应该解析ls(1)的输出
Charles Duffy

4

令我惊讶的是,一个简单的发现与ls -f非常相似

> time ls -f my_dir | wc -l
17626

real    0m0.015s
user    0m0.011s
sys     0m0.009s

> time find my_dir -maxdepth 1 | wc -l
17625

real    0m0.014s
user    0m0.008s
sys     0m0.010s

当然,每次执行其中任何一个时,小数点后第三位的值都会移位一点,因此它们基本上是相同的。但是请注意,它find返回一个额外的单位,因为它会计算实际目录本身(并且如前所述,ls -f还会返回两个额外的单位,因为它也计算。和..)。


4

为了完整起见,仅添加此内容。正确的答案当然已经由其他人发布了,但是您还可以使用tree程序获得文件和目录的数量。

运行命令tree | tail -n 1以获取最后一行,该行将显示类似“ 763目录,9290文件”的内容。这会递归地计算文件和文件夹,但隐藏文件除外,隐藏文件可以与标记一起添加-a。作为参考,在我的计算机上花了4.8秒,树计算了我的整个主目录,该目录是24777个目录,238680个文件。find -type f | wc -l花了5.3秒,延长了半秒,所以我认为树在速度方面非常有竞争力。

只要您没有任何子文件夹,tree都是计数文件的快速简便的方法。

另外,纯粹出于乐趣,您可以使用tree | grep '^├'来仅显示当前目录中的文件/文件夹-这基本上是慢得多的版本ls


Brew install tail为OS X
的Unfun猫

@TheUnfunCat tail应该已经安装在Mac OS X系统上。
Christopher Schultz

4

快速Linux文件计数

我知道最快的linux文件数量是

locate -c -r '/home'

没有必要调用grep的!但是如上所述,您应该有一个新的数据库(每天由cron作业更新,或者由手动更新sudo updatedb)。

人找到

-c, --count
    Instead  of  writing  file  names on standard output, write the number of matching
    entries only.

另外,您应该知道它还将目录视为文件!


顺便说一句:如果要在系统类型上概述文件和目录

locate -S

它输出目录,文件等的数量。


请注意,您必须确保数据库是最新的
phuclv

1
大声笑,如果您已经在数据库中拥有所有计数,那么您当然可以快速计数。:)
Christopher Schultz,

3

由于我没有足够的声誉来评论答案,所以在这里写下来,但是我可以留下我自己的答案,这没有任何意义。无论如何...

关于Christopher Schultz回答,我建议将stat更改为lstat,并可能添加边界检查以避免缓冲区溢出:

if (strlen(path) + strlen(PATH_SEPARATOR) + strlen(ent->d_name) > PATH_MAX) {
    fprintf(stdout, "path too long (%ld) %s%c%s", (strlen(path) + strlen(PATH_SEPARATOR) + strlen(ent->d_name)), path, PATH_SEPARATOR, ent->d_name);
    return;
}

建议使用lstat来避免遵循符号链接,如果目录包含指向父目录的符号链接,则可能导致循环。


2
进行修改lstat是因为使用是一个很好的建议,您应该为此而报应。这个建议已合并到我上面和现在在GitHub上发布的代码中。
克里斯托弗·舒尔茨



2

当我尝试计算约1万个文件夹(每个文件约有10K个文件)的数据集中的文件时,我来到这里。许多方法的问题在于它们隐式统计100M文件,这需要很长时间。

我自由地使用christopher-schultz扩展了该方法,因此它支持通过args传递目录(他的递归方法也使用stat)。

将以下内容放入文件dircnt_args.c

#include <stdio.h>
#include <dirent.h>

int main(int argc, char *argv[]) {
    DIR *dir;
    struct dirent *ent;
    long count;
    long countsum = 0;
    int i;

    for(i=1; i < argc; i++) {
        dir = opendir(argv[i]);
        count = 0;
        while((ent = readdir(dir)))
            ++count;

        closedir(dir);

        printf("%s contains %ld files\n", argv[i], count);
        countsum += count;
    }
    printf("sum: %ld\n", countsum);

    return 0;
}

之后,gcc -o dircnt_args dircnt_args.c您可以像这样调用它:

dircnt_args /your/dirs/*

在10K文件夹中的100M文件中,以上操作很快完成(首次运行需要5分钟,对缓存的后续操作需要23秒)。

在不到一个小时的时间内完成的唯一另一种方法是ls,缓存大约1分钟:ls -f /your/dirs/* | wc -l。尽管每个目录有几个换行符,但计数...

除了预期之外,find一个小时内我的尝试都没有返回:-/


对于不是C程序员的人,您能解释一下为什么这样做会更快,以及不用做同样的事情就能得到相同的答案吗?
mlissner '18

您不需要成为C程序员,只需了解了解文件状态和目录表示方式的含义即可:目录本质上是文件名和索引节点的列表。如果您对文件进行统计,则可以访问驱动器上某处的索引节点,以获取例如文件大小,权限等信息。如果您仅对每个目录的计数感兴趣,则不需要访问inode信息,这可以节省大量时间。
约恩·希斯

这是Oracle Linux gcc版本4.8.5 20150623(Red Hat 4.8.5-28.0.1)(GCC)上的段错误...相对路径和远程fs似乎是原因
Rondo

2

在linux上(问题标记为linux)最快的方法是使用直接系统调用。这是一个小程序,用于计算目录中的文件(仅,无目录)。您可以计算数百万个文件,它比“ ls -f”快2.5倍,比Christopher Schultz的答案快1.3-1.5倍。

#define _GNU_SOURCE
#include <dirent.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/syscall.h>

#define BUF_SIZE 4096

struct linux_dirent {
    long d_ino;
    off_t d_off;
    unsigned short d_reclen;
    char d_name[];
};

int countDir(char *dir) {


    int fd, nread, bpos, numFiles = 0;
    char d_type, buf[BUF_SIZE];
    struct linux_dirent *dirEntry;

    fd = open(dir, O_RDONLY | O_DIRECTORY);
    if (fd == -1) {
        puts("open directory error");
        exit(3);
    }
    while (1) {
        nread = syscall(SYS_getdents, fd, buf, BUF_SIZE);
        if (nread == -1) {
            puts("getdents error");
            exit(1);
        }
        if (nread == 0) {
            break;
        }

        for (bpos = 0; bpos < nread;) {
            dirEntry = (struct linux_dirent *) (buf + bpos);
            d_type = *(buf + bpos + dirEntry->d_reclen - 1);
            if (d_type == DT_REG) {
                // Increase counter
                numFiles++;
            }
            bpos += dirEntry->d_reclen;
        }
    }
    close(fd);

    return numFiles;
}

int main(int argc, char **argv) {

    if (argc != 2) {
        puts("Pass directory as parameter");
        return 2;
    }
    printf("Number of files in %s: %d\n", argv[1], countDir(argv[1]));
    return 0;
}

PS:它不是递归的,但是您可以对其进行修改以实现该目的。


1
我不确定我是否同意这样做会更快。我没有追溯到编译器使用opendir/ 所做的所有事情readdir,但我怀疑它最终可以归结为几乎相同的代码。以这种方式进行系统调用也不是可移植的,并且由于Linux ABI不稳定,因此不能保证在一个系统上编译的程序可以在另一个系统上正常工作(尽管在任何* NIX系统IMO上从源代码进行编译都是相当不错的建议)。如果速度是关键,那么如果它实际上可以提高速度,那么这是一个很好的解决方案-我没有分别对程序进行基准测试。
Christopher Schultz


0

我意识到,当您拥有大量数据时,不使用内存处理比“添加”命令最快。所以我将结果保存到文件中,并进行了分析

ls -1 /path/to/dir > count.txt && cat count.txt | wc -l

这不是最快的解决方案,因为硬盘非常慢。还有其他更有效的方式发布在您之前的几年
phuclv

0

您应该使用“ getdents”代替ls / find

这是一篇非常好的文章,描述了getdents方法。

http://be-n.com/spw/you-can-list-a-million-files-in-a-directory-but-not-with-ls.html

这是摘录:

ls以及几乎所有其他列出目录的方法(包括python os.listdir,find。)都依赖于libc readdir()。但是readdir()一次只能读取32K目录条目,这意味着如果您在同一目录中有很多文件(即500M目录条目),那么将花费很长时间来读取所有目录条目,尤其是在慢速磁盘上。对于包含大量文件的目录,您需要比依赖readdir()的工具更深入地研究。您将需要直接使用getdents()系统调用,而不是libc的辅助方法。

我们可以从这里找到使用getdents()列出文件的C代码:

为了快速列出目录中的所有文件,您需要进行两项修改。

首先,将缓冲区大小从X增加到5兆字节左右。

#define BUF_SIZE 1024*1024*5

然后修改主循环,在该循环中打印出目录中每个文件的信息,以跳过inode == 0的条目。我通过添加

if (dp->d_ino != 0) printf(...);

就我而言,我实际上也只关心目录中的文件名,因此我也重写了printf()语句以仅打印文件名。

if(d->d_ino) printf("%sn ", (char *) d->d_name);

编译它(它不需要任何外部库,因此非常简单)

gcc listdir.c -o listdir

现在运行

./listdir [directory with insane number of files]

请注意,Linux会进行预读,因此readdir()实际上并不慢。在我相信有必要为此目的而放弃便携性之前,我需要有坚实的基础。
fuz

-1

我更喜欢以下命令来跟踪目录中文件数量的变化。

watch -d -n 0.01 'ls | wc -l'

该命令将使窗口保持打开状态,以0.1秒的刷新速率跟踪目录中的文件数。


您确定ls | wc -l在0.01s内包含成千上万个文件的文件夹是否可以完成?ls与其他解决方案相比,即使您的效率也非常低下。OP只是想获得计数,而不是坐在那里查看输出变化
phuclv

好。好。我找到了一个适合我的优雅解决方案。我想分享相同的内容,因此做到了。我不知道linux中的'ls'命令效率很低。您使用的是什么呢?0.01s是刷新率。不是时间。如果您不使用手表,请参阅手册页。
Anoop Toffy

好吧,我确实watch在评论后阅读了手册,并发现0.01s(而不是0.1s)是一个不切实际的数字,因为大多数PC屏幕的刷新率仅为60Hz,但这并不能以任何方式回答问题。OP询问“快速Linux计数大量文件”。在发布之前,您还没有阅读任何可用的答案
phuclv

我确实阅读了答案。但是我发布的是一种跟踪目录中文件数量变化的方法。例如:在将文件从一个位置复制到另一位置时,文件数量保持更改。使用我张贴者的方法,可以跟踪该情况。我同意我在任何地方所做的帖子都无法修改或改进以前的任何帖子。
Anoop Toffy

-2

前10名导演,无档案。

dir=/ ; for i in $(ls -1 ${dir} | sort -n) ; { echo "$(find ${dir}${i} \
    -type f | wc -l) => $i,"; } | sort -nr | head -10

3
当然,这看起来与maybybs编写的答案(具有相同的错误) 惊人地相似。如果您打算扩展或修改其他人编写的代码,则将其记为适当。足够了解您在答案中使用的代码以识别和修复其错误甚至合适。
查尔斯·达菲
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.