12

我有这个字符串:

%{Children^10 Health "sanitation management"^5}

我想将其转换为将其标记为哈希数组:

[{:keywords=>"children", :boost=>10}, {:keywords=>"health", :boost=>nil}, {:keywords=>"sanitation management", :boost=>5}]

我知道 StringScanner 和Syntax gem,但我找不到足够的代码示例。

任何指针?

4

3 回答 3

17

对于一门真正的语言,词法分析器是必经之路——就像 Guss 所说的那样。但是,如果完整的语言只和你的例子一样复杂,你可以使用这个快速破解:

irb> text = %{Children^10 Health "sanitation management"^5}
irb> text.scan(/(?:(\w+)|"((?:\\.|[^\\"])*)")(?:\^(\d+))?/).map do |word,phrase,boost|
       { :keywords => (word || phrase).downcase, :boost => (boost.nil? ? nil : boost.to_i) }
     end
#=> [{:boost=>10, :keywords=>"children"}, {:boost=>nil, :keywords=>"health"}, {:boost=>5, :keywords=>"sanitation management"}]

如果您正在尝试解析常规语言,那么这种方法就足够了 - 尽管使语言变得非常规并不需要更多的复杂性。

正则表达式的快速分解:

  • \w+匹配任何单项关键字
  • (?:\\.|[^\\"]])*使用非捕获括号 ( (?:...)) 来匹配转义双引号字符串的内容 - 转义符号 ( \n, \",\\等) 或任何不是转义符号或结束引号的单个字符。
  • "((?:\\.|[^\\"]])*)"仅捕获引用的关键字词组的内容。
  • (?:(\w+)|"((?:\\.|[^\\"])*)")匹配任何关键字 - 单个术语或短语,将单个术语捕获到$1并将短语内容捕获到$2
  • \d+匹配一个数字。
  • \^(\d+)捕获插入符号 ( ^) 后面的数字。由于这是第三组捕获括号,它将被捕获到$3.
  • (?:\^(\d+))?如果存在插入符号后面的数字,则捕获该数字,否则匹配空字符串。

String#scan(regex)尽可能多地将正则表达式与字符串匹配,输出“匹配”数组。如果正则表达式包含捕获括号,则“匹配”是捕获的项目数组 - 因此$1变为match[0]$2变为match[1]等。任何未与部分字符串匹配的捕获括号映射到nil结果“匹配”中的条目。

然后#map获取这些匹配项,使用一些块魔法将每个捕获的术语分解为不同的变量(我们可以这样做do |match| ; word,phrase,boost = *match),然后创建您想要的哈希值。恰好是wordorphrase之一nil,因为两者都不能与输入匹配,所以(word || phrase)将返回非nil一,#downcase并将其​​转换为全小写。 boost.to_i将字符串转换为整数,同时(boost.nil? ? nil : boost.to_i)确保nilboosts 保持不变nil

于 2009-04-03T12:17:02.270 回答
13

这是一个使用StringScanner. 这是我刚刚改编自Ruby Quiz: Parsing JSON的代码,它有一个很好的解释。

require 'strscan'

def test_parse
  text = %{Children^10 Health "sanitation management"^5}
  expected = [{:keywords=>"children", :boost=>10}, {:keywords=>"health", :boost=>nil}, {:keywords=>"sanitation management", :boost=>5}]


  assert_equal(expected, parse(text))
end

def parse(text)
  @input = StringScanner.new(text)

  output = []

  while keyword = parse_string || parse_quoted_string
    output << {
      :keywords => keyword,
      :boost => parse_boost
    }
    trim_space
  end

  output
end

def parse_string
  if @input.scan(/\w+/)
    @input.matched.downcase
  else
    nil
  end
end

def parse_quoted_string
  if @input.scan(/"/)
    str = parse_quoted_contents
    @input.scan(/"/) or raise "unclosed string"
    str
  else
    nil
  end
end

def parse_quoted_contents
  @input.scan(/[^\\"]+/) and @input.matched
end

def parse_boost
  if @input.scan(/\^/)
    boost = @input.scan(/\d+/)
    raise 'missing boost value' if boost.nil?
    boost.to_i
  else
    nil
  end
end

def trim_space
  @input.scan(/\s+/)
end
于 2009-04-03T13:55:42.297 回答
3

您在这里拥有的是任意语法,并且要解析它,您真正想要的是词法分析器 - 您可以编写一个描述您的语法的语法文件,然后使用词法分析器从您的语法生成递归解析器。

编写词法分析器(甚至是递归解析器)并不是一件容易的事——尽管它是编程中的一个有用练习——但你可以在此电子邮件中找到 Ruby 词法分析器/解析器的列表:http: //newsgroups.derkeiler.com /Archive/Comp/comp.lang.ruby/2005-11/msg02233.html

RACC 作为 Ruby 1.8 的标准模块提供,所以我建议您专注于它,即使它的手册不是很容易理解并且需要熟悉 yacc。

于 2009-04-03T11:49:06.393 回答