9

我正在尝试在一个 30Mb+ 的非常大的文本文件中替换 600 个不同的字符串。我目前正在构建一个执行此操作的脚本;在这个问题之后:

脚本:

$string = gc $filePath 
$string | % {
    $_ -replace 'something0','somethingelse0' `
       -replace 'something1','somethingelse1' `
       -replace 'something2','somethingelse2' `
       -replace 'something3','somethingelse3' `
       -replace 'something4','somethingelse4' `
       -replace 'something5','somethingelse5' `
       ...
       (600 More Lines...)
       ...
}
$string | ac "C:\log.txt"

但由于这将检查每一行 600 次,并且文本文件中有超过 150,000 多行,这意味着有很多处理时间。

有没有更好的替代方法可以更有效地执行此操作?

4

4 回答 4

6

结合Adi Inbar 的答案中的哈希技术和Keith Hill 对另一个最近问题的答案中的匹配评估器,以下是如何在 PowerShell 中执行替换:

# Build hashtable of search and replace values.
$replacements = @{
  'something0' = 'somethingelse0'
  'something1' = 'somethingelse1'
  'something2' = 'somethingelse2'
  'something3' = 'somethingelse3'
  'something4' = 'somethingelse4'
  'something5' = 'somethingelse5'
  'X:\Group_14\DACU' = '\\DACU$'
  '.*[^xyz]' = 'oO{xyz}'
  'moresomethings' = 'moresomethingelses'
}

# Join all (escaped) keys from the hashtable into one regular expression.
[regex]$r = @($replacements.Keys | foreach { [regex]::Escape( $_ ) }) -join '|'

[scriptblock]$matchEval = { param( [Text.RegularExpressions.Match]$matchInfo )
  # Return replacement value for each matched value.
  $matchedValue = $matchInfo.Groups[0].Value
  $replacements[$matchedValue]
}

# Perform replace over every line in the file and append to log.
Get-Content $filePath |
  foreach { $r.Replace( $_, $matchEval ) } |
  Add-Content 'C:\log.txt'
于 2013-07-28T23:21:15.447 回答
5

所以,你的意思是你想替换 150,000 行中的每一个中的 600 个字符串中的任何一个,并且你想每行运行一个替换操作?

是的,有办法做到这一点,但不是在 PowerShell 中,至少我想不出一个。它可以在 Perl 中完成。


方法:

  1. 构造一个散列,其中键是某些东西,值是其他东西。
  2. 用|加入散列的键 符号,并将其用作正则表达式中的匹配组。
  3. 在替换中,插入一个表达式,该表达式使用捕获组的匹配变量从哈希中检索一个值

问题:

令人沮丧的是,PowerShell 不会在正则表达式替换调用之外公开匹配变量。它不适用于-replace运算符,也不适用于[regex]::replace

在 Perl 中,您可以这样做,例如:

$string =~ s/(1|2|3)/@{[$1 + 5]}/g;

这将为整个字符串中的数字 1、2 和 3 添加 5,因此如果字符串为“1224526123 [2] [6]”,则变为“6774576678 [7] [6]”。

但是,在 PowerShell 中,这两个都失败了:

$string -replace '(1|2|3)',"$($1 + 5)"

[regex]::replace($string,'(1|2|3)',"$($1 + 5)")

在这两种情况下,$1的计算结果为 null,而表达式的计算结果为普通的旧 5。替换中的匹配变量仅在结果字符串中有意义,即单引号字符串或双引号字符串的计算结果。它们基本上只是看起来像匹配变量的反向引用。当然,您可以在双引号字符串中的数字前引用$,因此它将评估为相应的匹配组,但这违背了目的 - 它不能参与表达式。


解决方案:

[这个答案已经从原来的修改。它已被格式化以匹配带有正则表达式元字符的字符串。当然还有你的电视屏幕。]

如果您可以接受使用另一种语言,那么下面的 Perl 脚本就像一个魅力:

