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

136

Tengo una cadena que parece 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 cadena puede tener cualquier profundidad de anidamiento. Tiene todas las propiedades de cómo se escribe un Hash válido en Ruby.

Waseem
fuente
Creo que eval hará algo aquí. Déjame probar primero. Publiqué la pregunta demasiado pronto, creo. :)
Waseem
Ohh, sí, solo pásalo a la evaluación. :)
Waseem

Respuestas:

79

La cadena creada al llamar Hash#inspectse puede volver a convertir en un hash llamandoeval . 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=>#<Object:0x7f66b65cf4d0>}", y no puedo usar evalpara volver a convertirlo en un hash porque #<Object:0x7f66b65cf4d0>no es válida la sintaxis de Ruby.

Sin embargo, si todo lo que está en el hash son cadenas, símbolos, números y matrices, debería funcionar, porque tienen representaciones de cadenas que son válidas para la sintaxis de Ruby.

Ken Bloom
fuente
"si todo lo que está en el hash son cadenas, símbolos y números". Esto dice mucho Por lo tanto, puedo verificar la validez de una cadena que se evalevaluará como un hash asegurándome de que la declaración anterior sea válida para esa cadena.
Waseem el
1
Sí, pero para hacer eso necesita un analizador Ruby completo, o necesita saber de dónde vino la cadena en primer lugar y saber que solo puede generar cadenas, símbolos y números. (Véase también la respuesta de Toms Mikoss acerca de confiar en el contenido de la cadena.)
Ken Bloom
13
Tenga cuidado donde usa esto. Usar evalen el lugar equivocado es un gran agujero de seguridad. Cualquier cosa dentro de la cadena, será evaluada. Así que imagina si en una API alguien inyectararm -fr
Pithikos 05 de
153

Para diferentes cadenas, puede hacerlo sin usar un evalmétodo 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('=>', ':')
zolter
fuente
2
Esta respuesta debe seleccionarse para evitar el uso de eval.
Michael_Zhang
44
también debe reemplazar nils, feJSON.parse(hash_as_string.gsub("=>", ":").gsub(":nil,", ":null,"))
Yo Ludke
136

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 se pasa, debe estar 110% seguro (como en, al menos, ninguna entrada del usuario en ningún lugar del camino) contendría solo hashes formados correctamente o errores inesperados / criaturas horribles del espacio exterior podrían comenzar a aparecer.

Toms Mikoss
fuente
16
Tengo un sable de luz conmigo. Puedo cuidar de esas criaturas y bichos. :)
Waseem
12
USAR EVAL puede ser peligroso aquí, según mi maestro. Eval toma cualquier código ruby ​​y lo ejecuta. El peligro aquí es análogo al peligro de inyección SQL. Gsub es preferible.
boulder_ruby
9
Ejemplo de cadena que muestra por qué el maestro de David es correcto: '{: sorpresa => "# {system \" rm -rf * \ "}"}'
A. Wilson
13
¡No puedo enfatizar el PELIGRO de usar EVAL aquí lo suficiente! Esto está absolutamente prohibido si la entrada del usuario puede llegar a su cadena.
Dave Collins
Incluso si crees que nunca abrirás esto más públicamente, alguien más podría hacerlo. Todos (deberíamos) saber cómo se usa el código de formas que no habría esperado. Es como poner cosas extremadamente pesadas en un estante alto, por lo que es muy pesado. Nunca debes crear esta forma de peligro.
Steve Sether
24

Quizás YAML.load?

silencio
fuente
(el método de carga admite cadenas)
silencioso
55
Eso requiere una representación de cadena totalmente diferente, pero es mucho, mucho más segura. (Y la representación de cadena es tan fácil de generar, solo llame al #to_yaml, en lugar de #inspect)
Ken Bloom,
Guau. No tenía idea de que era tan fácil analizar cadenas con yaml. Toma mi cadena de comandos de Linux Bash que generan datos y los convierte de manera inteligente en un ruby ​​Hash sin ningún masaje de formato de cadena.
laberinto
Esto y to_yaml resuelve mi problema ya que tengo cierto control sobre la forma en que se genera la cadena. ¡Gracias!
mlabarca
23

Este pequeño fragmento lo hará, pero no puedo verlo trabajando con un hash anidado. Aunque 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. Me divido en 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 que acabo de separar. 4. Me queda una serie de hashes que luego combino.

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

hrdwdmrbl
fuente
1
¡Esa es una línea enferma! :) +1
blushrt
3
¿Esto tampoco eliminará los {}:caracteres de los valores dentro del hash stringified?
Vladimir Panteleev
@VladimirPanteleev Tienes razón, lo haría. ¡Buena atrapada! Puedes hacer mis revisiones de código cualquier día :)
hrdwdmrbl
20

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

