ディレクトリ内のファイル数を数える最もリソース効率の良い方法は何ですか?


55

CentOS 5.9

先日、ディレクトリに多くのファイルがある問題に遭遇しました。それを数えるために、私は走ったls -l /foo/foo2/ | wc -l

1つのディレクトリに100万を超えるファイルがあったことがわかります(長い話-根本的な原因は修正されつつあります)。

私の質問は次のとおりです。カウントを行うより速い方法はありますか?カウントを取得する最も効率的な方法は何でしょうか?


5
ls -l|wc -l原因の第一行中の総ブロックにいずれかによってオフになりls -l、出力
トーマスナイマン

3
@ThomasNymanドットとドットドットの擬似エントリのために実際には数分ずれていますが、-Aフラグを使用することでそれらを回避できます。-lまた、拡張リスト形式を生成するためにファイルのメタデータを読み取るため、問題があります。NOT強制-l使用してすることは\ls非常に良いオプションです(-1出力をパイプ場合に想定される。)を参照してくださいジルの答えここに最善の解決策のために。
カレブ

2
@Calebは、ls -l出力任意の隠しファイルもない...のエントリを。ls -a出力は隠しファイル、含みを含むを .して..いる間ls -A、出力は隠しファイルが含まれ除く ...。でジルの答えのbash dotglob シェルオプションは隠しファイルを含めるように拡張を引き起こし除く ...
トーマスナイマン

回答:


61

簡潔な答え:

\ls -afq | wc -l

(これは、...、そう2を差し引きます)


ディレクトリ内のファイルを一覧表示すると、次の3つの一般的なことが起こります。

  1. ディレクトリ内のファイル名を列挙します。これは避けられません。ディレクトリ内のファイルを列挙せずに数える方法はありません。
  2. ファイル名の並べ替え。シェルワイルドカードとlsコマンドがそれを行います。
  3. statディレクトリであるかどうかなど、各ディレクトリエントリに関するメタデータを取得するために呼び出します。

#3は、ファイルごとにiノードをロードする必要があるため、群を抜いて最も高価です。これに対して、#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ため、ディレクトリにサブディレクトリがなく、ファイル名に改行が含まれていない場合、ディレクトリ内のファイルをカウントするポータブルで高速な方法です。

ディレクトリにサブディレクトリがなく、ファイル名に改行が含まれている可能性がある場合は、これらのいずれかを試してください(サポートされている場合は2番目の方が高速ですが、それほど顕著ではない場合があります)。

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

