计算目录中有多少个文件的最节省资源的方法是什么?


55

CentOS 5.9

前几天,我遇到一个问题,其中目录包含许多文件。算一下,我跑了ls -l /foo/foo2/ | wc -l

事实证明,一个目录中有超过一百万个文件(长话说说,根本原因正在解决)。

我的问题是:有没有更快的方法来计数?什么是最有效的计数方法?


5
ls -l|wc -l由于ls -l输出的第一行中的总块数,该值将减少一个
Thomas Nyman

3
@ThomasNyman由于点和点点伪条目,它实际上将关闭几个,但是可以通过使用-A标志来避免这些伪条目。-l由于读取文件元数据以生成扩展列表格式,因此也存在问题。-l使用不强制使用\ls是更好的选择(-1在通过管道输出时假定使用。)有关最佳解决方案,请参见Gilles的答案
Caleb 2013年

2
@Caleb ls -l不会输出任何隐藏文件,也不会输出...条目。ls -a输出包括隐藏文件,包括 .和,..ls -A输出包括隐藏文件,排除 ...。在Gilles的答案中,bash dotglob shell选项使扩展包含 .以外的隐藏文件..
Thomas Nyman 2013年

Answers:


61

简短答案:

\ls -afq | wc -l

(这包括...,因此减去2。)


当您在目录中列出文件时,可能会发生三种常见情况:

  1. 枚举目录中的文件名。这是不可避免的:如果不枚举目录,就无法计数目录中的文件。
  2. 排序文件名。Shell通配符和ls命令可以执行此操作。
  3. 调用stat以检索有关每个目录条目的元数据,例如是否为目录。

#3是迄今为止最昂贵的,因为它需要为每个文件加载一个索引节点。相比之下,#1所需的所有文件名都紧凑地存储在几个块中。#2浪费了一些CPU时间,但通常不会破坏交易。

如果文件名中没有换行符,则简单ls -A | wc -l告诉您目录中有多少个文件。请注意,如果您具有的别名ls,这可能会触发对的调用stat(例如,ls --color或者ls -F需要知道文件类型,这需要对进行调用stat),因此请从命令行调用command ls -A | wc -l\ls -A | wc -l避免使用别名。

如果文件名中包含换行符,是否列出换行符取决于Unix变体。GNU coreutils和BusyBox默认显示?为换行符,因此很安全。

