treetop を使う
構文解析に慣れていないのと、 サンプルがあまり見当たらないのでけっこうはまった。
文字列を空白で分割する
treetop で、まずは文字列を空白で分割して配列にするパーサを記述してみる。 ただ、「"」で囲まれた文字列は空白があっても一つとする。 ダブルクォーテーションとスラッシュについては、スラッシュでエスケープするようにする。 つまり、「"」そのものは「\"」とし、「\」そのものは「\\」とする。
split_word.rb
grammar SplitWord
rule words
word " "+ words {
def value
elements[2].value.unshift(elements[0].value)
end
}
/
word {
def value
[super]
end
}
end
rule word
'"' ('\\\\' / '\\"' / (!'"' .))* '"' {
def value
text_value[1...-1]
end
}
/
[^ ]+ {
def value
text_value
end
}
end
end
test.rb
require 'treetop'
Treetop.load "split_word"
parser = SplitWordParser.new
inputs = ['one', '1 2 3', 'hello world', '"1 2" 3', '"a\\" b"', '"a\\\\" b"']
inputs.each do |input|
if result = parser.parse(input)
p result.value
else
puts "Can not parse"
end
end
実行
split_word.rb と test.rb を同じディレクトリにおいて実行すると
$ ruby test.rb
["one"]
["1", "2", "3"]
["hello", "world"]
["1 2", "3"]
["a\\\" b"]
["a\\\\", "b\""]
となる。
GMail の検索っぽい解析
GMail のような検索文字列の解析をやってみた。
- 「”」で囲むと特殊な文字も文字列として扱う
- 「 」で AND
- 「OR」で OR
- 「-」を先頭につけると NOT
- 「(」と「)」囲うとグループにする
- 「before:」と「after:」で時間を指定できる
query.rb
結果は :and や :or などのシンボルと値を順に配列に入れることにした。
grammar Query
rule queries
query " "+ "OR" " "+ queries {
def value
[:or, elements[0].value, elements[4].value]
end
}
/
query " "+ queries {
def value
[:and, elements[0].value, elements[2].value]
end
}
/
query
end
rule query
word " "+ "OR" " "+ queries {
def value
[:or, elements[0].value, elements[4].value]
end
}
/
word " "+ queries {
def value
[:and, elements[0].value, elements[2].value]
end
}
/
"(" query ")" {
def value
elements[1].value
end
}
/
word
end
rule word
"before:" (quoted_word / word_without_special_characters) {
def value
[:before, elements[1].value]
end
}
/
"after:" (quoted_word / word_without_special_characters) {
def value
[:after, elements[1].value]
end
}
/
"-" (quoted_word / word_without_special_characters) {
def value
[:not, elements[1].value]
end
}
/
quoted_word
/
!"(" [^() ]* {
def value
elements[1].text_value
end
}
end
rule quoted_word
'"' ('\\\\' / '\\"' / (!'"' .))* '"' {
def value
text_value[1...-1]
end
}
end
rule word_without_special_characters
[^ ()]* {
def value
text_value
end
}
end
end
test.rb
require 'treetop'
Treetop.load "query"
parser = QueryParser.new
inputs = ['hello world', '"1 2" 3', '" OR "', '"a OR b"',
'a (b c)', '((a))', '(a b) OR (c d)', '((a b)) OR (c d)',
'(a OR b) (c d)', '(a OR (b c) OR d) (c d)',
'"a\\" b" "a\\\\" b"', "hello OR world", '(ABC "(1, 2)")',
'"-hi" -hello', '-hi OR -hello', '(-hi)', '(-hi OR -hello)', '(-hi -hello)', 'HELLO (-hi OR -hello)',
'before:2011-09-03', 'after:2011-09-03', "(after:2011-09-03 before:2011-09-04)"]
inputs.each do |input|
if result = parser.parse(input)
p result.value
else
puts "Can not parse: #{input}"
end
end
実行
$ ruby test.rb
[:and, "hello", "world"]
[:and, "1 2", "3"]
" OR "
"a OR b"
[:and, "a", [:and, "b", "c"]]
"a"
[:or, [:and, "a", "b"], [:and, "c", "d"]]
[:or, [:and, "a", "b"], [:and, "c", "d"]]
[:and, [:or, "a", "b"], [:and, "c", "d"]]
[:and, [:or, "a", [:or, [:and, "b", "c"], "d"]], [:and, "c", "d"]]
[:and, "a\\\" b", [:and, "a\\\\", "b\""]]
[:or, "hello", "world"]
[:and, "ABC", "(1, 2)"]
[:and, "-hi", [:not, "hello"]]
[:or, [:not, "hi"], [:not, "hello"]]
[:not, "hi"]
[:or, [:not, "hi"], [:not, "hello"]]
[:and, [:not, "hi"], [:not, "hello"]]
[:and, "HELLO", [:or, [:not, "hi"], [:not, "hello"]]]
[:before, "2011-09-03"]
[:after, "2011-09-03"]
[:and, [:after, "2011-09-03"], [:before, "2011-09-04"]]
変換する
上では treetop のファイルの拡張子を .rb にしていたが、ここでは .treetop にしておく。 そして、次のような Rakefile
task :default => :treetop
desc "Recompile treetop files."
task :treetop do |t|
Dir.glob("**/*.treetop").each do |path|
sh "tt #{path}"
end
end
を作っておき
rake treetop
としておくと treetop のファイルが普通の ruby のファイルに変換される。