Какой самый эффективный способ подсчета количества файлов в каталоге?


55

CentOS 5.9

Я столкнулся с проблемой на днях, когда в каталоге было много файлов. Чтобы посчитать это, я побежалls -l /foo/foo2/ | wc -l

Оказывается, в одном каталоге было более 1 миллиона файлов (длинная история - основная причина исправляется).

Мой вопрос: есть ли более быстрый способ сделать подсчет? Какой самый эффективный способ получить счет?


5
ls -l|wc -lбудет отключен на единицу из-за общего количества блоков в первой строке ls -lвывода
Томас Найман

3
@ThomasNyman На самом деле это может быть отключено несколькими из-за псевдо-записей точек и точек, но их можно избежать с помощью -Aфлага. -lтакже проблематично из-за чтения метаданных файла для создания расширенного формата списка. Принудительное использование NOT -lс помощью \lsгораздо более удобного варианта ( -1предполагается при выводе по конвейеру). См . Ответ Жиля для лучшего решения здесь.
Калеб

2
@Caleb ls -lне выводит никаких скрытых файлов , ни .и ..записи. ls -aвывод включает в себя скрытые файлы, в том числе . и в ..то время как ls -Aвывод включает в себя скрытые файлы, исключая . и ... В ответе Жиля dotglob опция оболочки bash заставляет расширение включать скрытые файлы, исключая . и ...
Томас Найман

Ответы:


61

Краткий ответ:

\ls -afq | wc -l

(Это включает .и .., так что вычтите 2.)


Когда вы перечисляете файлы в каталоге, могут произойти три общие вещи:

  1. Перечисление имен файлов в каталоге. Это неизбежно: нет способа подсчитать файлы в каталоге, не перечислив их.
  2. Сортировка имен файлов. Подстановочные знаки оболочки и lsкоманда делают это.
  3. Вызов statдля получения метаданных о каждой записи каталога, например, является ли он каталогом.

№ 3, безусловно, самый дорогой, потому что он требует загрузки индекса для каждого файла. Для сравнения, все имена файлов, необходимые для # 1, компактно хранятся в нескольких блоках. # 2 тратит некоторое процессорное время, но часто это не прерывает сделки.

Если в именах файлов нет новых строк, простой 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), который снижает производительность.

В оболочке без внешних инструментов вы можете запустить подсчет файлов в текущем каталоге с помощью 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 это надежный способ подсчета файлов в текущем каталоге:

    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
Мое эмпирическое тестирование> 1 миллиона файлов показывает, что оно find -maxdepth 1легко идет в ногу с тем, \ls -Uчто вы не добавляете ничего, как -typeобъявление, которое должно выполнять дополнительные проверки. Вы уверены, что GNU действительно находит звонки stat? Даже замедление find -type- ничто по сравнению с тем, сколько ls -lболот, если вы заставите его возвращать детали файла. С другой стороны, победитель с чистой скоростью zshиспользует несортировочный шар. (сортированные глобусы в 2 раза медленнее, чем несортируемые в 2 lsраза быстрее). Интересно, будут ли типы файловой системы значительно влиять на эти результаты.
Калеб

@ Калеб, я побежал strace. Это верно только в том случае, если в каталоге есть подкаталоги: в противном случае findначинается оптимизация конечного каталога (даже без -maxdepth 1), я должен был упомянуть об этом. На результат может повлиять множество факторов, в том числе тип файловой системы (вызов statв файловых системах, представляющих каталоги в виде линейных списков, обходится намного дороже, чем в файловых системах, представляющих каталоги в виде деревьев), независимо от того, все ли иноды были созданы вместе и, таким образом, находятся рядом. на диске, в холодном или горячем кеше и т. д.
Жиль "ТАК - перестань быть злым"

1
Исторически, ls -fэто был надежный способ предотвратить вызовы stat- сегодня это часто просто описывается как «вывод не отсортирован» (что он также вызывает), и включает в себя .и ... -Aи -Uне являются стандартными вариантами.
Random832

1
Если вы специально хотите посчитать файл с общим расширением (или другой строкой), вставка этого в команду исключает лишние 2. Вот пример:\ls -afq *[0-9].pdb | wc -l
Стивен С. Хауэлл

К вашему сведению, с ksh93 version sh (AT&T Research) 93u+ 2012-08-01в моей системе на основе Debian, FIGNOREпохоже, не работает. В .и ..записи включены в результирующий массив
Sergiy Kolodyazhnyy

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

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


