Как я могу токенизировать эту строку в Ruby? - PullRequest
12 голосов
/ 03 апреля 2009

У меня есть эта строка:

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

И я хочу преобразовать его в токенизацию в массив хэшей:

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

Мне известны StringScanner и синтаксис gem , но я не могу найти достаточно примеров кода для обоих.

Есть указатели?

Ответы [ 3 ]

17 голосов
/ 03 апреля 2009

Для настоящего языка лексеру свой путь - , как сказал Гасс . Но если полный язык настолько сложен, как ваш пример, вы можете использовать этот быстрый взлом:

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, \", \\ и т. Д.), Либо любого другого символ, который не является escape-символом или конечной кавычкой.
  • "((?:\\.|[^\\"]])*)" захватывает только содержимое цитируемой ключевой фразы.
  • (?:(\w+)|"((?:\\.|[^\\"])*)") соответствует любому ключевому слову - одному термину или фразе, включая отдельные термины в $1 и содержание фраз в $2
  • \d+ соответствует номеру.
  • \^(\d+) захватывает число после каретки (^). Поскольку это третий набор скобок, он будет заключен в $3.
  • (?:\^(\d+))? захватывает число после каретки, если оно там, в противном случае соответствует пустой строке.

String#scan(regex) сопоставляет регулярное выражение со строкой столько раз, сколько возможно, выводя массив "совпадений". Если регулярное выражение содержит захватывающие парены, «match» - это массив захваченных элементов, поэтому $1 становится match[0], $2 становится match[1] и т. Д. Любая скобка захвата, которая не сопоставляется с частью строка соответствует записи nil в полученном "совпадении".

Затем #map берет эти совпадения, использует некоторую магию блока, чтобы разбить каждый захваченный термин на разные переменные (мы могли бы сделать do |match| ; word,phrase,boost = *match), а затем создает желаемые хэши. Точно один из word или phrase будет nil, поскольку оба не могут быть сопоставлены с входом, поэтому (word || phrase) вернет не-1055 * единицу, а #downcase преобразует его во все в нижнем регистре. boost.to_i преобразует строку в целое число, в то время как (boost.nil? ? nil : boost.to_i) гарантирует, что nil повышает значение nil.

12 голосов
/ 03 апреля 2009

Вот ненадежный пример использования 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
3 голосов
/ 03 апреля 2009

Здесь у вас есть произвольная грамматика, и для ее анализа то, что вам действительно нужно, это лексер - вы можете написать файл грамматики, который описывает ваш синтаксис, а затем использовать лексер для генерации рекурсивного синтаксического анализатора из вашей грамматики.

Написание лексера (или даже рекурсивного парсера) на самом деле не тривиально - хотя это полезное упражнение в программировании - но вы можете найти список лексеров / парсеров Ruby в этом сообщении электронной почты здесь: http://newsgroups.derkeiler.com/Archive/Comp/comp.lang.ruby/2005-11/msg02233.html

RACC доступен в качестве стандартного модуля Ruby 1.8, поэтому я советую вам сосредоточиться на этом, даже если его руководство не очень простое и требует знакомства с yacc.

...