PowerShell5。修改带有行号字符串的 ascii 文本文件字符串。 Switch 和 .NET Framework 或 cmdlet 和管道?哪个更快?

PowerShell5. Modify ascii text file string with line number string is on. Switch and .NET framework or cmdlets & the pipeline? Which is faster?

如何使用 PowerShell 5 使用易于阅读且易于 add/modify/delete 的搜索字符串修改 windows ascii 文本文件中的字符串 (LINE2 "line number LINE2 is on")。此脚本将解析一个 2500 行的文件,找到 139 个字符串实例,替换它们并在平均不到 165 毫秒的时间内覆盖原始字符串,具体取决于您使用的方法。哪种方法更快?哪种方法更容易 add/modify/delete 字符串?

搜索字符串 "AROUND LINE {1-9999}" 和 "LINE2 {1-9999}" 并将 {1-9999} 替换为代码所在的 {行号}。测试是使用 2500 行文件而不是两行 sample.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}

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

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

-replace 'AROUND LINE \d+', "AROUND LINE $lc" -replace 'LINE2 \d+', "LINE2 $lc" -replace 'PLACEMARK \d+', "PLACEMARK $lc"

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

(gc $args[0]) | foreach -Begin {$lc = 1} -Process { $_ -replace 'AROUND LINE \d+', "AROUND LINE $lc" -replace 'LINE2 \d+', "LINE2 $lc" ++$lc } | sc -Encoding 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)}

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

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

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)}

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

Add/Modify/Delete 您的搜索字符串:

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)

编者注:这是

的后续问题

这道题从小到大的演变: 1. 2. 3. 4.

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

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

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

  • ++$lc inside default { ++$lc; $_ } 确保行计数器在通过行之前也为非匹配行递增递交 ($_).


性能说明

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

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

  • 此外,如果 case-sensitive 匹配就足够了,你可以通过添加 一点 来挤出更多的性能switch 语句的 -CaseSensitive 选项。

  • 在高层次上,使解决方案快速的原因是使用 switch -File 来处理行 ,并且通常 对文件 I/O 使用 .NET 类型(而不是 cmdlet)(IO.File]::WriteAllLines() 在这种情况下,如问题所示)- 另请参阅 .

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

基准

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

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

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

用600行、3000行和30000行的文件代替6行的示例文件来测试性能,以显示迭代次数对性能的影响。

正在计算 100 次运行的平均值。

示例结果 来自我的 Windows 10 机器 运行 Windows PowerShell v5.1 - 绝对 次并不重要,但希望 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 使用 字符串文字 是最快的,但是在大约 1,500 行时 foreach 解决方案使用预编译 [regex] 实例开始变得更快;使用具有 switch -regex 的预编译 [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

}

备选方案:

$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)