正则表达式 - Ruby与Perl的比较

24

我注意到我的 Ruby(1.9)脚本存在极度的延迟问题,经过一些挖掘,发现问题在于正则表达式的匹配。我使用了以下 Perl 和 Ruby 的测试脚本:

Perl:

$fname = shift(@ARGV);
open(FILE, "<$fname" );
while (<FILE>) {
    if ( /(.*?) \|.*?SENDING REQUEST.*?TID=(.*?),/ ) {
        print "$1: $2\n";
    }
}

Ruby:

=>

Ruby:

f = File.open( ARGV.shift )
while ( line = f.gets )
    if /(.*?) \|.*?SENDING REQUEST.*?TID=(.*?),/.match(line)
        puts "#{$1}: #{$2}"
    end
end

我在两个脚本中使用相同的输入,一个只有44290行的文件。 每个脚本的时间如下:

Perl:

xenofon@cpm:~/bin/local/project$ time ./try.pl input >/dev/null

real    0m0.049s
user    0m0.040s
sys     0m0.000s

Ruby:

xenofon@cpm:~/bin/local/project$ time ./try.rb input >/dev/null

real    1m5.106s
user    1m4.910s
sys     0m0.010s

我想我在做某些非常愚蠢的事情,有什么建议吗?

谢谢


2
你尝试过 if line =~ /(.*?) \|.*?SENDING REQUEST.*?TID=(.*?),/ 吗?这在 Ruby 中也可以工作,我很想知道它是否具有不同的性能特征。 - Michael Kohl
6个回答

7
regex = Regexp.new(/(.*?) \|.*?SENDING REQUEST.*?TID=(.*?),/)

f = File.open( ARGV.shift ).each do |line|
    if regex .match(line)
        puts "#{$1}: #{$2}"
    end
end

或者
regex = Regexp.new(/(.*?) \|.*?SENDING REQUEST.*?TID=(.*?),/)

f = File.open( ARGV.shift )
f.each_line do |line|
  if regex.match(line)
    puts "#{$1}: #{$2}"
  end

3
我尝试了你的建议,但没有任何改变,执行时间仍然是1分5.134秒。 - xpapad
2
几个小问题:在完成后,您需要释放文件描述符,可以通过调用close或使用File.open('filename') { |file| }来实现,这可以确保文件已关闭。此外,/#{...}/表示Regexp字面量;调用Regexp.new是不必要的。 - Matheus Moreira
@stema Perl自动执行了什么操作? - SwiftMango
2
@texasbruce:当perl(解释器)发现一个常量正则表达式时,它会编译并缓存它以便重复使用。这与大多数其他语言不同,其他语言需要程序员手动完成此步骤。(请参见stema的答案)。 - Robert P

5

来自perlretut章节:使用正则表达式在Perl中的"搜索和替换"部分:

(尽管正则表达式出现在循环中,但Perl足够聪明,只编译一次。)

我不太了解Ruby,但我怀疑它会在每个循环中编译正则表达式。
(尝试使用LaGrandMere答案中的代码进行验证)。


我怀疑这一点。有一种特殊的语法,所以它很可能是在解析阶段构建的…比循环要早得多。 - remram

5

可能的一个区别是回溯的数量。当回溯时(即注意到某个模式部分不可能匹配时),Perl 可能会更好地修剪搜索树。其正则表达式引擎高度优化。

首先,添加前导“^”可能会产生很大的差异。如果该模式在位置0处不匹配,则它也不会在位置1处开始匹配!因此,请勿尝试在位置1处进行匹配。

同样,在这条线上,“.*?”并不像您想象的那样受限制,并且将每个实例替换为更具限制性的模式可以防止大量回溯。

为什么不试试:

/
    ^
    (.*?)                       [ ]\|
    (?:(?!SENDING[ ]REQUEST).)* SENDING[ ]REQUEST
    (?:(?!TID=).)*              TID=
    ([^,]*)                     ,
/x

(不确定是否安全将第一个«.*?»替换为«[^|]»,所以我没有这样做。)

(至少对于匹配单个字符串的模式,(?:(?!PAT).)PAT相当于[^CHAR]CHAR。)

如果允许«.»匹配换行符,则使用/s可能会加快速度,但我认为这只是小事情。

在Ruby中,使用«\space»而不是«[space]»来匹配空格可能会稍微快一些。(在最近版本的Perl中它们是相同的。)我使用后者是因为它更易读。


2
尝试使用(?>re)扩展。详见Ruby-Documentation,以下是引述:
该构造[...]抑制回溯,这可以提高性能。例如,模式/a.*b.*a/会在匹配包含一个a后跟一些b但没有尾随a的字符串时花费指数时间。但是,可以通过使用嵌套正则表达式/a(?>.*b).*a/来避免这种情况。
File.open(ARGV.shift) do |f|
  while line = f.gets
    if /(.*?)(?> \|.*?SENDING REQUEST.*?TID=)(.*?),/.match(line)
      puts "#{$1}: #{$2}"
    end
  end
end

1

Ruby:

File.open(ARGV.shift).each do |line|
    if line =~ /(.*?) \|.*?SENDING REQUEST.*?TID=(.*?),/
        puts "#{$1}: #{$2}"
    end
end

match方法更改为=~运算符。这样做会更快,因为:

(Ruby具有基准测试功能。我不知道您的文件内容,所以随意输入了一些内容)

require 'benchmark'

def bm(n)
    Benchmark.bm do |x|
    x.report{n.times{"asdfajdfaklsdjfklajdklfj".match(/fa/)}}
    x.report{n.times{"asdfajdfaklsdjfklajdklfj" =~ /fa/}}
    x.report{n.times{/fa/.match("asdfajdfaklsdjfklajdklfj")}}
    end
end

bm(100000)

输出报告:

       user     system      total        real
   0.141000   0.000000   0.141000 (  0.140564)
   0.047000   0.000000   0.047000 (  0.046855)
   0.125000   0.000000   0.125000 (  0.124945)

中间的方法使用了=~,它比其他两种方法快了三分之二。其他两种方法使用了match函数。因此,在您的代码中使用=~


1

与其他形式的匹配相比,正则表达式匹配需要耗费更多时间。由于您期望在匹配行的中间获得一个长的静态字符串,请尝试使用相对便宜的字符串操作来过滤掉不包含该字符串的行。这应该会减少需要通过正则表达式解析的内容(当然,这取决于您的输入是什么样子)。

f = File.open( ARGV.shift )
my_re = Regexp.new(/(.*?) \|.*?SENDING REQUEST.*?TID=(.*?),/)
while ( line = f.gets )
    continue if line.index('SENDING REQUEST') == nil
    if my_re.match(line)
        puts "#{$1}: #{$2}"
    end
end
f.close()

我没有对这个特定版本进行基准测试,因为我没有你的输入数据。不过,我过去在类似的事情上取得了成功,尤其是在处理冗长的日志文件时,预过滤可以消除大部分输入而不运行任何正则表达式。


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