1
Благодарю. Я вынужден задать глупый вопрос: почему это быстрее? Потому что это не мешает искать атрибуты файла?
Майк Б

2
Да, это мое понимание. Пока вы не используете -typeпараметр, findдолжно быть быстрее, чемls
Джоэл Тейлор

1
Хммм .... если я хорошо понимаю документацию по find , это должно быть лучше моего ответа. Кто-нибудь с большим опытом может проверить?
Луис Мачука

Добавьте a, -mindepth 1чтобы пропустить сам каталог.
Стефан Шазелас

8

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

Вы также можете использовать ls -fболее или менее ярлык для ls -1aU.

Я не знаю, есть ли ресурсосберегающий способ сделать это с помощью команды без трубопровода, хотя.


8
Кстати, -1 подразумевается, когда выход идет в трубу
энзотиб

@enzotib - это так? Ух ты ... каждый день узнает что-то новое!
Луис Мачука

6

Еще одна точка сравнения. Хотя эта программа на С не является оболочкой-оболочкой, она не делает ничего лишнего. Обратите внимание, что скрытые файлы игнорируются, чтобы соответствовать выводу ls|wc -l( ls -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 stdio добавляет некоторые издержки и не дает вам контроля над размером буфера, передаваемого базовому системному вызову ( getdentsв Linux)
Стефан Шазелас

3

Вы могли бы попробовать perl -e 'opendir($dh,".");$i=0;while(readdir $dh){$i++};print "$i\n";'

Было бы интересно сравнить время с вашей трубкой.


На моих тестах, это держит в значительной степени точно такие же темпы, что и три других быстрых решений ( find -maxdepth 1 | wc -l, \ls -AU | wc -lи zshпо нестандартной сортировке Глоба и массив счета). Другими словами, он выбивает параметры с различными недостатками, такими как сортировка или чтение посторонних свойств файла. Рискну сказать, так как он тоже ничего вам не зарабатывает, не стоит использовать более простое решение, если вы уже не в Perl :)
Caleb

Обратите внимание , что это будет включать в себя .и ..элементы каталога в счете, так что вам нужно вычесть два , чтобы получить фактическое количество файлов (и поддиректорий). В современном Perl, perl -E 'opendir $dh, "."; $i++ while readdir $dh; say $i - 2'сделал бы это.
Ильмари Каронен,

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 для .а ...
Матей Давид

См. Связанный ответ для примера времени, где это в 40 раз быстрее, чем принято ls -f.
Матей Давид

1

Решение только для bash, не требующее какой-либо внешней программы, но не знаю, насколько оно эффективно:

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

Расширение глобуса не является наиболее эффективным способом сделать это. Помимо большинства оболочек, имеющих верхний предел для количества предметов, которые они даже будут обрабатывать, так что это, вероятно, будет бомбить при работе с миллионами и более предметов, а также сортирует выходные данные. Решения с использованием find или ls без параметров сортировки будут быстрее.
Калеб

@Caleb, только старые версии ksh имели такие ограничения (и не поддерживали этот синтаксис) AFAIK. Во всех других оболочках пределом является только доступная память. У вас есть точка зрения, что это будет очень неэффективно, особенно в bash.
Стефан Шазелас

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))
Томас Найман

1
Вместо того, чтобы передавать столько данных через другой процесс, вероятно, было бы лучше просто выполнить некоторые математические расчеты для конечного результата. let count = $(find /foo/foo2 -maxdepth 1 | wc -l) - 2
Калеб

0

os.listdir () в python может сделать всю работу за вас. Это дает массив содержимого каталога, исключая специальный '.' и '..' файлы. Кроме того, нет необходимости беспокоиться о файлах abt со специальными символами, такими как '\ n' в имени.

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 -1Uэто быстрее, чем ls -1чисто академический - разница должна быть незначительной, но для очень больших каталогов.


0

Чтобы исключить подкаталоги из подсчета, вот вариант принятого ответа от Жиля:

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

Внешнее $(( ))арифметическое расширение вычитает выход второй $( )подоболочки из первого $( ). Первый $( )- это как раз Жиль сверху. Второй $( )выводит количество каталогов, «связывающих» с целью. Это происходит из ls -od(замените ls -ldпри желании), где столбец, который перечисляет количество жестких ссылок, имеет это как особое значение для каталогов. «Ссылка» включает в себя подсчет ., ..и любые подкаталоги.

Я не тестировал производительность, но, похоже, это похоже. Он добавляет статистику целевого каталога и некоторые накладные расходы для добавленных подоболочек и конвейера.


Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.