如何使用易于阅读且易于使用 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)
这个问题从最年轻到最老的演变: 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在输出中(不是2
和3
)。- 请注意
(?:...)
是一个非捕获组,因此根据设计,它不会反射(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
基于预编译正则表达式的循环方法,速度更快,迭代次数更高,但它更冗长。
- 也就是说,marsze's answer提供高度优化的
基准
以下代码比较了此答案的
switch
的性能与 marsze 的接近foreach
方法。请注意,为了使两个解决方案完全等效,进行了以下调整:
& { ... }
优化已添加到switch
命令也是如此。IgnoreCase
和CultureInvariant
选项已添加到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/