PowerShell使用基本操作的Get-Content速度非常慢

4

我正在合并许多大型CSV文件,例如在跳过前导垃圾并将文件名附加到每一行时:

Get-ChildItem . | Where Name -match "Q[0-4]20[0-1][0-9].csv" | 
Foreach-Object {
    $file = $_.BaseName
    Get-Content $_.FullName | select-object -skip 3 | % {
        "$_,${file}" | Out-File -Append temp.csv -Encoding ASCII
    }
}

在PowerShell中,即使在i7 / 16GB的机器上,这个过程非常缓慢(每分钟约5兆字节)。我应该如何提高效率或者考虑换用Python?

4
我认为你在这里的瓶颈是使用了Out-File -Append,也就是为了CSV中的每一行都写入磁盘。更好的方法是将所有文件加载到内存中,进行必要的操作,然后一次性输出合并后的CSV。 - G42
啊,谢谢。Add-Content 可以做到这个吗?我试一下。 - P.Windridge
Add-Content对速度没有影响,但是将附加文件名从每行中删除使其更快,因此我认为这是瓶颈(但您关于在RAM中执行所有操作的评论肯定是正确的..不幸的是,我可能无法最终做到这一点)。 - P.Windridge
2
如果你的文件很大,使用 Streamreader 会获得更好的性能提升。Get-Content 在处理较大的文件时会变得很慢。 - Matt
3个回答

4

Get-Content / Set-Content 在处理大文件时效率很低。当需要关注性能时,流是一个很好的替代方案。因此,在这种情况下,我们可以使用一个流来读取每个文件,并使用另一个流来输出结果。

$rootPath = "C:\temp"
$outputPath = "C:\test\somewherenotintemp.csv"
$streamWriter = [System.IO.StreamWriter]$outputPath
Get-ChildItem $rootPath -Filter "*.csv" -File  | ForEach-Object{
    $file = $_.BaseName
    [System.IO.File]::ReadAllLines($_.FullName) | 
        Select-Object -Skip 3 | ForEach-Object{
            $streamWriter.WriteLine(('{0},"{1}"' -f $_,$file))
    }
}
$streamWriter.Close(); $streamWriter.Dispose()

创建一个写入流$streamWriter以输出每个文件中编辑过的行。我们可以批量读取和写入文件,这样会更快,但由于需要忽略一些行并对每行进行修改,逐行处理会更简单。在此期间避免将任何内容写入控制台,因为这只会减慢速度。 '{0},"{1}"' -f $_,$file的作用是引用最后一个“列”,以防basename包含空格。

不错;值得注意的两件事:由于.NET通常看到一个_不同的_工作目录,应始终使用_完整路径_(这就是你正在做的)。您不需要_同时使用_.Close().Dispose() - 任何一个都可以(.Close()调用.Dispose(),它会完成所有工作)。 - mklement0

2
Measure-Command -Expression {
    Get-ChildItem C:\temp | Where Name -like "*.csv" | ForEach-Object {
        $file = $_.BaseName
        Get-Content $_.FullName | select-object -Skip 3 | ForEach-Object {
            "$_,$($file)" | Out-File -Append C:\temp\t\tempe1.csv -Encoding ASCII -Force
        }
    }
} # TotalSeconds      : 12,0526802 for 11415 lines

如果你首先将所有内容放入内存中的数组中,速度会更快:
Measure-Command -Expression {
    $arr = @()
    Get-ChildItem C:\temp | Where Name -like "*.csv" | ForEach-Object {
        $file = $_.BaseName
        $arr += Get-Content $_.FullName | select-object -Skip 3 | ForEach-Object {
            "$_,$($file)" 
        }
    }
    $arr | Out-File -Append C:\temp\t\tempe2.csv -Encoding ASCII -Force
} # TotalSeconds      :  0,8197193  for 11415 lines

编辑:已修复,现在每行都添加了您的文件名。


谢谢,我稍后会检查。即使我的测试用例有150万行,所以如果需要在内存中做更多的操作,我可能需要一些额外的分批处理。 - P.Windridge
6
不要使用 $arr += ...,因为在这个设置中它会极大地影响性能。 你已经在使用管道。 只需完全删除该逻辑,并立即将其发送到 out-file/set-content - Matt
为什么应该避免使用增量赋值运算符(+=)来创建集合? - iRon

1
为了避免-Append破坏脚本的性能,您可以使用缓冲数组变量:
# Initialize buffer
$csvBuffer = @()

Get-ChildItem *.csv | Foreach-Object {
    $file = $_.BaseName
    $content = Get-Content $_.FullName | Select-Object -Skip 3 | %{
        "$_,${file}" 
    }

    # Populate buffer
    $csvBuffer += $content

    # Write buffer to disk if it contains 5000 lines or more
    $csvBufferCount = $csvBuffer | Measure-Object | Select-Object -ExpandProperty Count
    if( $csvBufferCount -ge 5000 )
    {
        $csvBuffer | Out-File -Path temp.csv -Encoding ASCII -Append
        $csvBuffer = @()
    }
}

# Important : empty the buffer remainder
if( $csvBufferCount -gt 0 )
{
    $csvBuffer | Out-File -Path temp.csv -Encoding ASCII -Append
    $csvBuffer = @()
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接