También puse este código en github . 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í.

gene_wood
fuente
Muy buena solución. Se podría añadir un gsub de todos :nila :nullmanejar ese particular, la rareza.
SteveTurczyn
1
Esta solución también tiene la ventaja de trabajar en hashes multinivel de forma recursiva, ya que aprovecha el análisis JSON #. Tuve algunos problemas al anidar en otras soluciones.
Patrick leyó el
17

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

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

TL; DR: uso to_jsonyJSON.parse

Jared Menard
fuente
1
Esta es la mejor respuesta con diferencia. to_jsonyJSON.parse
ardochhigh
3
A quien me rechazó. ¿Por qué? Tuve el mismo problema, tratando de convertir una representación de cadena de un hash ruby ​​en un objeto hash real. Me di cuenta de que estaba tratando de resolver el problema equivocado. Me di cuenta de que resolver la pregunta que se hacía aquí era propenso a errores e inseguro. Me di cuenta de que necesitaba almacenar mis datos de manera diferente y usar un formato diseñado para serializar y deserializar objetos de manera segura. TL; DR: Tenía la misma pregunta que OP, y me di cuenta de que la respuesta era hacer una pregunta diferente. Además, si me rechaza, envíe sus comentarios para que todos podamos aprender juntos.
Jared Menard
3
El voto negativo sin un comentario explicativo es el cáncer de Stack Overflow.
ardochhigh
1
sí, el voto negativo debe requerir una explicación y mostrar quién votó negativamente.
Nick Res
2
Para que esta respuesta sea aún más aplicable a la pregunta del OP, si su representación de cadena de un hash se llama 'strungout', debería poder hacer hashit = JSON.parse (strungout.to_json) y luego seleccionar sus elementos dentro de hashit a través de hashit [ 'nombre clave'] como de costumbre.
cixelsyd
11

Prefiero abusar de ActiveSupport :: JSON. Su enfoque es convertir el hash a yaml y luego cargarlo. Desafortunadamente, la conversión a yaml no es simple y probablemente quieras pedirla prestada a AS si aún no tienes AS en tu proyecto.

También tenemos que convertir cualquier símbolo en teclas de cadena normales ya que los símbolos no son apropiados en JSON.

Sin embargo, es incapaz de manejar los hashes que tienen una cadena de fecha (nuestras cadenas de fecha terminan no 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('=>', ' : '))

Resultaría en un error de cadena JSON no válido cuando intenta analizar el valor de la fecha.

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

c. apolzon
fuente
2
Gracias por el puntero a .decode, funcionó muy bien para mí. Necesitaba convertir una respuesta JSON para probarla. Aquí está el código que usé:ActiveSupport::JSON.decode(response.body, symbolize_keys: true)
Andrew Philips
9

funciona en rails 4.1 y admite símbolos sin comillas {: a => 'b'}

simplemente agregue 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
Eugene
fuente
Funciona en la línea de comando, pero obtengo un "nivel de apilamiento profundo" cuando pongo esto en un inicializador ...
Alex Edelstein
2

Creé una gema hash_parser que primero comprueba si un hash es seguro o no usa ruby_parsergema. 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 garantizar que eval sea seguro.

bibstha
fuente
2

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

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
Alex Fortuna
fuente
1
it "generally works" pero no necesariamente? Sería más detallado en esas pruebas. it "converts strings to object" { expect('...').to eql ... } it "supports nested objects" { expect('...').to eql ... }
Lex
Hola @Lex, qué método hace se describe en su comentario de RubyDoc. Es mejor que la prueba no se repita, creará detalles innecesarios como texto pasivo. Por lo tanto, "generalmente funciona" es una buena fórmula para indicar que las cosas, bueno, generalmente funcionan. ¡Salud!
Alex Fortuna
Sí, al final del día, lo que sea que funcione. Cualquier prueba es mejor que ninguna prueba. Personalmente, soy un fanático de las descripciones explícitas, pero eso es solo una preferencia.
Lex
1

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 codigo 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]}
Pablo
fuente
0

Se encontró con un problema similar que necesitaba usar eval ().

Mi situación, estaba extrayendo algunos datos de una API y escribiéndolos en un archivo localmente. Luego poder extraer los datos del archivo y usar Hash.

Usé IO.read () para leer el contenido del archivo en una variable. En este caso, IO.read () lo crea como una cadena.

Luego usé eval () para convertir la cadena en 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

También solo para mencionar que IO es un antepasado de File. Por lo tanto, también puede usar File.read en su lugar si lo desea.

TomG
fuente