调用ls -f以列出条目而不对它们进行排序(#2)。这将自动打开-a(至少在现代系统上)。该-f选项位于POSIX中,但具有可选状态。大多数实现都支持它,但BusyBox不支持。该选项-q?;替换包括换行符在内的不可打印字符;它是POSIX,但不被BusyBox支持,因此如果您需要BusyBox支持,则可以忽略它,而要过多计算名称中包含换行符的文件。

如果目录没有子目录,则大多数版本find都不会调用stat其条目(叶目录优化:链接数为2的目录不能有子目录,因此find不需要查找条目的元数据,除非条件,如-type要求)。find . | wc -l如果目录中没有子目录并且文件名中不包含换行符,那么这是一种可移植的快速计数目录中文件的方法。

如果该目录没有子目录,但是文件名可能包含换行符,请尝试其中之一(如果受支持,则第二个换行符应该较快,但可能并不明显)。

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

另一方面,find如果目录具有子目录,则不要使用:甚至在每个条目上都find . -maxdepth 1调用stat(至少使用GNU find和BusyBox find)。您可以避免排序(#2),但要付出牺牲索引性能的代价(#3)。

在没有外部工具的Shell中,您可以使用来运行当前目录中的文件计数set -- *; echo $#。这会遗漏点文件(名称以开头的文件.),并在空目录中报告1而不是0。这是对小目录中文件进行计数的最快方法,因为它不需要启动外部程序,但是(由于zsh除外)由于排序步骤(#2),浪费了较大目录的时间。

  • 在bash中,这是一种对当前目录中的文件进行计数的可靠方法:

    shopt -s dotglob nullglob
    a=(*)
    echo ${#a[@]}
  • 在ksh93中,这是一种对当前目录中的文件进行计数的可靠方法:

    FIGNORE='@(.|..)'
    a=(~(N)*)
    echo ${#a[@]}
  • 在zsh中,这是一种对当前目录中的文件进行计数的可靠方法:

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

    如果已设置mark_dirs选项,请确保将其关闭:a=(*(DNoN^M))

  • 在任何POSIX Shell中,这都是一种对当前目录中的文件进行计数的可靠方法:

    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"

除了zsh以外,所有这些方法都对文件名进行排序。


1
我对超过100万个文件的经验测试表明,只要您不添加声明(例如必须做进一步检查的声明),就可以find -maxdepth 1轻松地与时俱进。您确定GNU查找实际调用了吗?如果使返回的文件详细信息,甚至连速度减慢都与没什么问题相比。另一方面,明显的速度优胜者正在使用非排序组。(已排序的glob的速度要比未排序的glob的速度快2倍)。我想知道文件系统类型是否会严重影响这些结果。\ls -U-typestatfind -typels -lzshls
Caleb 2013年

@Caleb我跑了strace。这仅在目录具有子目录的情况下才是正确的:否则find,叶目录优化会启动(即使没有-maxdepth 1),我也应该提到。可能会影响结果的很多因素,包括文件系统类型(将stat目录表示为线性列表的文件系统的调用要比将目录表示为树的文件系统的调用要昂贵得多),inode是否都一起创建并因此被关闭盘面上,冷或热缓存等上
吉尔“SO-停止作恶”

1
从历史上看,这是ls -f防止调用的可靠方法stat-今天通常将其简单地描述为“输出未排序”(这也会引起输出),并且确实包含...-A并且-U不是标准选项。
Random832 2013年

1
如果您特别想对具有公共扩展名(或其他字符串)的文件进行计数,请将其插入命令中可消除多余的2。这是一个示例:\ls -afq *[0-9].pdb | wc -l
Steven C. Howell

仅供参考,version sh (AT&T Research) 93u+ 2012-08-01在基于Debian的系统上使用ksh93 FIGNORE似乎不起作用。的...条目包括到所得的阵列
谢尔盖Kolodyazhnyy

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

在我的机器上要快得多,但是本地.目录已添加到计数中。


1
谢谢。但是我不得不问一个愚蠢的问题:为什么它更快?因为不麻烦查找文件属性?
Mike B

2
是的,这是我的理解。只要您不使用该-type参数的find速度应该快于ls
Joel Taylor

1
嗯...。如果我对find的文档了解得很好,那实际上应该比我的回答要好。有更多经验的人可以验证吗?
路易斯·马丘卡

添加a -mindepth 1可以省略目录本身。
斯特凡Chazelas

8

ls -1U由于管道不尝试对文件条目进行排序,因此管道应该只花费少一点的资源,它只是在磁盘文件夹中对它们进行排序时读取它们。它还产生较少的输出,这意味着的工作量稍少wc

您也可以使用ls -f或多或少的快捷方式ls -1aU

我不知道是否有一种资源有效的方法通过命令而不使用管道来完成。


8
顺便说一句,当输出到达管道时,意味着-1
enzotib

@enzotib-是吗?哇...每天都在学习新东西!
路易斯·马丘卡

6

另一个比较点。虽然不是Shell Oneliner,但此C程序不会做任何多余的事情。请注意,将忽略隐藏文件以匹配输出ls|wc -lls -l|wc -l由于输出的第一行中的总块数,所以偏移了一个)。

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

使用readdir()标准输入输出API确实增加了一些开销,不给你过传递到底层的系统调用(缓冲区的大小控制getdents在Linux上)
斯特凡Chazelas

3

你可以试试 perl -e 'opendir($dh,".");$i=0;while(readdir $dh){$i++};print "$i\n";'

将定时与外壳管道进行比较会很有趣。


在我的测试中,这与其他三个最快的解决方案(和find -maxdepth 1 | wc -l\ls -AU | wc -l以及zsh基于非排序的glob和数组计数)几乎完全相同。换句话说,它以各种低效率击败了选项,例如排序或读取无关的文件属性。我敢说,因为它也不赚钱,除非您碰巧已经在perl中,否则不值得在更简单的解决方案上使用它:)
Caleb 2013年

请注意,这将在计数中包括...目录条目,因此您需要减去2以获得文件(和子目录)的实际数量。在现代Perl中,perl -E 'opendir $dh, "."; $i++ while readdir $dh; say $i - 2'会做到这一点。
Ilmari Karonen 2013年

2

这个答案中,我可以认为这是一个可能的解决方案。

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

将上面的C程序复制到需要列出文件的目录中。然后执行以下命令:

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

1
一些事项:1)如果您愿意为此使用自定义程序,则不妨对文件进行计数并打印计数;2)与之比较ls -f,根本不过滤d_type,仅过滤d->d_ino != 0;3)为.和减去2 ..
Matei David

