Який найбільш ресурсно ефективний спосіб підрахувати кількість файлів у каталозі?


55

CentOS 5.9

Днями я зіткнувся з проблемою, де в каталозі було багато файлів. Щоб порахувати, я побігls -l /foo/foo2/ | wc -l

Виявляється, в одному каталозі було понад 1 мільйон файлів (довга історія - виправлення першопричини).

Моє запитання: чи є більш швидкий спосіб зробити підрахунок? Який був би найефективніший спосіб отримати підрахунок?


5
ls -l|wc -lбуло б вимкнене на один через загальну кількість блоків у першому рядку ls -lвипуску
Thomas Nyman

3
@ThomasNyman Це насправді буде вимкнено декількома через записи псевдо крапки та дотдоту, але їх можна уникнути, використовуючи -Aпрапор. -lтакож є проблематичним через метадані читання файлів з метою генерування розширеного формату списку. Примушувати НЕ -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 - найдорожчий на сьогоднішній день, оскільки він вимагає завантаження inode для кожного файлу. Для порівняння, всі назви файлів, необхідні для №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), але ви платите ціну пошуку inode (# 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 рази повільніші, ніж в lsтой час, як несортируюча - у 2 рази швидша). Цікаво, чи можуть типи файлової системи значно вплинути на ці результати.
Калеб

@Caleb я побіг strace. Це справедливо лише в тому випадку, якщо в каталозі є підкаталоги: інакше findоптимізація каталогів листів починається (навіть без -maxdepth 1), я повинен був це зазначити. На результат може вплинути багато речей, включаючи тип файлової системи (виклик statнабагато дорожчий у файлових системах, які представляють каталоги як лінійні списки, ніж у файлових системах, що представляють каталоги як дерева), незалежно від того, чи всі індекси були створені разом і, таким чином, близькі на диску, холодному або гарячому кеші тощо.
Жил "SO- перестань бути злим"

1
Історично склалося, ls -fщо це надійний спосіб запобігти виклику stat- це часто просто описується сьогодні як "вихід не сортується" (що це також спричиняє), а включає .і ... -Aі -Uне є стандартними варіантами.
Випадково832

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

FYI, з 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
Гммм .... якщо я добре розумію документацію, знайду , це насправді має бути кращим, ніж моя відповідь. Хто може мати більше досвіду?
Луїс Мачука

Додайте а, -mindepth 1щоб пропустити сам каталог.
Стефан Шазелас

8

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

Ви також можете використовувати ls -fякий є більш-менш ярликом ls -1aU.

Я не знаю, чи існує ресурсоефективний спосіб зробити це за допомогою команди, не маючи трубопроводів.


8
Btw, -1 мається на увазі, коли вихід іде на трубу
enzotib

@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)
Stéphane 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 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

Рішення лише для удару, не вимагає жодної зовнішньої програми, але не знаю, наскільки ефективно:

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

Розширення глобальної сфери не є найбільш ефективним способом цього зробити. Окрім того, що більшість снарядів мають верхню межу кількості предметів, які вони навіть оброблять, тому, ймовірно, це бомбить при роботі з мільйонами плюс пунктів, вона також сортує вихід. Рішення, що включають пошук або ls без сортування варіантів, будуть швидшими.
Калеб

@Caleb, лише старі версії ksh мали такі обмеження (і не підтримували цей синтаксис) AFAIK. У всіх більшості інших оболонок межами є лише наявна пам'ять. Ви зрозуміли, що це буде дуже неефективно, особливо в басі.
Стефан Шазелас

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'.

~ / тест $ time ls -Af | wc -l
399144

реальні 0м0.300с
користувач 0m0.104s
sys 0m0.240s
~ / test $ time python -c 'import os; print len ​​(os.listdir ("."))'
399142

реальні 0м0.249с
користувач 0m0.064s
sys 0m0.180s

0

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


0

Щоб виключити підкаталоги з підрахунку, ось варіант прийнятої відповіді від Gilles:

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

Зовнішнє $(( ))арифметичне розширення віднімає вихід другого $( )підшару з першого $( ). Перший $( )- це саме Жил зверху. Друга $( )виводить кількість каталогів, що "пов'язують" ціль. Це походить від ls -od(заміни ls -ldза бажанням), де стовпець, у якому перераховано кількість жорстких посилань, має особливе значення для каталогів. «Посилання» включає в себе підрахунок ., ..і будь-які підкаталоги.

Я не перевіряв продуктивність, але, здавалося б, був подібний. Він додає статтю цільового каталогу та деяку накладну частину для доданого підшару і труби.


-2

Я думаю, що echo * буде ефективнішим за будь-яку команду 'ls':

echo * | wc -w

4
Що з файлами з пробілом у їх імені? echo 'Hello World'|wc -wвиробляє 2.
Джозеф Р.

@JosephR. Caveat Emptor
Dan Garthwaite
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.