¿Cómo convierto un objeto String en un objeto Hash?

Tengo una cadena que se parece a un hash:

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

¿Cómo obtengo un Hash? me gusta:

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

La cuerda puede tener cualquier profundidad de anidación. Tiene todas las propiedades de cómo se escribe un Hash válido en Ruby.

La cadena creada al invocar Hash#inspect se puede volver a convertir en hash llamando a eval sobre ella. Sin embargo, esto requiere que lo mismo sea cierto para todos los objetos en el hash.

Si empiezo con el hash {:a => Object.new} , entonces su representación de cadena es "{:a=>#}" , y no puedo usar eval para volver a convertirlo en un hash porque # no es una syntax Ruby válida.

Sin embargo, si todo lo que hay en el hash son cadenas, símbolos, números y matrices, debería funcionar, porque esos tienen representaciones de cadenas que son syntax de Ruby válida.

El método rápido y sucio sería

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

Pero tiene graves implicaciones de seguridad.
Ejecuta lo que sea que pase, debes estar seguro al 110% (como en, al menos, no hay entrada de usuario en el camino) que solo contendría hashes formados correctamente o bichos inesperados / criaturas horribles del espacio exterior podrían comenzar a aparecer.

Para una cadena diferente, puede hacerlo sin utilizar el método de eval peligroso:

 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('=>', ':') 

Este breve pequeño fragmento lo hará, pero no puedo verlo trabajando con un hash nested. Creo que es bastante lindo

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

Pasos 1. Elimino el ‘{‘, ‘}’ y el ‘:’ 2. Participo de la cadena donde encuentre un ‘,’ 3. Divido cada una de las subcadenas que se crearon con la división, cada vez que encuentra a ‘=>’. Luego, creo un hash con los dos lados del hash. Simplemente me separé. 4. Me queda una serie de hash que luego fusiono.

ENTRADA DE EJEMPLO: “{: user_id => 11,: blog_id => 2,: comment_id => 1}” RESULTADO SALIDA: {“user_id” => “11”, “blog_id” => “2”, “comment_id” = > “1”}

Tal vez YAML.load?

Las soluciones hasta ahora cubren algunos casos pero fallan algunos (ver más abajo). Aquí está mi bash de una conversión más completa (segura). Sé de un caso de esquina que esta solución no maneja, que es símbolos de un solo carácter compuestos de caracteres extraños, pero permitidos. Por ejemplo, {:> => :< } es un hash ruby ​​válido.

Puse este código en github también . Este código comienza con una cadena de prueba para ejercitar todas las conversiones

 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) 

Aquí hay algunas notas sobre las otras soluciones aquí

  • Las soluciones de @Ken Bloom y @Toms Mikoss usan eval cual es demasiado aterrador para mí (como señala acertadamente Toms).
  • La solución de @zolter funciona si su hash no tiene símbolos ni teclas numéricas.
  • La solución de @jackquack funciona si no hay cadenas entrecomilladas mezcladas con los símbolos.
  • La solución de @Eugene funciona si sus símbolos no usan todos los caracteres permitidos (los literales de símbolos tienen un conjunto más amplio de caracteres permitidos ).
  • La solución de @Pablo funciona siempre que no tenga una combinación de símbolos y cadenas entre comillas.

Prefiero abusar de ActiveSupport :: JSON. Su enfoque es convertir el hash a yaml y luego cargarlo. Lamentablemente, la conversión a yaml no es simple y es probable que desee tomarla prestada de AS si ya no tiene AS en su proyecto.

También tenemos que convertir cualquier símbolo en cadenas de caracteres regulares ya que los símbolos no son apropiados en JSON.

Sin embargo, es incapaz de manejar hashes que tienen una cadena de fecha en ellos (nuestras cadenas de fechas terminan sin estar rodeadas de cadenas, que es donde entra el gran problema):

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

Ocasionaría un error de cadena JSON no válido cuando intente analizar el valor de la fecha.

Me encantaría cualquier sugerencia sobre cómo manejar este caso

Yo tuve el mismo problema. Estaba almacenando un hash en Redis. Al recuperar ese hash, era una cadena. No quise llamar a eval(str) por cuestiones de seguridad. Mi solución fue guardar el hash como una cadena json en lugar de una cadena de hash de Ruby. Si tienes la opción, usar json es más fácil.

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

TL; DR: use to_json y JSON.parse

funciona en Rails 4.1 y admite símbolos sin comillas {: a => ‘b’}

solo agrega esto a la carpeta de inicializadores:

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

Llegué a esta pregunta después de escribir una línea para este propósito, así que comparto mi código en caso de que ayude a alguien. Funciona para una cadena con solo un nivel de profundidad y posibles valores vacíos (pero no nulos), como:

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

El código es:

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

Construí una gem hash_parser que primero verifica si un hash es seguro o no está usando la gem ruby_parser . Solo entonces, aplica el eval .

Puedes usarlo como

 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) 

Las pruebas en https://github.com/bibstha/ruby_hash_parser/blob/master/test/test_hash_parser.rb le dan más ejemplos de las cosas que he probado para asegurarse de que eval es seguro.

Por favor considere esta solución. Biblioteca + espec .:

Archivo: 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 

Archivo: 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