当有大量文件(超过 100,000 个)时,我试图找出在特定目录中查找文件数量的最佳方法。
当有那么多文件时,执行ls | wc -l
需要很长时间才能执行。我相信这是因为它返回了所有文件的名称。我试图尽可能少地占用磁盘 I/O。
我尝试了一些 shell 和 Perl 脚本,但无济于事。我该怎么做?
默认情况下ls
对名称进行排序,如果名称很多,这可能需要一段时间。在所有名称都被读取和排序之前,也不会有输出。使用该ls -f
选项关闭排序。
ls -f | wc -l
注意:这也将启用-a
, 所以.
,..
和其他以开头的文件.
将被计算在内。
最快的方法是专门构建的程序,如下所示:
#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
函数本身并不会真正报告错误。唯一可能真正失败的调用是opendir
and stat
(如果你不走运并且有一个dirent
已经包含文件类型的系统)。我对检查子目录路径名的总长度并不偏执,但理论上,系统不应允许任何长于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 建议的两项更改:
lstat
而不是stat
. 如果您正在扫描的目录中有符号链接目录,这将改变程序的行为。以前的行为是(链接的)子目录将其文件计数添加到总计数中;新行为是链接目录将计为单个文件,其内容将不计入。编辑 2017-06-29
运气好的话,这将是这个答案的最后一次编辑:)
我已将此代码复制到GitHub 存储库中,以便更轻松地获取代码(而不是复制/粘贴,您只需下载源代码即可),而且它使任何人都可以更轻松地通过提交拉取来建议修改-来自 GitHub 的请求。
源代码在 Apache 许可证 2.0 下可用。补丁* 欢迎!
使用查找。例如:
find . -name "*.ext" | wc -l
对 40,000 个文件进行测试的find、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
[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
避免文件排序。
ls
without-f
比find
/慢两倍,perl
除非ls
与 with 一起使用-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的答案。
令我惊讶的是,一个简单的发现与 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
返回两个额外的单元,因为它也计算 . 和 ..)。
我所知道的最快的 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
它输出目录、文件等的数量。
您可以根据您的要求更改输出,但这是我编写的一个 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,
ls
花更多时间对文件名进行排序。用于-f
禁用排序,这将节省一些时间:
ls -f | wc -l
或者您可以使用find
:
find . -type f | wc -l
您可以使用树程序获取文件和目录的计数。
运行命令tree | tail -n 1
以获取最后一行,这将显示类似“763 个目录,9290 个文件”之类的内容。这会递归计算文件和文件夹,不包括隐藏文件,可以使用 flag 添加-a
。作为参考,在我的计算机上,tree 花了 4.8 秒来计算我的整个主目录,即 24,777 个目录,238,680 个文件。find -type f | wc -l
花了 5.3 秒,多半秒,所以我认为树在速度方面很有竞争力。
只要您没有任何子文件夹,tree就是一种快速简便的文件计数方法。
此外,纯粹为了好玩,您可以使用tree | grep '^├'
仅显示当前目录中的文件/文件夹 - 这基本上是一个慢得多的ls
.
You should use "getdents" in place of ls/find
Here is one very good article which described the getdents approach.
http://be-n.com/spw/you-can-list-a-million-files-in-a-directory-but-not-with-ls.html
Here is the extract:
ls
and practically every other method of listing a directory (including Python's os.listdir and find .
) rely on libc readdir(). However, readdir() only reads 32K of directory entries at a time, which means that if you have a lot of files in the same directory (e.g., 500 million directory entries) it is going to take an insanely long time to read all the directory entries, especially on a slow disk. For directories containing a large number of files, you'll need to dig deeper than tools that rely on readdir(). You will need to use the getdents() system call directly, rather than helper methods from the C standard library.
We can find the C code to list the files using getdents() from here:
There are two modifications you will need to do in order quickly list all the files in a directory.
First, increase the buffer size from X to something like 5 megabytes.
#define BUF_SIZE 1024*1024*5
Then modify the main loop where it prints out the information about each file in the directory to skip entries with inode == 0. I did this by adding
if (dp->d_ino != 0) printf(...);
In my case I also really only cared about the file names in the directory so I also rewrote the printf() statement to only print the filename.
if(d->d_ino) printf("%sn ", (char *) d->d_name);
Compile it (it doesn't need any external libraries, so it's super simple to do)
gcc listdir.c -o listdir
Now just run
./listdir [directory with an insane number of files]
如果使用opendir()
and readdir()
inPerl
更快,您可以尝试。有关这些功能的示例,请查看此处。
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:它不是递归的,但你可以修改它来实现。
对于非常大、非常嵌套的目录,这里的答案比此页面上的几乎所有其他内容都要快:
https://serverfault.com/a/691372/84703
locate -r '.' | grep -c "^$PWD"
当我试图计算一个包含大约 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;
}
在 a 之后,gcc -o dircnt_args dircnt_args.c
您可以像这样调用它:
dircnt_args /your/directory/*
在 10,000 个文件夹中的 1 亿个文件上,上述完成非常快(第一次运行大约 5 分钟,缓存后续:大约 23 秒)。
在不到一个小时内完成的唯一其他方法是ls
缓存大约 1 分钟:ls -f /your/directory/* | wc -l
. 不过,每个目录的计数会减少几个换行符......
除了预期之外,我的任何尝试都没有find
在一小时内返回:-/
我意识到,当你有大量数据时,不使用内存处理比“管道”命令要快。所以我将结果保存到一个文件中,然后进行分析:
ls -1 /path/to/dir > count.txt && wc-l count.txt
文件数最多的前 10 个目录。
dir=/ ; for i in $(ls -1 ${dir} | sort -n) ; { echo "$(find ${dir}${i} \
-type f | wc -l) => $i,"; } | sort -nr | head -10
我更喜欢使用以下命令来跟踪目录中文件数量的变化。
watch -d -n 0.01 'ls | wc -l'
该命令将保持一个窗口打开,以 0.1 秒的刷新率跟踪目录中的文件数。