【问题标题】:Speed of Powershell Script. Optimisation soughtPowershell 脚本的速度。寻求优化
【发布时间】:2017-02-10 05:00:15
【问题描述】:

我有一个工作脚本,其目标是在导入 Oracle 之前解析数据文件中的格式错误的行。要处理超过 100 万行、8 列的 450MB csv 文件,需要花费 2.5 小时多一点的时间,并且会最大化单个 CPU 内核。小文件快速完成(几秒钟)。

奇怪的是,具有相似行数和 40 列的 350MB 文件只需要 30 分钟。

我的问题是文件会随着时间的推移而增长,而 2.5 小时占用 CPU 并不好。谁能推荐代码优化?类似标题的帖子推荐了本地路径 - 我已经在这样做了。

$file = "\Your.csv"

$path = "C:\Folder"

$csv  = Get-Content "$path$file"

# Count number of file headers
$count = ($csv[0] -split ',').count

# https://blogs.technet.microsoft.com/gbordier/2009/05/05/powershell-and-writing-files-how-fast-can-you-write-to-a-file/
$stream1 = [System.IO.StreamWriter] "$path\Passed$file-Pass.txt"
$stream2 = [System.IO.StreamWriter] "$path\Failed$file-Fail.txt"

# 2 validation steps: (1) count number of headers is ge (2) Row split after first col.  Those right hand side cols must total at least 40 characters.
$csv | Select -Skip 1 | % {
  if( ($_ -split ',').count -ge $count -And ($_.split(',',2)[1]).Length -ge 40) {
     $stream1.WriteLine($_)
  } else {
     $stream2.WriteLine($_) 
  }
}
$stream1.close()
$stream2.close()

示例数据文件:

C1,C2,C3,C4,C5,C6,C7,C8 ABC,000000000000006732,1063,2016-02-20,0,P,估计,2015473497A10 ABC,000000000000006732,1110,2016-06-22,0,P,估计,2015473497A10 ABC,,2016-06-22,,201501 ,,,,,,,, ABC,000000000000006732,1135,2016-08-28,0,P,估计,2015473497B10 ABC,000000000000006732,1167,2015-12-20,0,P,估计,2015473497B10

【问题讨论】:

  • 输入文件的几行示例怎么样?以及你想要做什么的描述?
  • 获取内容超慢。使用 IO.StreamReader。还要为输出使用不同的硬盘驱动器或在 StreamWriter 构造函数中指定一个大的写入缓冲区。
  • 现在这就是我所希望的那种建议!好人,干杯。
  • 您能否在代码的每个部分周围放置一些 Measure-Command { } 块以查看延迟在哪里?即:是加载,文件写入等。
  • 我曾想过尝试那个 Simon,但我认为它必须是 If 语句。读取文件需要一段时间,但观看资源监视器我可以看到光盘写入在片刻后开始。我没有考虑过它可能是一边阅读一边写作。我会坚持一个措施...不能伤害!干杯。

标签: performance powershell csv if-statement


【解决方案1】:
  • 在所有 PowerShell 版本(包括 5.1)上,当文件包含数百万行时,Get-Content 在生成数组的默认模式下非常慢。更糟糕的是,您将它分配给一个变量,因此在整个文件被读取并分成几行之前,不会发生其他任何事情。在 3.9GHz $csv = Get-Content $path 的 Intel i7 3770K CPU 上读取 800 万行的 350MB 文件需要超过 2 分钟。

    解决方案:使用IO.StreamReader 读取一行并立即处理。
    在 PowerShell2 中,StreamReader 的优化不如 PS3+,但仍比 Get-Content 快。


  • 通过| 进行流水线化至少比通过whileforeach 语句(不是cmdlet)等流控制语句直接枚举慢几倍。
    解决方案:使用语句。

  • 将每一行拆分成一个字符串数组比只处理一个字符串要慢。
    解决方法:使用IndexOfReplace方法(不是运算符)来统计字符出现次数。

  • PowerShell 总是在使用循环时创建内部管道。
    解决方案:在这种情况下,使用 Invoke-Command { } trick 可实现 2-3 倍的加速!

以下是兼容 PS2 的代码。
它在 PS3+ 中更快(在我的 PC 上,350MB csv 中的 800 万行需要 30 秒)。

$reader = New-Object IO.StreamReader ('r:\data.csv', [Text.Encoding]::UTF8, $true, 4MB)
$header = $reader.ReadLine()
$numCol = $header.Split(',').count

