Come faccio a convertire un oggetto String in un oggetto Hash?


137

Ho una stringa che sembra un hash:

"{ :key_a => { :key_1a => 'value_1a', :key_2a => 'value_2a' }, :key_b => { :key_1b => 'value_1b' } }"

Come posso ottenere un hash da esso? piace:

{ :key_a => { :key_1a => 'value_1a', :key_2a => 'value_2a' }, :key_b => { :key_1b => 'value_1b' } }

La stringa può avere qualsiasi profondità di annidamento. Ha tutte le proprietà su come digitare un hash valido in Ruby.


Penso che eval farà qualcosa qui. Fammi provare prima. Ho pubblicato la domanda troppo presto, penso. :)
Waseem,

Oh sì, basta passarlo all'eval. :)
Waseem,

Risposte:


79

La stringa creata chiamando Hash#inspectpuò essere ripristinata in un hash chiamandoeval . Tuttavia, ciò richiede che lo stesso sia vero per tutti gli oggetti nell'hash.

Se comincio con l'hash {:a => Object.new}, la sua rappresentazione in forma di stringa è "{:a=>#<Object:0x7f66b65cf4d0>}"e non posso usarlo evalper trasformarlo in hash perché #<Object:0x7f66b65cf4d0>non è una sintassi di Ruby valida.

Tuttavia, se tutto ciò che è nell'hash sono stringhe, simboli, numeri e matrici, dovrebbe funzionare, poiché quelli hanno rappresentazioni di stringa che sono sintassi di Ruby valida.


"se tutto ciò che è nell'hash sono stringhe, simboli e numeri". Questo dice molto. Quindi posso verificare la validità di una stringa da usare evalcome hash assicurandomi che l'istruzione precedente sia valida per quella stringa.
Waseem,

1
Sì, ma per farlo hai bisogno di un parser Ruby completo, oppure devi sapere da dove proviene la stringa e sapere che può generare solo stringhe, simboli e numeri. (Vedi anche la risposta di Toms Mikoss sulla fiducia nel contenuto della stringa.)
Ken Bloom,

13
Fai attenzione a dove lo usi. Usare evalnel posto sbagliato è un enorme buco di sicurezza. Qualunque cosa all'interno della stringa verrà valutata. Quindi immagina se in un'API qualcuno ha iniettatorm -fr
Pithikos il

153

Per stringhe diverse, puoi farlo senza usare un evalmetodo pericoloso :

hash_as_string = "{\"0\"=>{\"answer\"=>\"1\", \"value\"=>\"No\"}, \"1\"=>{\"answer\"=>\"2\", \"value\"=>\"Yes\"}, \"2\"=>{\"answer\"=>\"3\", \"value\"=>\"No\"}, \"3\"=>{\"answer\"=>\"4\", \"value\"=>\"1\"}, \"4\"=>{\"value\"=>\"2\"}, \"5\"=>{\"value\"=>\"3\"}, \"6\"=>{\"value\"=>\"4\"}}"
JSON.parse hash_as_string.gsub('=>', ':')

2
Questa risposta dovrebbe essere selezionata per evitare di usare eval.
Michael_Zhang,

4
dovresti anche sostituire nils, feJSON.parse(hash_as_string.gsub("=>", ":").gsub(":nil,", ":null,"))
Yo Ludke

136

Il metodo rapido e sporco sarebbe

eval("{ :key_a => { :key_1a => 'value_1a', :key_2a => 'value_2a' }, :key_b => { :key_1b => 'value_1b' } }") 

Ma ha gravi implicazioni per la sicurezza.
Esegue tutto ciò che viene passato, devi essere sicuro al 110% (come in, almeno nessun input dell'utente da nessuna parte lungo la strada) conterrà solo hash correttamente formati o bug imprevisti / creature orribili dallo spazio esterno potrebbero iniziare a spuntare.


16
Ho una sciabola leggera con me. Posso prendermi cura di quelle creature e quei bug. :)
Waseem,

12
UTILIZZARE EVAL può essere pericoloso qui, secondo il mio insegnante. Eval prende qualsiasi codice ruby ​​e lo esegue. Il pericolo qui è analogo al pericolo di iniezione SQL. Gsub è preferibile.
boulder_ruby,

9
Stringa di esempio che mostra perché l'insegnante di David è corretto: '{: surprise => "# {system \" rm -rf * \ "}"}'
A. Wilson,

