加快我的lexing算法

时间:2011-11-20 07:58:08

标签: ruby lexical-analysis

我正在将一个可能很大的字符串(比方说20MB,虽然这完全是任意的)拆分成由正则表达式列表定义的标记。

我当前的算法采用以下方法:

  1. 所有正则表达式都经过优化,在其开头具有零宽度断言^
  2. 对于列表中的每个正则表达式,我尝试#slice!输入字符串
  3. 如果我们#slice!什么,我们得到一个匹配并且输入字符串已经准备好找到下一个标记(因为#slice!修改了字符串)
  4. 不幸的是,这很慢,这是由于长字符串上重复#slice! ...似乎修改ruby中的大字符串并不快。

    所以我想知道是否有办法将我的正则表达式与新的子字符串(即字符串的其余部分)相匹配而不修改它?

    (测试的,可运行的)伪代码中的当前算法:

      rules = {
        :foo => /^foo/,
        :bar => /^bar/,
        :int => /^[0-9]+/
      }
    
      input = "1foofoo23456bar1foo"
      # or if you want your computer to cry
      # input = "1foofoo23456bar1foo" * 1_000_000
    
      tokens = []
    
      until input.length == 0
        matched = rules.detect do |(name, re)|
          if match = input.slice!(re)
            tokens << { :rule => name, :value => match }
          end
        end
    
        raise "Uncomsumed input: #{input}" unless matched
      end
    
      pp tokens
      # =>
      [{:rule=>:int, :value=>"1"},
       {:rule=>:foo, :value=>"foo"},
       {:rule=>:foo, :value=>"foo"},
       {:rule=>:int, :value=>"23456"},
       {:rule=>:bar, :value=>"bar"},
       {:rule=>:int, :value=>"1"},
       {:rule=>:foo, :value=>"foo"}]
    

    请注意,虽然非常简单地将正则表达式与字符串匹配相同的次数并不是很快,但是在你等待的时候你没有时间煮比萨饼(几秒钟,vs很多分钟。)

2 个答案:

答案 0 :(得分:3)

String#match()方法有一个双参数版本,它将匹配从字符串中特定字符位置开始的正则表达式。您只需要将one-past-the-last-matching-character from the previous match作为新匹配的起始位置。

在未经测试的非运行伪代码中:

input = "foo"
input_pos = 0
input_end = input.length

until input_pos == input_end do
  matched = rules.detect do |(name, re)|
    if match = input.match(re, input_pos)
        tokens << { :rule => name, :value => match }
        input_pos = match.post_match
    end
  end
end

答案 1 :(得分:1)

也许我过于简单了,但String#scan最有可能胜过其他任何事情:

tokens = input.scan(/foo|bar|\d+/).map{|m| {:value => m, :rule => rules.find{|name,re| m =~ re}[0]}}

或更一般地说:

rules = {
    :foo => /foo/,
    :bar => /bar/,
    :int => /[0-9]+/
}
tokens = input.scan(Regexp.union(rules.values)).map{|m| {:value => m, :rule => rules.find{|name,re| m =~ re}[0]}}