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

189

我正在尝试找出一种最佳的方法,在有大量文件(超过100,000个)的情况下,查找特定目录中的文件数量。

当存在如此多的文件时,执行 ls | wc -l 要花费相当长的时间。我认为这是因为它返回了所有文件的名称。 我正在尽可能少地使用磁盘I/O。

我已经尝试了一些Shell和Perl脚本,但都没有成功。 我该怎么做?


3
确保你的“ls”是“/usr/bin/ls”,而不是别的更复杂的别名。 - glenn jackman
类似的编程问题在这里有一些有趣的答案: http://serverfault.com/questions/205071/fast-way-to-recursively-count-files-in-linux - aidan
值得指出的是,对于这个问题提出的大多数解决方案(如果不是全部),都不仅适用于Linux,而且适用于所有类Unix系统。也许删除“Linux”标签是合适的。 - Christopher Schultz
17个回答

259

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

ls -f | wc -l

注意: 这也将启用-a,因此以.开头的...和其他文件也会被计算在内。


20
+1 我原以为我已经对“ls”命令了如指掌了。 - mob
7
对于 ls 命令在每个文件上进行 stat() 调用的操作来说,对 10 万行的排序算不了什么。相比之下,find 命令不会进行 stat() 操作,因此速度更快。注意保持原意,使文意更通俗易懂。 - Dummy00001
16
ls -f 不执行 stat()。但是当特定选项被使用时,例如 ls -lfind -mtimelsfind 都会调用 stat() - mark4o
10
为了提供背景,这在一个较小的Slicehost服务器上花费1-2分钟来计算250万个jpg文件。 - philfreo
9
如果您想要将子目录计入总数,请执行 ls -fR | wc -l - Ryan Walls
显示剩余11条评论

86
最快的方法是使用专门的程序,例如这个:
#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等系统上运行此程序,欢迎报告错误; 我很乐意更新这个程序。
正如你所看到的,它比原来的复杂得多,这是必要的:至少必须存在一个函数以便递归调用,否则代码将变得非常复杂(例如管理子目录堆栈并在单个循环中处理)。由于我们必须检查文件类型,因此不同操作系统之间的差异、标准库等都会产生影响,所以我编写了一个程序,试图在任何能编译它的系统上都可用。
几乎没有什么错误检查,而且函数本身并没有真正报告错误。实际上只有和这两个调用可能会失败(如果你不幸运地拥有一个已经包含文件类型的系统)。我并不是非常担心检查子目录路径名的总长度,但在理论上,系统不应该允许任何超过的路径名。如果有疑虑,我可以解决它,但这只是需要向学习编写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 提交 pull-request 来更容易地建议修改。

源代码在Apache许可证2.0下可用。欢迎提交补丁*


  • “patch”是我们这些老年人所称的“pull request”。

3
太好了!谢谢! 顺便提醒一下不知道的人:你可以在终端中编译上述代码: gcc -o dircnt dircnt.c,然后像这样使用它:./dircnt some_dir - aesede
1
@ChristopherSchultz,您上面发布的基准测试中,涉及到的目录有多大? - Dom Vinyard
1
我真的很想在Python中使用它,所以我将其打包成ffcount软件包。感谢@ChristopherSchultz提供代码! - GjjvdBurg
1
我不是说这很快,但我发起了一个“查找”,意识到它会花很长时间,但还是让它继续运行,然后在谷歌上搜索更好的工具,找到了这个页面,安装了用于vscode的C工具和扩展,以便能够编译C程序,将这段代码粘贴到vscode中,构建并执行这段代码以获得所需的结果,并且在“查找”完成之前就完成了。 - Matt Parkins
1
我不是说这个很快,但我启动了一个“查找”,意识到它需要很长时间,但还是让它继续运行,然后在谷歌上寻找更好的工具,找到了这个页面,安装了用于vscode的C工具和扩展,这样我就可以编译C程序,将这段代码粘贴到vscode中,构建并执行这段代码以获得我所需的结果,而且在“查找”完成之前就完成了。 - undefined
显示剩余13条评论

41

使用find命令。例如:

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

1
这将在当前目录下递归地查找文件。 - mark4o
在我的系统上,find /usr/share | wc -l(约137,000个文件)比ls -R /usr/share | wc -l(包括目录名、目录总计和空行的大约160,000行)第一次运行时快25%,当比较后续(缓存)运行时至少快两倍。 - Dennis Williamson
14
如果他只想要当前目录,而不是整个树形结构,他可以在查找命令中添加“-maxdepth 1”选项。 - igustin
4
似乎 findls 更快的原因是你使用 ls 的方式。如果停止排序,则 lsfind 的性能相似。 - Christopher Schultz
3
为了加速 find + wc 命令,你可以只打印一个字符:find . -printf x | wc -c。否则,你会从整个路径中创建字符串并将其传递给 wc(多余的输入/输出操作)。 - ives
2
无论如何,您应该像@ives所示的那样使用“-printf”,因此当某些人使用带有换行符的文件名时,计数才是正确的。 - Toby Speight

20

findlsperl在测试4万个文件时速度相同(尽管我没有尝试清除缓存):

