performance - 电源外壳。修改 ascii 文本文件字符串,其中行号字符串已打开。交换机和 .NET 框架或 cmdlet 和管道?哪个更快?

标签 performance powershell replace file-io powershell-5.0

如何使用易于阅读且易于使用 PowerShell 5 添加/修改/删除的搜索字符串来修改 Windows ascii 文本文件中的字符串(LINE2“行号 LINE2 is on”)。此脚本将解析 2500 line 文件中,找到 139 个字符串实例,替换它们并覆盖原始字符串,平均耗时不到 165 毫秒,具体取决于您使用的方法。哪种方法更快?哪种方法更容易添加/修改/删除字符串?

搜索字符串“AROUND LINE {1-9999}”和“LINE2 {1-9999}”,并将 {1-9999} 替换为代码所在的{行号}。测试是使用 2500 行文件而不是两行 example.bat 完成的。

sample.bat 包含两行:

ECHO AROUND LINE 5936
TITLE %TIME%   DISPLAY TCP-IP SETTINGS   LINE2 5937

方法一:使用 Get-Content + -replace + Set-Content:

Measure-command {
copy-item $env:temp\sample9.bat -d $env:temp\sample.bat -force
(gc $env:temp\sample.bat) | foreach -Begin {$lc = 1} -Process {
  $_ -replace 'AROUND LINE \d+', "AROUND LINE $lc" -replace 'LINE2 \d+', "LINE2 $lc"
  ++$lc
} | sc -Encoding Ascii $env:temp\sample.bat}

结果:十次运行 175ms-387ms,平均 215ms。

您可以通过添加/删除/修改 -replace 来修改搜索。

-替换 'AROUND LINE\d+'、"AROUND LINE $lc"-替换 'LINE2\d+'、"LINE2 $lc"-替换 'PLACEMARK\d+'、"PLACEMARK $lc"

powershell $env:temp\sample.ps1 $env:temp\sample.bat:

(gc $args[0]) | foreach -Begin {$lc = 1} -Process { $_ -替换 'AROUND LINE\d+', "AROUND LINE $lc"-替换 'LINE2\d+', "LINE2 $lc" ++$lc } | sc -编码 Ascii $args[0]

方法二:使用 switch 和 .NET 框架:

Measure-command {
    copy-item $env:temp\sample9.bat -d $env:temp\sample.bat -force
    $file = "$env:temp\sample.bat"
    $lc = 0
    $updatedLines = switch -Regex ([IO.File]::ReadAllLines($file)) {
      '^(.*? (?:AROUND LINE|LINE2) )\d+(.*)$' { $Matches[1] + ++$lc + $Matches[2] }
      default { ++$lc; $_ }
    }
    [IO.File]::WriteAllLines($file, $updatedLines, [Text.Encoding]::ASCII)}

结果:十次运行 73ms-816ms,平均 175ms。

方法三:使用基于预编译正则表达式的 switch 和 .NET 框架优化版本:

Measure-command {
copy-item $env:temp\sample9.bat -d $env:temp\sample.bat -force
$file = "$env:temp\sample.bat"
$regex = [Regex]::new('^(.*? (?:AROUND LINE|LINE2) )\d+(.*)$', 'Compiled, IgnoreCase, CultureInvariant')
$lc = 0
$updatedLines = & {foreach ($line in [IO.File]::ReadLines($file)) {
    $lc++
    $m = $regex.Match($line)
    if ($m.Success) {
        $g = $m.Groups
        $g[1].Value + $lc + $g[2].Value
    } else { $line }
}}
[IO.File]::WriteAllLines($file, $updatedLines, [Text.Encoding]::ASCII)}

结果:十次运行 71ms-236ms,平均 106ms。

添加/修改/删除您的搜索字符串:

AROUND LINE|LINE2|PLACEMARK
AROUND LINE|LINE3
LINE4

powershell $env:temp\sample.ps1 $env:temp\sample.bat:

$file=$args[0]
$regex = [Regex]::new('^(.*? (?:AROUND LINE|LINE2) )\d+(.*)$', 'Compiled, IgnoreCase, CultureInvariant')
    $lc = 0
    $updatedLines = & {foreach ($line in [IO.File]::ReadLines($file
)) {
        $lc++
        $m = $regex.Match($line)
        if ($m.Success) {
            $g = $m.Groups
            $g[1].Value + $lc + $g[2].Value
        } else { $line }
    }}
    [IO.File]::WriteAllLines($file
, $updatedLines, [Text.Encoding]::ASCII)