13
Non posso sottolineare abbastanza il PERICOLO di usare EVAL qui! Questo è assolutamente vietato se l'input dell'utente può mai farsi strada nella tua stringa.
Dave Collins,

Anche se pensi che non lo aprirai mai più pubblicamente, qualcun altro potrebbe. Tutti (dovremmo) sapere come il codice viene utilizzato in modi che non ti saresti aspettato. È come mettere cose estremamente pesanti su uno scaffale alto, rendendolo estremamente pesante. Non dovresti mai creare questa forma di pericolo.
Steve Sether,

24

Forse YAML.load?


(il metodo di caricamento supporta le stringhe)
silenzioso

5
Ciò richiede una rappresentazione di stringa totalmente diversa, ma molto, molto più sicura. (E la rappresentazione della stringa è altrettanto facile da generare - basta chiamare #to_yaml, piuttosto che #inspect)
Ken Bloom,

Wow. Non avevo idea che fosse così facile analizzare le stringhe con yaml. Prende la mia catena di comandi bash di Linux che generano dati e li trasforma in modo intelligente in un hash ruby ​​senza alcun massaggio di formato stringa.
labirinto,

Questo e to_yaml risolve il mio problema dato che ho un certo controllo sul modo in cui viene generata la stringa. Grazie!
mlabarca,

23

Questo breve frammento lo farà, ma non riesco a vederlo lavorare con un hash nidificato. Penso che sia piuttosto carino

STRING.gsub(/[{}:]/,'').split(', ').map{|h| h1,h2 = h.split('=>'); {h1 => h2}}.reduce(:merge)

Passaggi 1. Elimino '{', '}' e ':' 2. Ho diviso la stringa ovunque trovasse un ',' 3. Ho diviso ciascuna delle sottostringhe create con la divisione, ogni volta che trova a '=>'. Quindi, creo un hash con i due lati dell'hash che ho appena diviso. 4. Mi rimane una serie di hash che poi unisco.

INPUT ESEMPIO: "{: user_id => 11,: blog_id => 2,: comment_id => 1}" OUTPUT DEI RISULTATI: {"user_id" => "11", "blog_id" => "2", "comment_id" = > "1"}


1
Questo è un oneliner malato! :) +1
blushrt

3
Questo non toglierà anche i {}:caratteri ai valori all'interno dell'hash rigoroso?
Vladimir Panteleev,

@VladimirPanteleev Hai ragione, lo sarebbe. Bella presa! Puoi fare le mie revisioni del codice in qualsiasi giorno :)
hrdwdmrbl,

20

Le soluzioni finora coprono alcuni casi ma mancano alcuni (vedi sotto). Ecco il mio tentativo di una conversione più approfondita (sicura). Conosco un caso angolare che questa soluzione non gestisce, ovvero i simboli a carattere singolo costituiti da caratteri dispari, ma consentiti. Per esempio{:> => :<} è un hash rubino valido.

Ho inserito questo codice anche su github . Questo codice inizia con una stringa di prova per esercitare tutte le conversioni

require 'json'

# Example ruby hash string which exercises all of the permutations of position and type
# See http://json.org/
ruby_hash_text='{"alpha"=>{"first second > third"=>"first second > third", "after comma > foo"=>:symbolvalue, "another after comma > foo"=>10}, "bravo"=>{:symbol=>:symbolvalue, :aftercomma=>10, :anotheraftercomma=>"first second > third"}, "charlie"=>{1=>10, 2=>"first second > third", 3=>:symbolvalue}, "delta"=>["first second > third", "after comma > foo"], "echo"=>[:symbol, :aftercomma], "foxtrot"=>[1, 2]}'

puts ruby_hash_text

