【问题标题】:Regular expression - Ruby vs Perl正则表达式 - Ruby vs Perl
【发布时间】:2012-05-01 20:53:51
【问题描述】:

我注意到我的 Ruby (1.9) 脚本中有一些极端的延迟,经过一番挖掘,它归结为正则表达式匹配。我在 Perl 和 Ruby 中使用以下测试脚本:

Perl:

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

鲁比:

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

鲁比:

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

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

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

谢谢

【问题讨论】:

  • 你试过if line =~ /(.*?) \|.*?SENDING REQUEST.*?TID=(.*?),/吗?这也适用于 Ruby,我很好奇它是否具有不同的性能特征。

标签: ruby regex perl


【解决方案1】:

尝试使用(?&gt;re) 扩展。有关详细信息,请参阅Ruby-Documentation,此处引用:

这个结构 [..] 禁止回溯,这可以是 性能增强。例如,/a.*b.*a/ 模式采用 与包含a 的字符串匹配时的指数时间 后跟多个bs,但没有尾随a。然而, 这可以通过使用嵌套的正则表达式来避免 /a(?&gt;.*b).*a/.

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

【讨论】:

    【解决方案2】:

    与其他形式的匹配相比,正则表达式匹配比较耗时。由于您希望匹配行中间有一个长的静态字符串,因此请尝试使用相对便宜的字符串操作过滤掉不包含该字符串的行。这应该会减少需要通过正则表达式解析的工作(当然,这取决于您的输入内容)。

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

    我没有对这个特定版本进行基准测试,因为我没有您的输入数据。不过,我过去曾成功地做这样的事情,尤其是对于冗长的日志文件,其中预过滤可以消除绝大多数输入而无需运行任何正则表达式。

    【讨论】:

      【解决方案3】:

      鲁比:

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

      将match 方法更改为=~ 运算符。它更快,因为:

      (Ruby 有 Benchmark。我不知道你的文件内容,所以我随机输入了一些内容)

      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)
      

      中间的使用=~。它需要不到其他人的1/3。其他两个正在使用match 方法。所以,在你的代码中使用=~。

      【讨论】:

        【解决方案4】:

        来自perlretut chapter: Using regular expressions in Perl 部分 - “搜索和替换”

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

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

        【讨论】:

        • 我对此表示怀疑。它有一个特殊的语法,所以它可能是在解析阶段构建的......在循环之前。
        【解决方案5】:

        一个可能的区别是正在执行的回溯量。 Perl 在回溯时可能会更好地修剪搜索树(即,当模式的一部分可能无法匹配时注意到)。它的正则表达式引擎经过高度优化。

        首先,添加前导 «^» 可能会产生巨大的影响。如果模式从位置 0 开始不匹配,那么它也不会在开始位置 1 匹配!所以不要尝试在位置 1 匹配。

        同样,«.*?» 并不像您想象的那样具有限制性,将它的每个实例替换为更具限制性的模式可以防止大量回溯。

        你为什么不试试:

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

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

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

        如果允许 «.» 匹配换行符,使用 /s 可能会加快速度,但我认为它很小。

        使用«\<i>space</i>»而不是«[<i>space</i>]»来匹配/x下的空格在Ruby中可能会稍微快一些。 (在最新版本的 Perl 中它们是相同的。)我使用后者是因为它更具可读性。

        【讨论】:

          【解决方案6】:
          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
          

          【讨论】:

          • 我尝试了你的建议,但没有任何变化,执行时间仍然是 1m5.134s
          • 一些吹毛求疵:您需要在使用完文件描述符后释放文件描述符,方法是调用close 或使用File.open('filename') { |file| },以确保文件已关闭。此外,/#{...}/ 表示 Regexp 文字;调用Regexp.new 是不必要的。
          • @stema Perl 自动在做什么?
          • @texasbruce:当 perl(解释器)发现一个常量正则表达式时,它会编译并缓存它以供重用。这与大多数其他语言相反,在大多数其他语言中,此步骤必须由程序员手动完成。 (见stema's answer.)
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-07-22
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-08-06
          相关资源
          最近更新 更多