请参阅链接的答案,了解一个比接受的速度快40倍的计时示例ls -f
Matei David

1

仅bash的解决方案,不需要任何外部程序,但是不知道效率如何:

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

全局扩展不是执行此操作的最节省资源的方法。除了大多数外壳甚至对要处理的项目数量有上限之外,因此当处理一百万个以上的项目时,这很可能会爆炸,它还会对输出进行排序。没有排序选项的涉及find或ls的解决方案将更快。
Caleb 2013年

@Caleb,只有旧版本的ksh具有这样的限制(并且不支持该语法)。在所有其他大多数Shell中,限制就是可用内存。您已经知道它将非常低效,尤其是在bash中。
StéphaneChazelas

1

可能节省资源的方法将不涉及任何外部流程调用。所以我赌...

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
有相对数字?多少个文件?
smci

0

解决了@Joel的答案中的问题后,将其添加.为文件:

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

tail只需删除第一行,就意味着.不再计算。


1
添加一对管道以省略一行wc输入不是很有效,因为开销随输入大小线性增加。在这种情况下,为什么不简单地将最终计数递减以补偿它被减一,这是一个恒定的时间操作:echo $(( $(find /foo/foo2 -maxdepth 1 | wc -l) - 1))
Thomas Nyman 2013年

1
与其在另一个过程中输入大量数据,不如对最终输出进行一些数学运算可能会更好。let count = $(find /foo/foo2 -maxdepth 1 | wc -l) - 2
Caleb 2013年

0

python中的os.listdir()可以为您完成这项工作。它给出了目录内容的数组,不包括特殊的“。”。和“ ..”文件。此外,无需担心名称中带有特殊字符(如“ \ n”)的abt文件。

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

以下是上述python命令与“ ls -Af”命令相比所花费的时间。

〜/ test $ time ls -Af | wc -l
399144

真正的0m0.300s
用户0m0.104s
sys 0m0.240s
〜/ test $ time python -c'import os; print len(os.listdir(“。”))'
399142

真正的0m0.249s
用户0m0.064s
sys 0m0.180s

0

ls -1 | wc -l立刻浮现在我的脑海。是否ls -1Uls -1纯粹学术上要快-差异应该可以忽略不计,但对于非常大的目录而言。


0

要从计数中排除子目录,以下是Gilles接受的答案的变体:

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

外部$(( ))算术展开式从第一$( )个子壳减去第二个子壳的输出$( )。首先$( )是吉尔斯从上面来的。第二个$( )输出“链接”到目标的目录数。这来自ls -odls -ld如果需要,可以替代),其中列出硬链接数的列将其作为目录的特殊含义。“链接”计数包括...和任何子目录。

我没有测试性能,但是看起来很相似。它添加了目标目录的统计信息,并增加了子外壳程序和管道的开销。


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.