PowerShell5。查找/替换文本。交换机和.NET Framework或cmdlet和管道?哪个更快?哪个更容易阅读?

时间:2019-02-20 00:52:03

标签: performance powershell replace file-io powershell-v5.0

如何在Windows文本文件中查找和替换文本,并使用搜索和替换易于阅读且易于添加/修改/删除的字符串。该脚本将解析6800行文件,找到70个字符串实例,对其重新编号,并在不到400ms的时间内覆盖原始字符串。

搜索字符串“ AROUND LINE {1-9999}”和“ LINE2 {1-9999}”,并将{1-9999}替换为代码所在的{line number}。弦线周围有前后空格。最后两个测试是使用整个源批处理副本完成的,并将其粘贴到sample.bat中。

sample.bat包含两行:

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

当前代码包括寻找AROUND LINE和@ mklement0解决方案。

copy-item $env:temp\sample.bat -d $env:temp\sample.bat.$((get-date).tostring("HHmmss"))
$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)

预期结果:

ECHO AROUND LINE 1
TITLE %TIME%   DISPLAY TCP-IP SETTINGS   LINE2 2

实际结果:

ECHO AROUND LINE 1 
TITLE %TIME%   DISPLAY TCP-IP SETTINGS   LINE2 2 

使用switch,.NET框架和粘贴到sample.bat中的整个批处理文件进行测量:

Measure-command {
copy-item $env:temp\sample.bat -d $env:temp\sample.bat.$((get-date).tostring("HHmmss"))
    $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)}

结果:十次运行中75ms-386ms。

使用Get-Content + -replace + Set-Content并将整个批处理文件粘贴到sample.bat中进行测量:

Measure-command {
copy-item $env:temp\sample.bat -d $env:temp\sample.bat.$((get-date).tostring("HHmmss"))
(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}

结果:十次运行中363ms-451ms。

搜索字符串是一个易于理解的正则表达式。

您可以通过添加另一个-replace来搜索其他字符串。

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

编者注:这是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正则表达式解决方案。

2 个答案:

答案 0 :(得分:2)

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

    • 请注意,2是一个不捕获组,因此从设计上讲,它不会反映在3中。
  • 不是使用(?:...)来读取文件,而是使用$Matches的{​​{1}}选项,该选项直接从文件[IO.File]::ReadAllLines($file)中读取行。 / p>

  • -File中的switch确保在 non-matching 行中,行计数器也要递增({{1 })。


性能说明

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

    $file
  • 使用预编译的++$lc实例而不是PowerShell转换为的字符串文字,具有高迭代次数(大量行)幕后的正则表达式可以进一步加快处理速度-请参见下面的基准测试。

  • 此外,如果区分大小写 匹配就足够了,则可以通过在{上添加default { ++$lc; $_ }选项来降低 的性能。 {1}}语句。

  • 从总体上讲,使解决方案快速的原因是使用$_处理行,并且通常使用.NET类型文件I / O (而不是cmdlet)(在本例中为# Enclose the switch statement in & { ... } to speed it up slightly. $updatedLines = & { switch -Regex -File ... } ,如问题所示)-另请参见this related answer

    • 也就是说,marsze's answer提供了一种基于预编译正则表达式的高度优化的[regex]循环方法,该方法在更高的迭代次数下会更快-然而,它更为冗长。

基准

  • 以下代码将此答案的-CaseSensitive方法与marsze的switch方法的性能进行了比较。

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

    • switch -File优化也已添加到IO.File]::WriteAllLines()命令中。
    • foreach方法中添加了switchforeach选项,以匹配隐式使用 PS正则表达式的选项。

分别用600行,3,000和30,000行文件而不是6行示例文件来测试性能,以显示迭代计数对性能的影响。

平均100次跑步。

我运行Windows PowerShell v5.1的Windows 10计算机上的

采样结果-绝对时间并不重要,但希望相对 & { ... }栏中显示的效果通常具有代表性:

switch

请注意,使用 string文字在较低的迭代次数下IgnoreCase最快,但是在大约1,500行的情况下,使用预编译的CultureInvariant实例的foreach解决方案开始了变得更快;将预编译的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... 一起使用,只会得到较小的回报,只有迭代次数更高。

基准代码,使用Time-Command function

switch -regex

答案 1 :(得分:2)

替代解决方案:

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