none
全てを処理しない RRS feed

  • 質問

  • お世話になります

    Get-ChildItem ディレクトリパス -Recurse | Where-Object {$_.LastWriteTime -lt (Get-Date).AddDays(-7)}

    これだとデレクトリ内すべてデータを舐めるから処理速度が遅いです

    デレクトリ配下にファイルずつ日付け比較見つかれば処理を実行すれば早くなると思ってます

    どなたかご教授ください

    2014年9月7日 13:31

すべての返信

  • そうではなく1ファイル毎に Get-Date や AddDayes を実行していることが問題ではありませんか?
    2014年9月7日 14:03
  • そうなんですか?

    どう修正すれば処理が速くなりますか?

    初心者でわかりません

    2014年9月8日 13:47
  • んーと、ディレクトリの中から最終更新日が一週間前よりこっちのを全部探すわけですよね。

    そうすると、結局ディレクトリすべてのファイルをチェックする必要がありますよね。

    一度に取ろうが順次取っていこうがパフォーマンスに大差はないと思いますよ?

    // Get-DateだのAddDaysだのはIOパフォーマンスに比べりゃ誤差でしょう。

    // 条件に合致するのが1つ見つかればいいだけなら、確かに最初に全部取るのは効率悪いですが。

    2014年9月9日 1:02
  • チャブーンです。

    Where-Objectはすべてのデータをメモリに読み込んで実行しますので、オンデマンドで実行するForeach-Objectを使ってみればいいと思います。ただしくふうがいります。

    $A=@()
    Get-ChildItem ディレクトリパス -Recurse | Sort-Object LastWriteTime | Foreach-Object {If ($_.LastWriteTime -lt (Get-Date).AddDays(-7)) {$A+=$_} Else {Return}}
    $A
    動作確認はしていないので、実際にどう動くかはご自身で確認してみてください。

    2014年9月9日 1:47
  • >チャブーンさん

    Sort-Objectを入れてしまうと、ソート処理がかかる分だけ計算量が増えてしまうので、特にファイル数が多くなると顕著に遅くなるかと思います。

    それにWhere-Objectコマンドレットは別にすべてのデータを読み込んでから実行しているわけではなく、ForEach-Objectと同じく、「オンデマンドに」(オブジェクトが入力されるたびに、という意味で)フィルタ処理をかけています。

    なので、PPP5963さんが提示されたコードそのものは特に大きな問題はないかと思います。

    Hongliangさんのおっしゃるとおり、日付計算はそれ程重い処理ではないですから、佐祐理さんがご指摘のように日付計算を毎回せず、

    $date=(Get-Date).AddDays(-7)
    Get-ChildItem ディレクトリパス -Recurse | Where-Object {$_.LastWriteTime -lt $date}

    のようにしてもそれ程は大きな変化はないでしょうね。


    2014年9月9日 4:13
    モデレータ
  • チャブーンです。

    牟田口さん、ご指摘ありがとうございます。不正確な部分はお詫びします。すみません。

    うえのスクリプトのキモ、なんですが、各要素全部をなめるかたちで処理するのではなく、Sortで先頭部分に集めてから IF 文で条件分岐させる (LastWriteTime条件を満たさなければ、その後の要素はすべて満たさない方に該当する) ことで、処理の短縮をおこなっていることです。

    もしSortを行った時点で全部なめることとおなじような処理が発生する、ということでしたら、私の理解不足によるミスリードですから、申し訳なく思います。


    sort処理を含まないケース(たとえばセキュリティログから特定範囲の日時のデータだけを抽出)であれば、実際に効果があることは確認しています。
    2014年9月9日 4:36
  • ソート処理は、どれだけ効率のよいソートアルゴリズムでも、配列を1回ループするだけでは完了しません。計算量としてはO(n)を必ず超える処理になります。(Sort-Objectコマンドレットが内部的に使用しているアルゴリズムは多分クイックソートだと思うんですが、その場合は平均O(n log n)です)

    しかしながら、今回のお題のように、配列要素をプロパティ値によって振り分ける処理は、配列を1回ループするだけで必ず完了するので、計算量はO(n)です。

    配列|Where-Objectの処理は、1回ループと計算量的には等価です。

    したがって、ソート処理は、「配列を全部なめる」どころかそれ以上に時間がかかる処理であり、特に要素数が増えるとそれに比例するどころか、それ以上の勢いで処理がどんどん遅くなるので、今回のように要素数が多く、処理速度が重要であるお題では不適と考えられる、ということになります。これで説明になっていますでしょうか?

    2014年9月9日 5:12
    モデレータ
  • チャブーンです。

    なるほど。牟田口さんの説明で十分理解できました。自分の脳内が思考停止状態だったようです。ミスリードになってしまい、すみませんでした。

    セキュリティログの件は「書き込み日時順に最初からソートされている」から使える話ということですね。了解です。

    2014年9月9日 5:35
  • 260文字以上のパスの考慮は必要無いのかな?
    2014年9月9日 7:58
  • 260文字以上のパス考慮は必要ありません

    2014年9月9日 13:09
  • >チャブーンさんへ

    >うえのスクリプトのキモ、なんですが、各要素全部をなめるかたちで処理するのではなく、Sortで先頭部分に集めてから IF 文で条件分岐させる (LastWriteTime条件を満たさなければ、その後の要素はすべて満たさない方に該当する) ことで、処理の短縮をおこなっていることです。

    少し脱線しますが

    ディレクトリパスに1000ファイルあるとして、1ファイルづつファイルスタンプを指定日と比較し、

    たとえば150ファイル目で条件を満たすファイルがあれば処理を抜け、対象ファイル情報をログとして出力。

    すれば自分の中で解決も有りです

    2014年9月9日 14:35
  • 「自分の中で解決も有り」というのは、本件とは無関係だが知識として知っておきたい、ということでしょうか?

    とりあえず、System.Linq.Enumerableを使う方法は思いつきますね。(古いPowerShellだとFuncへのキャスト構文が使えないかも?)

    $files = (New-Object IO.DirectoryInfo($dirPath)).EnumerateFiles()
    $first = [Linq.Enumerable]::FirstOrDefault($files, [Func[IO.FileInfo, bool]]{
        param($file) $file.LastWriteTime -ge (Get-Date).AddDays(-7)
    })
    if ($first) {
        echo $first.LastWriteTime
    }
    <追記>ちょっと調べたところ、Enumerable::FirstOrDefaultを直接呼び出せるのは結構新しいPowerShellのみみたいですね。それ以前はType::GetMethodsからMethodInfo探してMakeGenericMethodしてInvokeする必要があるとか。</追記>
    • 編集済み Hongliang 2014年9月10日 7:54
    2014年9月10日 7:25
  • 条件を満たすファイルが見つかったらディレクトリ検索処理を抜け、見つかったファイルを出力する、ということであれば、以下のような書き方でも可能かと思います。

    $date=(Get-Date).AddDays(-7)
    Get-ChildItem C:\windows -Recurse | where {$_.LastWriteTime -lt $date} | foreach {$_;break}
    2014年9月10日 10:40
    モデレータ
  • パイプラインで受け渡してからforeach?

    最初にGet-ChildItemがあるので、
    全てのアイテムを格納してます?

    2014年9月10日 15:11
  • PowerShellにおけるパイプライン処理は、一度にすべてのオブジェクトを受け取ってから後続コマンドに渡すのではなく、先行コマンドにおける出力を一つずつ受け取って、後続コマンドに逐一入力していくという動きをします。

    (Sort-ObjectとかMeasure-Object等の、コマンドの性格上、全てのオブジェクトを受け取ってからでないと結果を出力できないコマンドを除く)

    そのため、私が提示したコマンドは、Get-ChildItemコマンドレットでファイルを列挙するたび、どんどん後続のWhere-Objectでフィルタを通り、フィルタを最初に抜けたものがForEach-Objectコマンドレットに渡されます。

    ForEach-Objectでは入力をそのまま出力し($_)、出力すると同時にパイプライン全体をbreakし、処理を終了します。よってGet-ChildItemコマンドレットによるファイル列挙はその時点で停止します。

    2014年9月10日 18:59
    モデレータ
  • Sort-Objectの部分だけど、1つづつ受け渡したところで、

    全てのファイルを読み込まない限りソート出来ないから、

    読み込んだ後じゃないと次のパイプラインに渡せないんじゃないの?

    2014年9月11日 14:45
  • 仰る通りです。

    なので、

    >(Sort-ObjectとかMeasure-Object等の、コマンドの性格上、全てのオブジェクトを受け取ってからでないと結果を出力できないコマンドを除く)

    と書きました。

    2014年9月12日 2:29
    モデレータ
  • 横からすいません。

    そもそも結果として

    1.「条件を満たすファイルの一覧」 を出力したいのか、

    2.「ひとつでも条件を満たすファイルが存在するか」 を確認したいのか

    のどちらなんでしょう。それによって変わってくると思いますが。

    2.であれば牟田口さんので満たすでしょうけれども、1,の場合はループを抜けてはだめですよね。

    2014年9月12日 9:51
  • すみません、Why is Get-ChildItem so Slow?なんて記事もありそもそもGet-ChildItemが遅いんですね。知りませんでした。

    .NET Frameworkにも同様の問題があり、.NET 4未満のDirectoryInfo.GetFiles()は配列で値を返すという問題の他に今回使用しているようなLastWriteTimeをセットしないため、1ファイル毎に再度ファイル情報を取得する必要がありました。.NET 4以降では改善されいくつかのプロパティがセットされた状態で取得できるようになっています。

    PowerShellが当該APIを使用しているかまでは確認していませんが、もしかすると.NET 4以降を使用するPowerShell 3.0以降であればパフォーマンス改善しているかもしれませんね。

    2014年9月13日 2:07