[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的opendirreaddir,可以同时完成相同的任务:

[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 确保绕过可能会稍微减慢速度的别名选项,-f 用于避免文件排序。没有 -flsfind/perl 慢两倍,除非 ls-f 一起使用,这时候时间似乎一样:

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

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

我希望有一些脚本可以直接询问文件系统,而无需所有不必要的信息。
这些测试是基于Peter van der Heijdenglenn jackmanmark4o的答案。

7
在测试之间一定要清除缓存。当我第一次在一个拥有100万个文件的外部2.5英寸硬盘驱动器上运行 ls -l | wc -l 命令时,操作需要大约3分钟才能完成。第二次只需12秒左右,如我所记得的那样。另外,这也可能取决于您的文件系统。我使用的是 Btrfs。 - Behrang
谢谢,Perl代码片段对我来说是解决方案。 $ time perl -e 'opendir D, "."; @files = readdir D; closedir D; print scalar(@files)."\n"' 1315029真实时间 0m0.580秒 用户时间 0m0.302秒 系统时间 0m0.275秒 - Pažout
你可以通过仅打印单个字符来加快 find + wc 的速度:find . -printf x | wc -c。否则,您将从整个路径创建字符串并将其传递给 wc(额外的 I/O)。 - ives

8

令我惊讶的是,一个简单的find命令和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 返回两个额外的单位,因为它还计算 . 和 ..)。

7

快速的Linux文件计数

我知道的最快的Linux文件计数方法是:

locate -c -r '/home'

不需要调用grep!但是如前所述,您应该拥有一个新鲜的数据库(由cron作业每天更新,或通过sudo updatedb手动更新)。

来自locate手册

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

另外要注意的是,它也将目录视为文件!


顺带一提:如果您想查看系统中文件和目录的概述,请输入以下命令:

locate -S

它输出目录、文件等数量。


1
请确保数据库是最新的。 - phuclv
3
如果你的数据库中已经包括了所有需要计算的数据,那么你肯定能够快速地进行计算。 :) - Christopher Schultz
这对于近似值和估计是合理的,但不适用于验证数据迁移等任务。 - ives

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,

2
我觉得这个例子有点混乱。我想知道为什么左边是数字而不是目录名。无论如何,谢谢您,我最终使用了它并进行了一些微小的调整(计算目录并删除基本文件夹名称。for i in $(ls -1 . | sort -n) ; { echo "$i => $(find ${i} | wc -l)"; })。 - TheJacobTaylor
1
左边的数字是我示例数据中的目录名称。抱歉造成困惑。 - mightybs
1
如果没有更多的空间,ls -1 ${dir} 将无法正常工作。此外,并不能保证ls返回的名称可以传递给 find,因为 ls 会转义非打印字符以便人类使用。(如果你想要一个特别有趣的测试用例,请使用mkdir $'oddly\nnamed\ndirectory')。请参考 为什么不应该解析ls(1)的输出 - Charles Duffy

3

ls 命令需要花费更多的时间来对文件名进行排序。使用 -f 参数可以禁用排序,这样可以节省一些时间:

ls -f | wc -l

或者您可以使用 find

find . -type f | wc -l

3
您可以使用tree程序来获取文件和目录的计数。
运行命令tree | tail -n 1以获取最后一行,该行将显示类似于“763 directories, 9290 files”的内容。这个命令会递归地计算文件和文件夹的数量,但不包括隐藏文件,如果需要包括隐藏文件,可以添加标志-a。参考资料显示,在我的电脑上,tree 计算整个 home 目录花费了4.8秒,其中包括24,777个目录和238,680个文件。而find -type f | wc -l命令需要5.3秒,比 tree 命令慢了半秒钟,因此我认为 tree 在速度方面相当有竞争力。
只要没有子文件夹,tree 就是一种快速简便的统计文件的方法。
此外,仅出于好玩的目的,您可以使用tree | grep '^├'仅显示当前目录中的文件/文件夹 - 这基本上是ls命令的缓慢版本。

Brew install tail for OS X. - The Unfun Cat
@TheUnfunCat 在你的 Mac OS X 系统上应该已经安装了 tail - Christopher Schultz

2

当我试图计算大约有10,000个文件夹,每个文件夹大约有10,000个文件的数据集中的文件数时,我来到了这里。许多方法的问题在于它们隐式地统计了1亿个文件,而这需要很长时间。

我采用了扩展Christopher Schultz的方法的自由,以便通过参数传递目录(他的递归方法也使用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/directory/*

在一千万个文件和一万个文件夹中,上述操作很快就能完成(第一次运行大约需要5分钟,后续在缓存中:大约23秒)。唯一另外一个不到一小时就完成的方法是使用 "ls" 命令,缓存时间大约为一分钟:"ls -f /your/directory/* | wc -l" 。但每个目录的行数会有两个换行符的误差......出乎意料的是,我用 "find" 命令的所有尝试都没有在一个小时内返回结果 :-/

对于不是C程序员的人来说,你能解释一下为什么这样做会更快,并且如何在不执行相同操作的情况下得到相同的答案吗? - mlissner
你不需要成为C程序员,只需要理解如何启动文件以及目录是如何表示的:目录本质上是文件名和索引节点的列表。如果你启动一个文件,你就可以访问某个位置上的索引节点,例如获取文件大小、权限等信息。如果你只对每个目录中的计数感兴趣,那么你不需要访问索引节点信息,这可能会节省很多时间。 - Jörn Hees
这段代码在Oracle Linux上会出现段错误,gcc版本为4.8.5 20150623(Red Hat 4.8.5-28.0.1)(GCC)...相对路径和远程文件系统似乎是原因。 - Rondo
关于 *"The count is off by a couple of newlines per directory though"*:这可以通过将 -f-A(大写字母 'a')组合来解决:ls -f -A。选项 -f 启用了 -a(小写字母 'a'),但是它可以被 -A 覆盖。这已经在 ls 版本 8.30 中进行了测试。 - Peter Mortensen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接