Подтвердить что ты не робот

Как сделать токенизацию этой строки в Ruby?

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

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

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

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

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

Любые указатели?

4b9b3361

Ответ 1

Для реального языка, lexer способ пойти - как сказал 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) соответствует регулярному выражению по строке столько раз, сколько возможно, выводя массив "совпадений". Если регулярное выражение содержит захват parens, "совпадение" представляет собой массив элементов, захваченных - поэтому $1 становится match[0], $2 становится match[1] и т.д. Любая скользящая скобка, которая не сопоставляется с частью строковые карты в запись nil в результате "соответствия".

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

Ответ 2

Ниже приведен пример ненадежного использования 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

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

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

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