$writer1 = New-Object IO.StreamWriter ('r:\1.csv', $false, [Text.Encoding]::UTF8, 4MB)
$writer2 = New-Object IO.StreamWriter ('r:\2.csv', $false, [Text.Encoding]::UTF8, 4MB)
$writer1.WriteLine($header)
$writer2.WriteLine($header)

Write-Progress 'Filtering...' -status ' '
$watch = [Diagnostics.Stopwatch]::StartNew()
$currLine = 0

Invoke-Command { # the speed-up trick: disables internal pipeline
while (!$reader.EndOfStream) {
    $s = $reader.ReadLine()
    $slen = $s.length
    if ($slen-$s.IndexOf(',')-1 -ge 40 -and $slen-$s.Replace(',','').length+1 -eq $numCol){
        $writer1.WriteLine($s)
    } else {
        $writer2.WriteLine($s)
    }
    if (++$currLine % 10000 -eq 0) {
        $pctDone = $reader.BaseStream.Position / $reader.BaseStream.Length
        Write-Progress 'Filtering...' -status "Line: $currLine" `
            -PercentComplete ($pctDone * 100) `
            -SecondsRemaining ($watch.ElapsedMilliseconds * (1/$pctDone - 1) / 1000)
    }
}
} #Invoke-Command end

Write-Progress 'Filtering...' -Completed -status ' '
echo "Elapsed $($watch.Elapsed)"

$reader.close()
$writer1.close()
$writer2.close()

另一种方法是在两遍中使用正则表达式(虽然它比上面的代码慢)。
由于数组元素属性简写语法,需要 PowerShell 3 或更高版本:

$text = [IO.File]::ReadAllText('r:\data.csv')
$header = $text.substring(0, $text.indexOfAny("`r`n"))
$numCol = $header.split(',').count

$rx = [regex]"\r?\n(?:[^,]*,){$($numCol-1)}[^,]*?(?=\r?\n|$)"
[IO.File]::WriteAllText('r:\1.csv', $header + "`r`n" +
                                    ($rx.matches($text).groups.value -join "`r`n"))
[IO.File]::WriteAllText('r:\2.csv', $header + "`r`n" + $rx.replace($text, ''))

【讨论】:

  • 优秀的答案@wOxxOm ...清晰简洁地解释了为什么使用的方法很慢以及改用什么。运行时间从几个小时缩短到几分钟。
  • @felixmc,由于 Invoke-Command,速度又提高了 2 倍!查看更新的代码。
  • “在 PowerShell2 中,StreamReader 的优化不如 PS3+,但仍比 Get-Content 快”——这是怎么回事? StreamReader 不是来自 .NET 而不是 PowerShell?我很好奇,因为我以为我是从 PowerShell 调用本机 .NET 类,而不是我实际上可能在做的其他一些奇怪的构造???
【解决方案2】:

如果您想安装 awk,您可以在一秒钟内完成 1,000,000 条记录 - 对我来说似乎是一个很好的优化 :-)

awk -F, '
   NR==1                    {f=NF; printf("Expecting: %d fields\n",f)}  # First record, get expected number of fields
   NF!=f                    {print > "Fail.txt"; next}                  # Fail for wrong field count
   length($0)-length($1)<40 {print > "Fail.txt"; next}                  # Fail for wrong length
                            {print > "Pass.txt"}                        # Pass
   ' MillionRecord.csv

您可以从here 获取适用于 Windows 的 gawk

Windows 在参数中使用单引号有点尴尬,所以如果在 Windows 下运行,我会使用相同的代码,但格式如下:

将其保存在名为 commands.awk 的文件中:

NR==1                    {f=NF; printf("Expecting: %d fields\n",f)}
NF!=f                    {print > "Fail.txt"; next}
length($0)-length($1)<40 {print > "Fail.txt"; next}
                         {print > "Pass.txt"}

然后运行:

awk -F, -f commands.awk Your.csv

这个答案的其余部分与 cmets 部分中提到的 “Beat hadoop with the shell” 挑战有关,我想在某个地方保存我的代码,所以它在这里....运行6.002 秒在我的 iMac 上超过 3.5GB 的 1543 个文件,总计约 1.04 亿条记录:

