Come faccio a tokenize questa stringa in Ruby?
-
23-08-2019 - |
Domanda
Non ho questa stringa:
%{Children^10 Health "sanitation management"^5}
E voglio convertirlo in tokenize questo in un array di hash:
[{:keywords=>"children", :boost=>10}, {:keywords=>"health", :boost=>nil}, {:keywords=>"sanitation management", :boost=>5}]
Sono consapevole del StringScanner ed il Sintassi gioiello ma non riesco a trovare esempi di codice abbastanza per entrambi .
Tutti gli indicatori?
Soluzione
Per un vero e proprio linguaggio, un lexer è la strada da percorrere - come Guss detto . Ma se la lingua è completo solo così complicato come il tuo esempio, è possibile utilizzare questo trucco veloce:
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"}]
Se si sta cercando di analizzare un linguaggio regolare, allora questo metodo sarà sufficiente -. Anche se non vorrebbero molti più complicazioni per rendere il linguaggio non regolare
Una rapida ripartizione del regex:
-
\w+
soddisfa le eventuali parole chiave singola termine -
(?:\\.|[^\\"]])*
utilizza non cattura parentesi ((?:...)
) per abbinare il contenuto di una stringa doppia citato sfuggito - sia un simbolo di escape (\n
,\"
,\\
, etc.) o qualsiasi singolo carattere che non è un simbolo di fuga o un preventivo fine . -
"((?:\\.|[^\\"]])*)"
cattura solo il contenuto di una frase chiave citata. -
(?:(\w+)|"((?:\\.|[^\\"])*)")
corrisponde una qualsiasi parola chiave - singolo termine o una frase, catturando singoli termini in$1
e frase contenuto in$2
-
\d+
corrisponde a un numero. -
\^(\d+)
cattura un numero che segue un accento circonflesso (^
). Dal momento che questo è il terzo set di catturare parentesi, sarà caputred in$3
. -
(?:\^(\d+))?
cattura un numero che segue un accento circonflesso se è lì, corrisponde alla stringa vuota altrimenti.
String#scan(regex)
corrisponde alla regex contro la stringa come numero di volte possibile, outputing una serie di "incontri". Se l'espressione regolare contiene parentesi cattura, un "match" è un array di oggetti catturati - così $1
diventa match[0]
, $2
diventa match[1]
, ecc Qualsiasi parentesi di cattura che non viene confrontata con una parte delle mappe di stringa a una voce nel nil
con conseguente "match".
Il #map
poi prende queste partite, usa un po 'di magia a blocchi di rompere ogni termine catturato in variabili differenti (avremmo potuto fare do |match| ; word,phrase,boost = *match
), e quindi crea i tuoi hash desiderati. Esattamente uno di word
o phrase
saranno nil
, poiché entrambi non possono essere confrontati con l'ingresso, in modo (word || phrase)
restituirà il non-nil
uno e #downcase
convertirà in lettere minuscole. boost.to_i
convertirà una stringa in un intero, mentre (boost.nil? ? nil : boost.to_i)
garantirà che aumenta nil
rimanere nil
.
Altri suggerimenti
Ecco un esempio non robusta utilizzando StringScanner
. Si tratta di codice che ho appena adattato da Rubino Quiz:. Analisi JSON , che ha un'eccellente spiegazione
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
Quello che avete qui è una grammatica arbitraria, e di analizzarlo cosa si vuole veramente è un lexer - è possibile scrivere un file di grammatica che descrive la sintassi e quindi utilizzare il lexer per generare un parser ricorsivo dalla grammatica.
Scrivi lexer (o anche un parser ricorsivo) non è realmente banale - anche se è un esercizio utile nella programmazione - ma è possibile trovare un elenco di Ruby lexer / parser in questo messaggio e-mail qui: http://newsgroups.derkeiler.com/Archive/Comp/comp. lang.ruby / 2005-11 / msg02233.html
RACC è disponibile come modulo standard di Ruby 1.8, quindi vi consiglio di concentrarsi su che, anche se il suo manuale non è davvero facile da seguire e richiede familiarità con Yacc.