¿Cómo puedo tokenize esta cadena en Ruby?
-
23-08-2019 - |
Pregunta
Tengo esta cadena:
%{Children^10 Health "sanitation management"^5}
Y quiero convertirlo en tokenize esto en una serie de valores hash:
[{:keywords=>"children", :boost=>10}, {:keywords=>"health", :boost=>nil}, {:keywords=>"sanitation management", :boost=>5}]
Soy consciente de StringScanner y la Sintaxis joya pero no puedo encontrar ejemplos de código suficiente para los dos .
Cualquier punteros?
Solución
En un lenguaje real, un analizador léxico es el camino a seguir - como Guss dijo . Pero si el lenguaje completo solamente es tan complicado como su ejemplo, puede utilizar 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 usted está tratando de analizar un lenguaje regular entonces este método será suficiente -. Pesar de que no tomaría muchos más complicaciones para hacer que el lenguaje no regular
A rápido desglose de la expresión regular:
-
\w+
coincide con las palabras clave de un solo plazo - utiliza
(?:\\.|[^\\"]])*
no capturar paréntesis ((?:...)
) para que coincida con el contenido de una cadena entre comillas dobles escapado - ya sea un símbolo escapado (\n
,\"
,\\
, etc.) o con cualquier carácter que no es un símbolo de escape o un fin de la cita . -
"((?:\\.|[^\\"]])*)"
capta sólo el contenido de una frase clave citado. -
(?:(\w+)|"((?:\\.|[^\\"])*)")
coincide con cualquier palabra clave - un término o frase, capturando términos individuales en contenidos y$1
frase en$2
-
\d+
coincide con un número. -
\^(\d+)
captura un número que sigue a un símbolo de intercalación (^
). Dado que este es el tercer juego de la captura de paréntesis, se caputred en$3
. -
(?:\^(\d+))?
captura un número que sigue a un símbolo de intercalación si está allí, coincide con la cadena vacía de otra manera.
String#scan(regex)
coincide con la expresión regular con la cadena tantas veces como sea posible, outputing una serie de "coincidencias". Si la expresión regular contiene parens captura, una "coincidencia" es una serie de artículos capturado - por lo que se convierte en $1
match[0]
, $2
convierte match[1]
, etc. Cualquier paréntesis de captura que no consigue compara con parte de los mapas de cadena a una entrada en el nil
resultante "partido".
El #map
toma entonces estos partidos, utiliza un poco de magia para romper el bloque cada término capturado en distintas variables (que podríamos haber hecho do |match| ; word,phrase,boost = *match
), y luego crea sus hashes deseados. Exactamente una de word
o phrase
se nil
, ya que ambos no puede ser igualada en contra de la entrada, por lo (word || phrase)
devolverá el no nil
uno, y #downcase
lo convertirá a minúsculas. boost.to_i
será convertir una cadena a un entero, mientras que (boost.nil? ? nil : boost.to_i)
asegurará que aumenta nil
permanecen nil
.
Otros consejos
Aquí está un ejemplo no robusto utilizando StringScanner
. Este es el código acabo Adaptado de Rubí Cuestionario:. Analizar 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
Lo que tenemos aquí es una gramática arbitraria, y para analizarlo lo que realmente quiere es un analizador léxico - puede escribir un archivo de gramática que describe la sintaxis y luego utilizar el analizador léxico para generar un analizador recursivo de su gramática.
Escribir un analizador léxico (o incluso un analizador recursivo) no es realmente trivial - aunque es un ejercicio útil en la programación - pero se puede encontrar una lista de lexers / analizadores en este mensaje de correo electrónico aquí Ruby: http://newsgroups.derkeiler.com/Archive/Comp/comp. lang.ruby / 2005-11 / msg02233.html
RACC está disponible como un módulo estándar de Rubí 1.8, por lo que sugiere concentrarse en que, incluso si su manual no es muy fácil de seguir y requiere familiaridad con yacc.