从哈希表中使用Perl正则表达式进行替换

13
有没有一种有效的方法,可以使用 Perl 哈希表中的值来替换多个字符串?
例如:
$regex{foo} = "bar";
$regex{hello} = "world";
$regex{python} = "perl";

open(F, "myfile.txt");
while (<F>) {
      foreach $key (keys %regex) {
            s/$key/$regex{$key}/g;
      }
}
close(F);

在Perl中有没有一种方法可以实现上述功能?


1
如果一切都失败了,尝试使用 eval(http://perldoc.perl.org/functions/eval.html) - Nick
1
@Nick,就我而言,那是最糟糕的建议。它怎么可能有所帮助呢? - ikegami
在Perl代码中,不能替换变量--eval允许您在字符串内扩展变量,然后将该字符串作为Perl代码执行,例如:eval "s/$key/$regex{$key}/g" - Nick
@Nick,再说一遍,那会让事情变得更糟。s///已经插值了。 - ikegami
好的——看看我的答案,基于“可怕”的eval - Nick
7个回答

7

第一个问题:你确定你所拥有的是低效的吗?

第二个问题,最明显的下一步是将所有内容合并为单个正则表达式:

my $check = join '|', keys %regex;

然后您可以进行如下替换:

s/($check)/$regex{$1}/g;

这可能会因为键的重叠而变得“缓慢”,导致正则表达式引擎不断重新检查相同的字母。您可以尝试使用Regexp::Optimizer之类的工具来消除这种重叠。但是,优化的成本可能比执行所有操作的成本更高,这取决于您要修改的哈希表中有多少更改(键/值)和多少行。过早地进行优化是不明智的。
请注意,当然,您的示例代码没有对替换后的文本做任何处理。它不会在原地修改文件,因此我假设您会单独处理该问题。

4

定义一个正则表达式,匹配任何一个键。

$regex = join("|", map {quotemeta} keys %regex);

将任何匹配$regex的内容替换为$regex{$1}

s/($regex)/$regex{$1}/go;

如果在程序执行期间$regex发生更改,则省略o修饰符。

请注意,如果有某些键是另一个键的前缀(例如ffoo),则以连接后的正则表达式中先出现的键为匹配项(例如f|foo匹配f,但foo|f匹配foobar中的foo)。如果可能出现这种情况,您可能需要根据您想要获胜的匹配进行排序keys %regex。(感谢ysth指出这一点。)


3
如果你有像abc和abcd这样的键,按长度递减排序很重要:map {quotemeta} sort { length($b)<=>length($a) } keys %regex - ysth
@ysth 谢谢,我从未意识到 Perl 有一个最左匹配策略,而不是最长匹配! - Gilles 'SO- stop being evil'

4
为了证明使用eval的观点并且出于好奇心,我进行了一些测试,比较了原帖中的代码、$regex{$1}方法以及eval方法。
首先,将所有可能的标记都塞入(token|token|...)匹配表达式中似乎没有什么价值。Perl需要一次性检查所有标记——这与逐个检查每个标记并用硬编码值进行替换相比,效率有多高仍存在争议。
其次,使用$regex{$1}意味着哈希映射键在每次匹配时都需要被提取。
无论如何,以下是一些数字(我在草莓5.12上运行了这个测试,使用了100K行的4MB文件):
  1. $regex{$1}方法需要6秒(使用/go而不是/g则为5秒)
  2. tie方法需要10秒
  3. 原帖中的方法需要不到1秒(使用/go而不是/g)
  4. eval方法需要不到1秒(比原帖中的代码更快)
这是使用eval的方法:
$regex{foo} = "bar";
$regex{hello} = "world";
$regex{python} = "perl";
$regex{bartender} = "barista";

$s = <<HEADER;
\$start = time;
open(F, "myfile.txt");
while (<F>) {
HEADER

foreach $key (keys %regex) {
   $s .= "s/$key/$regex{$key}\/go;\n"
}

$s .= <<FOOTER;
print \$_;
}
close(F);
print STDERR "Elapsed time (eval.pl): " . (time - \$start) . "\r\n";
FOOTER

eval $s;

很有趣,我没想到$regex{$1}方法会这么慢。使用Regexp::Optimizer会有所改善吗?时间会随着键的数量而改变吗? - Gilles 'SO- stop being evil'
@Giles,非常好的问题,显然--更不用说平台(Windows)和Perl分发可能会有所不同。任何关于这种性能分析的帮助都是非常欢迎的--也很好听到来自OP的一些东西-哪种方法在他/她的环境中最有效。 - Nick

1

你现在的代码可以正常工作,所以不清楚你的请求是什么。

但有一个问题:你发布的代码可能会因为%regex和/或$_的内容而出现双重替换的问题。例如:

my %regex = (
   foo => 'bar',
   bar => 'foo',
);

解决方案是将foreach放入模式中,这样说。
my $pat =
   join '|',
    map quotemeta,  # Convert text to regex patterns.
     keys %regex;

my $re = qr/$pat/;  # Precompile for efficiency.

my $qfn = 'myfile.txt'
open(my $fh, '<', $qfn) or die "open: $qfn: $!";
while (<$fh>) {
   s/($re)/$regex{$1}/g;
   ... do something with $_ ...
}

while循环不是解决方案!你在哪里写? - cirne100
@cirne100,你可以指定你想用编辑后的文本做什么。如果你想把它写在某个地方,就去吧。 - ikegami

1

开始:

#!/usr/bin/perl
use strict;
use Tie::File;

my %tr=(   'foo' => 'bar',
            #(...)
        );
my $r =join("|", map {quotemeta} keys %tr);
$r=qr|$r|;

处理大文件时使用:

tie my @array,"Tie::File",$ARGV[0] || die;
for (@array) { 
    s/($r)/$tr{$1}/g;
}
untie @array;

对于小文件使用:

open my $fh,'<',$ARGV[0] || die;
local $/ = undef;
my $t=<$fh>;
close $fh;
$t=~s/($r)/$tr{$1}/g;
open $fh,'>',$ARGV[0] || die;
print $fh $t;
close $fh;

1
perl -e '                                                         \
          my %replace =  (foo=>bar, hello=>world, python=>perl);  \
          my $find    =  join "|", sort keys %replace;            \
          my $str     =  "foo,hello,python";                      \
          $str        =~ s/($find)/$replace{$1}/g;                \
          print "$str\n\n";                                       \
        '

你可能想考虑的一件事是不要逐行读取文件,而是一次性处理整个文件,并在正则表达式上使用/s修饰符进行单行模式。


0
这是一个老问题,所以我很惊讶还没有人提出明显的建议:预编译每个正则表达式(即哈希键)。
$regex{qr/foo/} = 'bar';
$regex{qr/hello/} = 'world';
$regex{qr/python/} = 'perl';

open(F, "myfile.txt");
while (<F>) {
      foreach $key (keys %regex) {
            s/$key/$regex{$key}/g;
      }
}
close(F);

或者为了更好的可读性使用:

%regex = (
    qr/foo/    => 'bar',
    qr/hello/  => 'world',
    qr/python/ => 'perl',
);

如果您知道每个输入行只能有一个可能的匹配项,那么在成功匹配后使用last跳过剩余的正则表达式也会有所帮助,特别是当存在大量键时。例如,在for循环内部:
s/$key/$regex{$key}/g && last;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接