scan - ¿Cómo puedo tokenizar esta cadena en Ruby?
scan ruby example (3)
Aquí hay un ejemplo no robusto usando StringScanner
. Este es el código que acabo de adaptar de Ruby Quiz: Parsing JSON , que tiene una excelente explicación.
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
Tengo esta cadena:
%{Children^10 Health "sanitation management"^5}
Y quiero convertirlo para convertir esto en una matriz de valores hash:
[{:keywords=>"children", :boost=>10}, {:keywords=>"health", :boost=>nil}, {:keywords=>"sanitation management", :boost=>5}]
Conozco StringScanner y la gema de sintaxis pero no puedo encontrar suficientes ejemplos de código para ambos.
¿Alguna sugerencia?
Lo que tienes aquí es una gramática arbitraria, y para analizarlo, lo que realmente quieres es un lexer: puedes escribir un archivo de gramática que describa tu sintaxis y luego usar el lexer para generar un analizador recursivo a partir de tu gramática.
Escribir un lexer (o incluso un analizador recursivo) no es realmente trivial, aunque es un ejercicio útil de programación, pero puede encontrar una lista de lexers / analizadores de Ruby en este mensaje de correo electrónico aquí: http://newsgroups.derkeiler.com /Archive/Comp/comp.lang.ruby/2005-11/msg02233.html
RACC está disponible como un módulo estándar de Ruby 1.8, por lo que le sugiero que se concentre en eso, incluso si su manual no es realmente fácil de seguir y requiere familiaridad con yacc.
Para un lenguaje real, un lexer es el camino a seguir, como dijo Guss . Pero si el lenguaje completo es tan complicado como su ejemplo, puede usar este truco rápido:
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"}]
Si está tratando de analizar un idioma normal, este método será suficiente, aunque no tomaría muchas más complicaciones para que el idioma no sea regular.
Un rápido desglose de la expresión regular:
-
/w+
coincide con palabras clave de un solo término -
(?://.|[^//"]])*
usa paréntesis que no se capturan ((?:...)
) para hacer coincidir el contenido de una cadena de texto doble escapada, ya sea un símbolo de escape (/n
,/"
,//
, etc.) o cualquier carácter que no sea un símbolo de escape o una cita final. -
"((?://.|[^//"]])*)"
captura solo el contenido de una frase de palabras clave entre comillas. -
(?:(/w+)|"((?://.|[^//"])*)")
coincide con cualquier palabra clave - término o frase individual, capturando términos individuales en$1
y contenidos de frases en$2
-
/d+
coincide con un número. -
/^(/d+)
captura un número siguiendo un símbolo de intercalación (^
). Como este es el tercer conjunto de paréntesis de captura, se convertirá en$3
. -
(?:/^(/d+))?
captura un número siguiendo un símbolo de intercalación si está allí, coincide con la cadena vacía de lo contrario.
String#scan(regex)
coincide con la expresión regular contra la cadena tantas veces como sea posible, generando una matriz de "coincidencias". Si la expresión regular contiene capturar parens, una "coincidencia" es una matriz de elementos capturados, por lo que $1
convierte en match[0]
, $2
convierte en match[1]
, etc. Cualquier paréntesis de captura que no match[1]
con parte de los mapas de cadena a una entrada nil
en el "partido" resultante.
El #map
luego toma estas coincidencias, utiliza un poco de magia de bloques para dividir cada término capturado en diferentes variables (podríamos haber hecho do |match| ; word,phrase,boost = *match
), y luego crea los hashes deseados. Exactamente uno de word
o phrase
será nil
, ya que ambos no se pueden comparar con la entrada, por lo que (word || phrase)
devolverá el no- nil
, y #downcase
lo convertirá a minúsculas. boost.to_i
convertirá una cadena en un entero mientras (boost.nil? ? nil : boost.to_i)
asegurará que los nil
boosts permanezcan nil
.