$filePath = $ARGV[0]; # Or hard-code it or whatever
open INPUT, "< $filePath";
open OUTPUT, '> C:\log.txt';
%replacements = (
  'something0' => 'somethingelse0',
  'something1' => 'somethingelse1',
  'something2' => 'somethingelse2',
  'something3' => 'somethingelse3',
  'something4' => 'somethingelse4',
  'something5' => 'somethingelse5',
  'X:\Group_14\DACU' => '\\DACU$',
  '.*[^xyz]' => 'oO{xyz}',
  'moresomethings' => 'moresomethingelses'
);
foreach (keys %replacements) {
  push @strings, qr/\Q$_\E/;
  $replacements{$_} =~ s/\\/\\\\/g;
}
$pattern = join '|', @strings;
while (<INPUT>) {
  s/($pattern)/$replacements{$1}/g;
  print OUTPUT;
}
close INPUT;
close OUTPUT;

它搜索散列的键(=>的左侧),并将它们替换为相应的值。这是正在发生的事情:

  • foreach循环遍历哈希的所有元素并创建一个名为@strings的数组,其中包含%replacements哈希的键,元字符使用\Q\E引用,引用的结果用作正则表达式模式(qr = 引用正则表达式)。在同一遍中,它通过将替换字符串中的所有反斜杠加倍来转义它们。
  • 接下来,数组的元素用|连接。's 来形成搜索模式。如果需要,您可以在$pattern中包含分组括号,但我认为这种方式可以更清楚地说明发生了什么。
  • while循环从输入文件中读取每一行,将搜索模式中的任何字符串替换为散列中相应的替换字符串,然后将该行写入输出文件。

顺便说一句,您可能已经注意到原始脚本的其他一些修改。在我最近的 PowerShell 启动过程中,我的 Perl 积聚了一些灰尘,再看一遍,我发现有几件事可以做得更好。

  • while (<INPUT>)一次读取一行文件。比将整个 150,000 行读入一个数组要明智得多,尤其是当您的目标是效率时。
  • 我简化@{[$replacements{$1}]}$replacements{$1}. Perl 没有像 PowerShell 的$()这样的内插表达式的内置方法,因此@{[ ]}用作一种解决方法 - 它创建一个包含表达式的元素的文字数组。但我意识到,如果表达式只是一个单一的标量变量(我将它作为初始测试的保留,我将计算应用于$1匹配变量),则没有必要。
  • close语句不是绝对必要的,但明确关闭文件句柄被认为是一种很好的做法。
  • 我将for缩写更改为foreach,以便 PowerShell 程序员更清楚、更熟悉。
于 2013-07-18T06:04:57.230 回答
2

我也不知道如何在 powershell 中解决这个问题,但我知道如何在 Bash 中解决它,那就是使用一个名为 sed 的工具。幸运的是,还有适用于 Windows 的 Sed。如果您只想在任何地方将“something#”替换为“somethingelse#”,那么此命令将为您解决问题

sed -i "s/something([0-9]+)/somethingelse\1/g" c:\log.txt

在 Bash 中,您实际上需要使用反斜杠来转义其中的几个字符,但我不确定您是否需要在 Windows 中这样做。如果第一个命令抱怨你可以尝试

sed -i "s/something\([0-9]\+\)/somethingelse\1/g" c:\log.txt
于 2013-07-20T01:16:07.727 回答
1

我会使用 powershell switch 语句:

$string = gc $filePath 
$string | % {
    switch -regex ($_)  {
        'something0' { 'somethingelse0' }
        'something1' { 'somethingelse1' }
        'something2' { 'somethingelse2' }
        'something3' { 'somethingelse3' }
        'something4' { 'somethingelse4' }
        'something5' { 'somethingelse5' }
        'pattern(?<a>\d+)' { $matches['a'] } # sample of more complex logic
   ...
   (600 More Lines...)
   ...
        default { $_ }
   }
} | ac "C:\log.txt"
于 2013-10-22T14:14:28.973 回答