2009-04-03 13 views
12

tengo esta cadena:¿Cómo puedo tokenizar esta cadena en Ruby?

%{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}] 

yo sepa StringScanner y la Syntax gem pero no puedo encontrar suficientes ejemplos de código para ambos.

¿Alguna sugerencia?

Respuesta

18

Para un lenguaje real, un lexer es el camino a seguir - like Guss said. 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 será suficiente este método - a pesar de que no se llevaría a muchos más complicaciones para hacer que el idioma no sea regular.

Un rápido resumen de la expresión regular:

  • \w+ coincide con las palabras clave de un solo plazo
  • (?:\\.|[^\\"]])* utiliza paréntesis no captura ((?:...)) para que coincida con el contenido de una cadena entre comillas dobles escapado - ya sea un escaparon símbolo (\n, \", \\, etc.) o cualquier carácter que no sea un símbolo de escape o una cita final.
  • "((?:\\.|[^\\"]])*)" capta solo el contenido de una frase de palabra clave entre comillas.
  • (?:(\w+)|"((?:\\.|[^\\"])*)") coincide con cualquier palabra clave - un término o frase, capturando términos individuales en $1 y frase contenido 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 incluirá 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, produciendo una matriz de "coincidencias". Si la expresión regular contiene capturas de parens, una "coincidencia" es una matriz de elementos capturados, por lo que $1 se convierte en match[0], $2 se convierte en match[1], etc.Cualquier paréntesis de captura que no coincida con una parte de la cadena se correlaciona con una entrada nil en la "coincidencia" resultante.

El #map toma estas coincidencias, utiliza un poco de magia de bloque para dividir cada término capturado en diferentes variables (podríamos haber hecho do |match| ; word,phrase,boost = *match), y luego crea los valores hash deseados. Exactamente uno de word o phrase será nil, ya que ambos no pueden coincidir con la entrada, por lo que (word || phrase) devolverá el que no sea nil, y #downcase lo convertirá a minúsculas. boost.to_i convertirá una cadena en un entero, mientras que (boost.nil? ? nil : boost.to_i) asegurará que nil aumente nil.

+0

Si lo estuviera usando en código, probablemente usaría // x y agregaría comentarios en su lugar. – rampion

+0

Si fuera posible, les daría dos votos favorables. 1 para el enfoque pragmático y uno para la expresión regular bien descrita. – slothbear

3

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 utilizar el lexer para generar un analizador recursivo de tu 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 Rubí/analizadores 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.

12

Aquí hay un ejemplo no robusto que usa 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 
Cuestiones relacionadas