2017-02-21 43 views
2

我正在使用自定義函數在8TB驅動器(數千個文件)上實質上執行DIR命令(遞歸文件列表)。如何使用Powershell管道避免大型對象?

我的第一次迭代是:

$results = $PATHS | % {Get-FolderItem -Path "$($_)" } | Select Name,DirectoryName,Length,LastWriteTime 
$results | Export-CVS -Path $csvfile -Force -Encoding UTF8 -NoTypeInformation -Delimiter "|" 

這導致了巨大的$結果變量,減緩了系統崩潰到爬行通過扣球PowerShell的過程中使用99%-100%的CPU爲處理繼續進行。

我決定使用管道的力量來寫,直接CSV文件(可能釋放內存),而不是保存到一箇中間變量,以及與此想出了:

$PATHS | % {Get-FolderItem -Path "$($_)" } | Select Name,DirectoryName,Length,LastWriteTime | ConvertTo-CSV -NoTypeInformation -Delimiter "|" | Out-File -FilePath $csvfile -Force -Encoding UTF8 

這似乎工作正常(CSV文件正在增長......並且CPU看起來很穩定),但當CSV文件大小達到〜200MB時突然停止,並且控制檯的錯誤是「管道已停止」。

我不確定CSV文件的大小與錯誤消息有什麼關係,但我無法用任何一種方法處理這個大目錄!有關如何讓此過程成功完成的任何建議?

+2

是否有您使用'ConvertTo-Csv | Out-File'而不是'Export-Csv'? – briantist

+1

不要收集所有對象,然後處理。相反,輸出你走。 –

+1

可能是[Get-FolderItem](https://gallery.technet.microsoft.com/scriptcenter/Get-Deeply-Nested-Files-a2148fd7)在中間。這是一件很好的工作,但它依賴於解析'robocopy'輸出。嘗試使用[AlphaFS](https://github.com/alphaleonis/AlphaFS/wiki/PowerShell)(請參閱*示例:模擬Get-ChildItem以克服鏈接頁面上的「Path Too Long」*)。 – beatcracker

回答

5

Get-FolderItem運行robocopy列出文件並將其輸出轉換爲PSObject數組。這是一個緩慢的操作,嚴格來說,這對於實際任務並不需要。與foreach 陳述相比,流水線操作還增加了大量開銷。在數千或數十萬次重複的情況下,這些重複會變得明顯。

我們可以加快流程,超越任何流水線操作,標準PowerShell cmdlet可以提供在10秒鐘內爲SSD驅動器寫入400,000個文件的信息。

  1. .NET框架4或更新(包括自Win8的,可安裝上的Win7/XP)IO.DirectoryInfoEnumerateFileSystemInfos枚舉非阻塞管道狀的方式的文件;
  2. PowerShell 3或更新,因爲它總體上比PS2更快;
  3. foreach聲明這並不需要創建腳本塊背景下的每個項目因此它比ForEach cmdlet的
  4. IO.StreamWriter快得多立即寫入每個文件的信息在非阻塞管道狀的形式;
  5. \\?\ prefix trick解除260個字符的路徑長度限制;
  6. 手動排隊要處理的目錄以便通過「訪問被拒絕」錯誤,否則將會停止原始IO.DirectoryInfo枚舉;
  7. 進度報告。

function List-PathsInCsv([string[]]$PATHS, [string]$destination) { 
    $prefix = '\\?\' #' UNC prefix lifts 260 character path length restriction 
    $writer = [IO.StreamWriter]::new($destination, $false, [Text.Encoding]::UTF8, 1MB) 
    $writer.WriteLine('Name|Directory|Length|LastWriteTime') 
    $queue = [Collections.Generic.Queue[string]]($PATHS -replace '^', $prefix) 
    $numFiles = 0 

    while ($queue.Count) { 
     $dirInfo = [IO.DirectoryInfo]$queue.Dequeue() 
     try { 
      $dirEnumerator = $dirInfo.EnumerateFileSystemInfos() 
     } catch { 
      Write-Warning ("$_".replace($prefix, '') -replace '^.+?: "(.+?)"$', '$1') 
      continue 
     } 
     $dirName = $dirInfo.FullName.replace($prefix, '') 

     foreach ($entry in $dirEnumerator) { 
      if ($entry -is [IO.FileInfo]) { 
       $writer.WriteLine([string]::Join('|', @(
        $entry.Name 
        $dirName 
        $entry.Length 
        $entry.LastWriteTime 
       ))) 
      } else { 
       $queue.Enqueue($entry.FullName) 
      } 
      if (++$numFiles % 1000 -eq 0) { 
       Write-Progress -activity Digging -status "$numFiles files, $dirName" 
      } 
     } 
    } 
    $writer.Close() 
    Write-Progress -activity Digging -Completed 
} 

用法:

List-PathsInCsv 'c:\windows', 'd:\foo\bar' 'r:\output.csv' 
+0

謝謝@wOxxOm。我會嘗試重構,讓你知道它是如何工作的! – tresstylez

1

不使用ROBOCOPY,使用本地PowerShell命令,像這樣:

$PATHS = 'c:\temp', 'c:\temp2' 
$csvfile='c:\temp\listresult.csv' 

$PATHS | % {Get-ChildItem $_ -file -recurse } | Select Name,DirectoryName,Length,LastWriteTime | export-csv $csvfile -Delimiter '|' -Encoding UTF8 -NoType 

短版沒有純粹:

$PATHS | % {gci $_ -file -rec } | Select Name,DirectoryName,Length,LastWriteTime | epcsv $csvfile -D '|' -E UTF8 -NoT