【问题标题】:How to use Powershell Pipeline to Avoid Large Objects?如何使用 Powershell Pipeline 避免大对象?
【发布时间】:2017-07-11 15:27:18
【问题描述】:

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

我的第一次迭代是:

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

这导致了一个巨大的 $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

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

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

【问题讨论】:

  • 你使用ConvertTo-Csv | Out-File而不是Export-Csv有什么具体原因吗?
  • 不要收集所有对象然后处理。相反,你可以随时输出。
  • 可能是Get-FolderItem 闯入了中间。这是一项不错的工作,但它依赖于解析robocopy 输出。尝试在链接页面上使用AlphaFS(请参阅示例:模拟 Get-ChildItem 以克服“路径过长”)。

标签: powershell export-to-csv pipeline


【解决方案1】:

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

我们可以在 10 秒内将 400,000 个文件的信息写入 SSD 驱动器上的任何流水线和标准 PowerShell cmdlet 所能提供的任何东西之外,加快这一过程。

  1. .NET Framework 4 或更高版本(自 Win8 起包含,可在 Win7/XP 上安装)IO.DirectoryInfoEnumerateFileSystemInfos 以非阻塞管道式方式枚举文件;
  2. PowerShell 3 或更高版本,因为它总体上比 PS2 更快;
  3. foreach statement 不需要为每个项目创建 ScriptBlock 上下文,因此它比 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'

【讨论】:

  • 谢谢@wOxxOm。我会尝试重构并告诉你它是如何工作的!
【解决方案2】:

不要使用 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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-04-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-07-30
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多