#!/bin/bash
doit(){
   awk '!/^\[Result/{next} /1-0/{w++;next} /0-1/{b++} END{print w,b}' $@
}

export -f doit
find . -name \*.pgn -print0 | parallel -0 -n 4 -j 12 doit {}

【讨论】:

  • 一个很好的答案,但不幸的是不是一个适用的解决方案。将牢记以备将来参考。干杯。
  • 当 awk 可以做得非常好和更快时,为什么还要使用它?比如看看这个线程foxdeploy.com/2016/03/01/…
  • @4c74356b41 OP 要求进行优化,我认为 10,000 倍的加速是一个非常好的优化。有时最好的优化涉及不同的算法或不同的工具。如果正如 OP 所指出的,他希望坚持使用 Powershell,那么欢迎他忽略我的建议。
  • 我要去看看我是否可以实现 gawk。我保持开放状态,看看是否提供了特定于 Powershell 的解决方案。我完全同意你的“不同工具”的说法,但在这种情况下,我必须考虑支持能力。
  • @4c74356b41 有趣的挑战 - 我玩了一点游戏,使用awk 在 6.002 秒内轻松击败他们的最佳时间 8.7 秒。我不知道我的系统和他们的系统有多大的可比性,但我的只是一个桌面 iMac,没什么特别的。
【解决方案3】:

尝试尝试不同的循环策略,例如,切换到 for 循环可将处理时间缩短 50% 以上,例如:

[String]                 $Local:file           = 'Your.csv';
[String]                 $Local:path           = 'C:\temp';
[System.Array]           $Local:csv            = $null;
[System.IO.StreamWriter] $Local:objPassStream  = $null;
[System.IO.StreamWriter] $Local:objFailStream  = $null; 
[Int32]                  $Local:intHeaderCount = 0;
[Int32]                  $Local:intRow         = 0;
[String]                 $Local:strRow         = '';
[TimeSpan]               $Local:objMeasure     = 0;

try {
    # Load.
    $objMeasure = Measure-Command {
        $csv = Get-Content -LiteralPath (Join-Path -Path $path -ChildPath $file) -ErrorAction Stop;
        $intHeaderCount = ($csv[0] -split ',').count;
        } #measure-command
    'Load took {0}ms' -f $objMeasure.TotalMilliseconds;

    # Create stream writers.
    try {
        $objPassStream = New-Object -TypeName System.IO.StreamWriter ( '{0}\Passed{1}-pass.txt' -f $path, $file );
        $objFailStream = New-Object -TypeName System.IO.StreamWriter ( '{0}\Failed{1}-fail.txt' -f $path, $file );

        # Process CSV (v1).
        $objMeasure = Measure-Command {
            $csv | Select-Object -Skip 1 | Foreach-Object { 
                if( (($_ -Split ',').Count -ge $intHeaderCount) -And (($_.Split(',',2)[1]).Length -ge 40) ) {
                    $objPassStream.WriteLine( $_ );   
                } else {
                    $objFailStream.WriteLine( $_ );
                } #else-if
                } #foreach-object
            } #measure-command
        'Process took {0}ms' -f $objMeasure.TotalMilliseconds;

        # Process CSV (v2).
        $objMeasure = Measure-Command {
            for ( $intRow = 1; $intRow -lt $csv.Count; $intRow++ ) {
                if( (($csv[$intRow] -Split ',').Count -ge $intHeaderCount) -And (($csv[$intRow].Split(',',2)[1]).Length -ge 40) ) {
                    $objPassStream.WriteLine( $csv[$intRow] );   
                } else {
                    $objFailStream.WriteLine( $csv[$intRow] );
                } #else-if
                } #for
            } #measure-command
        'Process took {0}ms' -f $objMeasure.TotalMilliseconds;

        } #try
    catch [System.Exception] {
        'ERROR : Failed to create stream writers; exception was "{0}"' -f $_.Exception.Message;
         } #catch
    finally {
        $objFailStream.close();
        $objPassStream.close();    
        } #finally

   } #try
catch [System.Exception] {
    'ERROR : Failed to load CSV.';
    } #catch

exit 0;

【讨论】:

  • 一个非常好的答案@Simon Catlin - 很棒的模板,我可以看到我回来并使用它进行测试。第二块减少50%的时间。太棒了!
猜你喜欢
  • 2020-04-12
  • 1970-01-01
  • 1970-01-01
  • 2011-08-01
  • 2016-07-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多