一方、findディレクトリにサブディレクトリがある場合は使用しないでください。すべてのエントリをfind . -maxdepth 1呼び出しますstat(少なくともGNU findおよびBusyBox findを使用)。ソート(#2)は避けますが、パフォーマンスを低下させるiノードルックアップ(#3)の代価を支払います。

外部ツールなしのシェルでは、を使用して現在のディレクトリ内のファイルをカウントできますset -- *; echo $#。これにより、ドットファイル(名前がで始まるファイル.)が欠落し、空のディレクトリで0ではなく1が報告されます。これは、外部プログラムを起動する必要がないため、小さなディレクトリ内のファイルをカウントする最速の方法ですが、ソート手順(#2)のために(zshを除き)大きなディレクトリの時間を無駄にします。

  • 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
100万を超えるファイルに対する私の経験的なテストでは、さらにチェックする必要がある宣言のようなものを追加しない限り、find -maxdepth 1簡単に対応できます。GNU findは実際に呼び出しますか?ファイルの詳細を返すように設定した場合、どれだけ遅くなるかと比較しても、速度は低下しません。一方、明確な速度の勝者は、非ソートグロブを使用しています。(ソートされたグロブは、ソートされていないグロブが2倍高速であるのに対し、2倍遅いです)。ファイルシステムの種類がこれらの結果に大きな影響を与えるのではないかと思います。\ls -U-typestatfind -typels -lzshls
カレブ

@カレブ私は走ったstrace。これは、ディレクトリにサブディレクトリがある場合にのみ当てはまります。それ以外findの場合、リーフディレクトリの最適化が有効になります(なしでも-maxdepth 1)。ファイルシステムのタイプ(statディレクトリをツリーとして表すファイルシステムよりもディレクトリを線形リストとして表すファイルシステムの方が呼び出しにかかるコストが高い)、iノードがすべて一緒に作成されて近くにあるかどうかなど、多くのことが結果に影響する可能性がありますディスク、コールドキャッシュまたはホットキャッシュなど
ジル「SO-悪であるのをやめる」

1
歴史的にls -fは、呼び出しを防ぐための信頼できる方法statでした-これは、今日では単に「出力がソートされていない」(これも原因となる)と説明され、andが含まれ.てい..ます。-Aそして-U標準オプションではありません。
Random832

1
あなたは、特にコマンドの中にそれを挿入する、一般的な拡張子(または他の文字列)を使用してファイルをカウントしたい場合はここで追加の2をなくし例である:\ls -afq *[0-9].pdb | wc -l
スティーブン・C.ハウエル

version sh (AT&T Research) 93u+ 2012-08-01参考までに、Debianベースのシステムでksh93を使用 してFIGNOREいると、うまくいかないようです。.そして..エントリが得られた配列に含まれている
Sergiy Kolodyazhnyy

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

私のマシンではかなり高速ですが、ローカル.ディレクトリがカウントに追加されます。


1
ありがとう。私は愚かな質問をせざるを得ない。なぜそれが速いのか?ファイルの属性を検索するのが面倒ではないからですか?
マイクB

2
はい、それは私の理解です。限り、あなたが使用していないとして-type、パラメータをfindより速くする必要がありますls
ジョエル・テイラー

1
うーん... 検索のドキュメントをよく理解しているなら、これは実際に私の答えよりも優れているはずです。経験のある人なら誰でも確認できますか?
ルイスマチュカ

-mindepth 1ディレクトリ自体を省略するには、a を追加します。
ステファンシャゼル

8

ls -1Uパイプがファイルエントリを並べ替えようとしないので、パイプがリソースを少し少なくする前に、ディスク上のフォルダで並べ替えられたとおりにそれらを読み取ります。また、出力も少なくなります。つまり、の作業がわずかに少なくなりwcます。

ls -f多かれ少なかれのショートカットを使用することもできls -1aUます。

しかし、パイプを使用せずにコマンドを使用してリソースを効率的に使用する方法があるかどうかはわかりません。


8
出力はパイプに行くときところで、-1暗示される
enzotib

@enzotib-そうですか?わあ...毎日何か新しいことを学ぶ!
ルイスマチュカ

6

別の比較ポイント。シェルのワンライナーではありませんが、このCプログラムは何もしません。ls|wc -l(出力のls -l|wc -l最初の行の合計ブロックにより、隠しファイルは1つずつオフになります)の出力と一致するため、無視されます。

#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()、いくつかのオーバーヘッドを追加しないstdioのAPIを、あなたは(基本となるシステムコールに渡されたバッファのサイズを制御できていないgetdentsLinux上)
ステファン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ベースの非ソートグロブと配列数)。つまり、並べ替えや無関係なファイルプロパティの読み取りなど、さまざまな非効率なオプションを打ち破ります。それはあなたにも何も得られないので、私は言いたいと思います、あなたがすでにperlにいるのでない限り、より簡単なソリューションを使用する価値はありません:)
Caleb

これにはカウントに.との..ディレクトリエントリが含まれるため、実際のファイル数(およびサブディレクトリ)を取得するには2を引く必要があります。現代の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の古いバージョンだけにそのような制限がありました(その構文をサポートしていませんでした)。他のほとんどのシェルでは、制限は使用可能なメモリだけです。特に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入力の1行を省略するために1組のパイプを追加することは、オーバーヘッドが入力サイズに関して直線的に増加するため、あまり効率的ではありません。この場合、一定の時間操作である1によるオフを補償するために、単純に最終カウントをデクリメントしないのはなぜですか?echo $(( $(find /foo/foo2 -maxdepth 1 | wc -l) - 1))
トーマスナイマン

1
大量のデータを別のプロセスに送るのではなく、最終的な出力に対していくつかの計算を行う方がよいでしょう。let count = $(find /foo/foo2 -maxdepth 1 | wc -l) - 2
カレブ

0

pythonのos.listdir()はあなたのために仕事をすることができます。特別な「。」を除く、ディレクトリのコンテンツの配列を提供します および「..」ファイル。また、名前に「\ n」などの特殊文字が含まれるabtファイルを心配する必要はありません。

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

以下は、「ls -Af」コマンドと比較した上記のpythonコマンドの所要時間です。

〜/ 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

カウントからサブディレクトリ除外するために、Gillesが受け入れた回答のバリエーションを次に示します。

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

外側の$(( ))算術展開$( )は、最初のサブシェルから2番目のサブシェルの出力を減算します$( )。1つ目$( )は、上から正確にジルズです。2番目$( )は、ターゲットに「リンク」しているディレクトリの数を出力します。これはls -odls -ld必要に応じて置換)、ハードリンクの数を一覧表示する列がディレクトリの特別な意味として持つ場合に由来します。「リンク」カウントが含まれ...および任意のサブディレクトリ。

パフォーマンスはテストしませんでしたが、似ているようです。ターゲットディレクトリの統計情報と、追加されたサブシェルとパイプのオーバーヘッドが追加されます。


弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.