# Transform object string symbols to quoted strings
ruby_hash_text.gsub!(/([{,]\s*):([^>\s]+)\s*=>/, '\1"\2"=>')

# Transform object string numbers to quoted strings
ruby_hash_text.gsub!(/([{,]\s*)([0-9]+\.?[0-9]*)\s*=>/, '\1"\2"=>')

# Transform object value symbols to quotes strings
ruby_hash_text.gsub!(/([{,]\s*)(".+?"|[0-9]+\.?[0-9]*)\s*=>\s*:([^,}\s]+\s*)/, '\1\2=>"\3"')

# Transform array value symbols to quotes strings
ruby_hash_text.gsub!(/([\[,]\s*):([^,\]\s]+)/, '\1"\2"')

# Transform object string object value delimiter to colon delimiter
ruby_hash_text.gsub!(/([{,]\s*)(".+?"|[0-9]+\.?[0-9]*)\s*=>/, '\1\2:')

puts ruby_hash_text

puts JSON.parse(ruby_hash_text)

Ecco alcune note sulle altre soluzioni qui


Soluzione molto interessante. Si potrebbe aggiungere un gsub di tutto :nilper :nullall'impugnatura quel particolare stranezza.
SteveTurczyn,

1
Questa soluzione ha anche il vantaggio di lavorare sugli hash multilivello in modo ricorsivo, poiché sfrutta JSON # parse. Ho avuto qualche problema con la nidificazione su altre soluzioni.
Patrick Leggi il

17

Ho avuto lo stesso problema. Stavo immagazzinando un hash a Redis. Nel recuperare quell'hash, era una stringa. Non volevo chiamare a eval(str)causa di problemi di sicurezza. La mia soluzione era quella di salvare l'hash come stringa json anziché come stringa hash ruby. Se hai l'opzione, usare json è più facile.

  redis.set(key, ruby_hash.to_json)
  JSON.parse(redis.get(key))

TL; DR: usare to_jsoneJSON.parse


1
Questa è di gran lunga la risposta migliore. to_jsoneJSON.parse
ardochhigh il

3
A chiunque mi abbia votato. Perché? Ho avuto lo stesso problema, provando a convertire una rappresentazione in formato stringa di un hash ruby ​​in un vero oggetto hash. Mi sono reso conto che stavo cercando di risolvere il problema sbagliato. Mi sono reso conto che risolvere la domanda posta qui era soggetto a errori e insicuro. Mi sono reso conto che dovevo archiviare i miei dati in modo diverso e utilizzare un formato progettato per serializzare e deserializzare gli oggetti in modo sicuro. TL; DR: ho avuto la stessa domanda di OP, e ho capito che la risposta era di porre una domanda diversa. Inoltre, se mi voti male, ti preghiamo di fornire un feedback in modo che possiamo imparare tutti insieme.
Jared Menard,

3
Il downvoting senza un commento esplicativo è il cancro di Stack Overflow.
Ardochhigh,

1
sì, il downvoting dovrebbe richiedere una spiegazione e mostrare chi è il downvotes.
Nick Res,

2
Per rendere questa risposta ancora più applicabile alla domanda del PO, se la rappresentazione di stringhe di un hash è chiamata "strungout", dovresti essere in grado di eseguire hashit = JSON.parse (strungout.to_json) e quindi selezionare i tuoi elementi all'interno di hash tramite hashit [ "keyname"] come di consueto.
cixelsyd,

11

Preferisco abusare di ActiveSupport :: JSON. Il loro approccio è convertire l'hash in yaml e quindi caricarlo. Sfortunatamente la conversione in yaml non è semplice e probabilmente vorresti prenderla in prestito da AS se non hai già AS nel tuo progetto.

Dobbiamo anche convertire tutti i simboli in normali chiavi di stringa poiché i simboli non sono appropriati in JSON.

Tuttavia, non è in grado di gestire gli hash che contengono una stringa di date (le nostre stringhe di date finiscono per non essere circondate da stringhe, ed è qui che si verifica il grosso problema):

string = '{' last_request_at ': 28-12-2013 23:00:00 UTC}' ActiveSupport::JSON.decode(string.gsub(/:([a-zA-z])/,'\\1').gsub('=>', ' : '))

Provocherebbe un errore stringa JSON non valido quando tenta di analizzare il valore della data.

Gradirei suggerimenti su come gestire questo caso


2
Grazie per il puntatore a .decode, ha funzionato benissimo per me. Ho dovuto convertire una risposta JSON per testarla. Ecco il codice che ho usato:ActiveSupport::JSON.decode(response.body, symbolize_keys: true)
Andrew Philips

9

funziona in rotaie 4.1 e supporta simboli senza virgolette {: a => 'b'}

basta aggiungere questo alla cartella inizializzatori:

class String
  def to_hash_object
    JSON.parse(self.gsub(/:([a-zA-z]+)/,'"\\1"').gsub('=>', ': ')).symbolize_keys
  end
end

Funziona sulla riga di comando, ma ottengo un "livello di stack troppo profondo" quando lo inserisco in un inizializzatore ...
Alex Edelstein,

2

Ho creato un gioiello hash_parser che per primo controlla se un hash è sicuro o non usa ruby_parsergem. Solo allora applica il eval.

Puoi usarlo come

require 'hash_parser'

# this executes successfully
a = "{ :key_a => { :key_1a => 'value_1a', :key_2a => 'value_2a' }, 
       :key_b => { :key_1b => 'value_1b' } }"
p HashParser.new.safe_load(a)

# this throws a HashParser::BadHash exception
a = "{ :key_a => system('ls') }"
p HashParser.new.safe_load(a)

I test in https://github.com/bibstha/ruby_hash_parser/blob/master/test/test_hash_parser.rb ti danno altri esempi delle cose che ho testato per assicurarsi che eval sia sicuro.


2

Si prega di considerare questa soluzione. Biblioteca + spec:

File lib/ext/hash/from_string.rb::

require "json"

module Ext
  module Hash
    module ClassMethods
      # Build a new object from string representation.
      #
      #   from_string('{"name"=>"Joe"}')
      #
      # @param s [String]
      # @return [Hash]
      def from_string(s)
        s.gsub!(/(?<!\\)"=>nil/, '":null')
        s.gsub!(/(?<!\\)"=>/, '":')
        JSON.parse(s)
      end
    end
  end
end

class Hash    #:nodoc:
  extend Ext::Hash::ClassMethods
end

File spec/lib/ext/hash/from_string_spec.rb::

require "ext/hash/from_string"

describe "Hash.from_string" do
  it "generally works" do
    [
      # Basic cases.
      ['{"x"=>"y"}', {"x" => "y"}],
      ['{"is"=>true}', {"is" => true}],
      ['{"is"=>false}', {"is" => false}],
      ['{"is"=>nil}', {"is" => nil}],
      ['{"a"=>{"b"=>"c","ar":[1,2]}}', {"a" => {"b" => "c", "ar" => [1, 2]}}],
      ['{"id"=>34030, "users"=>[14105]}', {"id" => 34030, "users" => [14105]}],

      # Tricky cases.
      ['{"data"=>"{\"x\"=>\"y\"}"}', {"data" => "{\"x\"=>\"y\"}"}],   # Value is a `Hash#inspect` string which must be preserved.
    ].each do |input, expected|
      output = Hash.from_string(input)
      expect([input, output]).to eq [input, expected]
    end
  end # it
end

1
it "generally works" ma non necessariamente? Sarei più prolisso in quei test. it "converts strings to object" { expect('...').to eql ... } it "supports nested objects" { expect('...').to eql ... }
Lex,

Hey @Lex, quale metodo fa è descritto nel suo commento su RubyDoc. È meglio che il test non lo re-dichiari, creerà dettagli non necessari come testo passivo. Pertanto, "generalmente funziona" è una buona formula per affermare che roba, beh, generalmente funziona. Saluti!
Alex Fortuna,

Sì, alla fine, qualunque cosa funzioni. Qualsiasi test è meglio di nessun test. Personalmente sono un fan di descrizioni esplicite, ma questa è solo una preferenza.
Lex,

1

Sono arrivato a questa domanda dopo aver scritto un one-liner per questo scopo, quindi condivido il mio codice nel caso in cui aiuti qualcuno. Funziona per una stringa con un solo livello di profondità e possibili valori vuoti (ma non nulli), come:

"{ :key_a => 'value_a', :key_b => 'value_b', :key_c => '' }"

Il codice è:

the_string = '...'
the_hash = Hash.new
the_string[1..-2].split(/, /).each {|entry| entryMap=entry.split(/=>/); value_str = entryMap[1]; the_hash[entryMap[0].strip[1..-1].to_sym] = value_str.nil? ? "" : value_str.strip[1..-2]}

0

Si è imbattuto in un problema simile che doveva usare eval ().

La mia situazione, stavo estraendo alcuni dati da un'API e scrivendoli in un file localmente. Quindi essere in grado di estrarre i dati dal file e utilizzare l'hash.

Ho usato IO.read () per leggere il contenuto del file in una variabile. In questo caso IO.read () lo crea come una stringa.

Quindi usato eval () per convertire la stringa in un hash.

read_handler = IO.read("Path/To/File.json")

puts read_handler.kind_of?(String) # Returns TRUE

a = eval(read_handler)

puts a.kind_of?(Hash) # Returns TRUE

puts a["Enter Hash Here"] # Returns Key => Values

puts a["Enter Hash Here"].length # Returns number of key value pairs

puts a["Enter Hash Here"]["Enter Key Here"] # Returns associated value

Anche solo per menzionare che IO è un antenato di File. Quindi puoi anche usare File.read se lo desideri.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.