编者注:这是 Iterate a backed up ascii text file, find all instances of {LINE2 1-9999} replace with {LINE2 "line number the code is on"}. Overwrite. Faster? 的后续问题

这个问题从最年轻到最老的演变: 1.54757890 2.54737787 3.54712715 4.54682186

更新:我使用了 @mklement0 正则表达式解决方案。

最佳答案

switch -Regex -File $file {
  '^(.*? (?:AROUND LINE|LINE2) )\d+(.*)$' { $Matches[1] + ++$lc + $Matches[2] }
  default { ++$lc; $_ }
}
  • 鉴于正则表达式 ^(.*? (?:AROUND LINE|LINE2) )\d+(.*)$仅包含 2 个捕获组 - 要替换​​的数字 ( \d+ ) 之前 的行部分以及之后 的行部分,您必须使用索引 1 引用这些组和2进入automatic $Matches variable在输出中(不是 23 )。

    • 请注意(?:...)是一个非捕获组,因此根据设计,它不会反射(reflect)在 $Matches 中.
  • 而不是使用 [IO.File]::ReadAllLines($file) 读取文件,我正在使用-File选项 switch ,它直接从文件 $file 读取行.

  • ++$lc里面default { ++$lc; $_ }确保在将手边的行传递给 ( $_ ) 之前,对于不匹配行,行计数器也会递增。


性能说明

  • 您可以使用以下 obscure optimization 稍微提高性能:

    # Enclose the switch statement in & { ... } to speed it up slightly.
    $updatedLines = & { switch -Regex -File ... }
    
  • 迭代次数较多(大量行),使用预编译的 [regex]实例而不是 PowerShell 在幕后转换为正则表达式的字符串文字可以进一步加快速度 - 请参阅下面的基准测试。

  • 此外,如果区分大小写的匹配就足够了,您可以通过添加 -CaseSensitive 来提高性能一点 switch 的选项声明。

  • 在较高的层面上,使解决方案变得快速的是使用 switch -File处理行,并且通常使用 .NET 类型进行文件 I/O(而不是 cmdlet)(在本例中为 IO.File]::WriteAllLines(),如问题所示) - 另见this related answer .

    • 也就是说,marsze's answer提供高度优化的foreach基于预编译正则表达式的循环方法,速度更快,迭代次数更高,但它更冗长。

基准

  • 以下代码比较了此答案的 switch 的性能与 marsze 的接近 foreach方法。

  • 请注意,为了使两个解决方案完全等效,进行了以下调整:

    • & { ... }优化已添加到 switch命令也是如此。
    • IgnoreCaseCultureInvariant选项已添加到 foreach 隐式使用 PS 正则表达式匹配选项的方法。

不再使用6行样本文件,而是分别使用600行、3000行和30000行文件进行性能测试,以显示迭代次数对性能的影响。

正在对 100 次运行进行平均。

示例结果来自运行 Windows PowerShell v5.1 的 Windows 10 计算机 - 绝对时间并不重要,但希望相对时间更重要性能如Factor所示栏目一般具有代表性:

VERBOSE: Averaging 100 runs with a 600-line file of size 0.03 MB...

Factor Secs (100-run avg.) Command
------ ------------------- -------
1.00   0.023               # switch -Regex -File with regex string literal...
1.16   0.027               # foreach with precompiled regex and [regex].Match...
1.23   0.028               # switch -Regex -File with precompiled regex...


VERBOSE: Averaging 100 runs with a 3000-line file of size 0.15 MB...

Factor Secs (100-run avg.) Command
------ ------------------- -------
1.00   0.063               # foreach with precompiled regex and [regex].Match...
1.11   0.070               # switch -Regex -File with precompiled regex...
1.15   0.073               # switch -Regex -File with regex string literal...


VERBOSE: Averaging 100 runs with a 30000-line file of size 1.47 MB...

Factor Secs (100-run avg.) Command
------ ------------------- -------
1.00   0.252               # foreach with precompiled regex and [regex].Match...
1.24   0.313               # switch -Regex -File with precompiled regex...
1.53   0.386               # switch -Regex -File with regex string literal...

