如何使用Powershell Pipeline避免大型物体?

tre*_*lez 2 powershell pipeline export-to-csv

我正在使用自定义函数基本上在8TB驱动器(数千个文件)上执行DIR命令(递归文件列表).

我的第一次迭代是:

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

这导致了一个巨大的$ results变量,并通过强制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
Run Code Online (Sandbox Code Playgroud)

这似乎工作正常(CSV文件正在增长......并且CPU似乎稳定)但是当CSV文件大小达到~200MB时突然停止,并且控制台的错误是" 管道已经停止 ".

我不确定CSV文件大小与错误消息有什么关系,但我无法使用任何一种方法处理这个大型目录!有关如何允许此过程成功完成的任何建议?

wOx*_*xOm 5

Get-FolderItem运行robocopy以列出文件并将其输出转换为PSObject数组.这是一个缓慢的操作,严格来说,这对于实际任务不是必需的.与foreach 语句相比,流水线操作也增加了很大的开销.在数千或数十万次重复的情况下变得明显.

我们可以将流程加速到任何流水线之外,标准PowerShell cmdlet可以在10秒内在SSD驱动器上写入400,000个文件的信息.

  1. .NET Framework 4或更新版本(自Win8以来,可在Win7/XP上安装)IO.DirectoryInfoEnumerateFileSystemInfos,以非阻塞管道方式枚举文件;
  2. PowerShell 3或更新,因为它比PS2整体更快;
  3. foreach 不需要为每个项创建ScriptBlock上下文的语句,因此它比ForEachcmdlet 快得多
  4. IO.StreamWriter 立即以非阻塞管道方式写入每个文件的信息;
  5. \\?\前缀技巧解除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
}
Run Code Online (Sandbox Code Playgroud)

用法:

List-PathsInCsv 'c:\windows', 'd:\foo\bar' 'r:\output.csv'
Run Code Online (Sandbox Code Playgroud)