Fast Linux File Count для большого количества файлов - PullRequest
113 голосов
/ 15 сентября 2009

Я пытаюсь найти лучший способ узнать количество файлов в определенном каталоге, когда существует очень большое количество файлов (> 100 000).

Когда файлов так много, выполнение "ls | wc -l" занимает довольно много времени. Я считаю, что это потому, что он возвращает имена всех файлов. Я пытаюсь использовать как можно меньше дискового ввода-вывода.

Я экспериментировал с некоторыми сценариями оболочки и Perl, но безрезультатно. Есть идеи?

Ответы [ 18 ]

163 голосов
/ 15 сентября 2009

По умолчанию ls сортирует имена, что может занять некоторое время, если их много. Также не будет никакого вывода, пока все имена не будут прочитаны и отсортированы. Используйте параметр ls -f, чтобы отключить сортировку.

ls -f | wc -l

Обратите внимание, что это также активирует -a, поэтому будут учитываться ., .. и другие файлы, начинающиеся с ..

53 голосов
/ 06 февраля 2015

Самый быстрый способ - это специальная программа, подобная этой:

#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 и stat (если вам не повезло, и у вас есть система, в которой 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 2.0. Патчи * добро пожаловать!


  • "patch" - это то, что старые люди, такие как я, называют "запросом на извлечение".
32 голосов
/ 15 сентября 2009

Вы пытались найти? Например:

find . -name "*.ext" | wc -l
17 голосов
/ 24 февраля 2011

find, ls и perl проверены на 40 000 файлов: одинаковая скорость (хотя я не пытался очистить кеш):

[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, чтобы обойти параметр псевдонима, который может немного замедлить, и -f, чтобы избежать упорядочения файлов. ls без -f в два раза медленнее, чем find / perl кроме случаев, когда ls используется с -f, похоже, что это то же самое время:

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

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

Мне также хотелось бы, чтобы какой-нибудь скрипт запрашивал файловую систему напрямую без всей ненужной информации.

тесты, основанные на ответе Питера ван дер Хейдена, Гленна Джекмана и Марк4о.

Thomas

5 голосов
/ 13 февраля 2012

Вы можете изменить вывод в соответствии с вашими требованиями, но вот одна строчка для 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,
4 голосов
/ 03 февраля 2011

Удивительно для меня, но находка голой кости очень сравнима с 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 возвращает две дополнительные единицы, поскольку она также считает. И ..).

3 голосов
/ 15 февраля 2014

Просто добавлю это для полноты картины. Правильный ответ, конечно, уже был опубликован кем-то другим, но вы также можете получить количество файлов и каталогов с помощью древовидной программы.

Запустите команду tree | tail -n 1, чтобы получить последнюю строку, которая скажет что-то вроде «763 каталогов, 9290 файлов». Это подсчитывает файлы и папки рекурсивно, исключая скрытые файлы, которые могут быть добавлены с флагом -a. Для справки, моему компьютеру потребовалось 4,8 секунды, чтобы дерево посчитало весь мой домашний каталог, который состоял из 24777 каталогов, 238680 файлов. find -type f | wc -l заняло 5,3 секунды, на полсекунды дольше, так что я думаю, что дерево довольно конкурентоспособно по скорости.

Пока у вас нет подпапок, дерево - это быстрый и простой способ подсчета файлов.

Кроме того, и просто для удовольствия, вы можете использовать tree | grep '^├', чтобы показывать только файлы / папки в текущем каталоге - это в основном намного медленнее версия ls.

2 голосов
/ 16 января 2017

Запись здесь, так как у меня недостаточно репутационных очков для комментария к ответу, но мне разрешено оставить свой собственный ответ, что не имеет смысла. Во всяком случае ...

Что касается ответа Кристофера Шульца , я предлагаю изменить 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 голосов
/ 28 сентября 2015

Этот ответ здесь быстрее, чем все остальное на этой странице для очень больших, очень вложенных каталогов:

https://serverfault.com/a/691372/84703

locate -r '.' | grep -c "^$PWD"

2 голосов
/ 15 сентября 2009

Вы можете попробовать, если использовать opendir() и readdir() в Perl быстрее. Для примера этих функций смотрите здесь

...