注意在较低迭代次数下的情况 switch -regex使用字符串文字最快,但 foreach 大约有 1,500 行。带有预编译的解决方案[regex]实例开始变得更快;使用预编译的[regex]实例 switch -regex返回程度较低,只是迭代次数较高。

基准代码,使用 Time-Command function :

# Sample file content (6 lines)
$fileContent = @'
TITLE %TIME%   NO "%zmyapps1%\*.*" ARCHIVE ATTRIBUTE   LINE2 1243
TITLE %TIME%   DOC/SET YQJ8   LINE2 1887
SET ztitle=%TIME%: WINFOLD   LINE2 2557
TITLE %TIME%   _*.* IN WINFOLD   LINE2 2597
TITLE %TIME%   %%ZDATE1%% YQJ25   LINE2 3672
TITLE %TIME%   FINISHED. PRESS ANY KEY TO SHUTDOWN ... LINE2 4922

'@

# Determine the full path to a sample file.
# NOTE: Using the *full* path is a *must* when calling .NET methods, because
#       the latter generally don't see the same working dir. as PowerShell.
$file = "$PWD/test.bat"

# Note: input is the number of 6-line blocks to write to the sample file,
#       which amounts to 600 vs. 3,000 vs. 30,0000 lines.
100, 500, 5000 | % { 

  # Create the sample file with the sample content repeated N times.
  $repeatCount = $_ 
  [IO.File]::WriteAllText($file, $fileContent * $repeatCount)

  # Warm up the file cache and count the lines.
  $lineCount = [IO.File]::ReadAllLines($file).Count

  # Define the commands to compare as an array of scriptblocks.
  $commands =
    { # switch -Regex -File with regex string literal
      & { 
        $i = 0
        $updatedLines = switch -Regex -File $file {
          '^(.*? (?:AROUND LINE|LINE2) )\d+(.*)$' { $Matches[1] + ++$i + $Matches[2] }
          default { ++$i; $_ }
        } 
        [IO.File]::WriteAllLines($file, $updatedLines, [text.encoding]::ASCII)
      }
    }, { # switch -Regex -File with precompiled regex
      & {
        $i = 0
        $regex = [Regex]::new('^(.*? (?:AROUND LINE|LINE2) )\d+(.*)$', 'Compiled, IgnoreCase, CultureInvariant')
        $updatedLines = switch -Regex -File $file {
          $regex { $Matches[1] + ++$i + $Matches[2] }
          default { ++$i; $_ }
        } 
        [IO.File]::WriteAllLines($file, $updatedLines, [text.encoding]::ASCII)
      }
    }, { # foreach with precompiled regex and [regex].Match
      & {
        $regex = [Regex]::new('^(.*? (?:AROUND LINE|LINE2) )\d+(.*)$', 'Compiled, IgnoreCase, CultureInvariant')
        $i = 0
        $updatedLines = foreach ($line in [IO.File]::ReadLines($file)) {
            $i++
            $m = $regex.Match($line)
            if ($m.Success) {
                $g = $m.Groups
                $g[1].Value + $i + $g[2].Value
            } else { $line }
        }
        [IO.File]::WriteAllLines($file, $updatedLines, [Text.Encoding]::ASCII)    
      }
    }

  # How many runs to average.
  $runs = 100

  Write-Verbose -vb "Averaging $runs runs with a $lineCount-line file of size $('{0:N2} MB' -f ((Get-Item $file).Length / 1mb))..."

  Time-Command -Count $runs -ScriptBlock $commands | Out-Host

}

关于performance - 电源外壳。修改 ascii 文本文件字符串,其中行号字符串已打开。交换机和 .NET 框架或 cmdlet 和管道?哪个更快?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54777202/

相关文章:

python - 在 Python 中比较字典键查找的性能如何?

ruby-on-rails - Rails 性能分析器

powershell - 如何使用Powershell捕获DISM输出并使用进度条反射(reflect)状态?

python - 使用Python的正则表达式在某些字符之间插入符号

java - 对于经典的动态规划 if-not-contains-then-put 来说,哪种风格更好?

.net - Windows Xperf diskio 操作不显示程序在性能跟踪 session 期间读取的文件

powershell - 如何读取文本文件中定义的参数值

powershell - 什么是 Get-Command -CommandType 脚本?

c# - 使用模式方法调用的 Resharper 搜索

r - 在 R 中,如何提取左括号之前的所有文本?