2009-11-03 29 views
101

que tienen una cadena que se parece a un hash:¿Cómo convierto un objeto String en un objeto Hash?

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

¿Cómo consigo un hash de ella? como:

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

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

+0

Creo que eval hará algo aquí. Déjame probar primero. Publiqué la pregunta demasiado pronto, creo. :) – Waseem

+0

Ohh, sí, simplemente pásalo a la evaluación. :) – Waseem

Respuesta

66

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

Si comienzo con el hash {:a => Object.new}, entonces su representación de cadena es "{:a=>#<Object:0x7f66b65cf4d0>}", y no puedo usar eval para convertirlo de nuevo en un hash porque #<Object:0x7f66b65cf4d0> no es válida la sintaxis de Ruby.

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

+0

"si todo lo que hay en el hash es cadenas, símbolos y números". Esto dice mucho. Así que puedo verificar la validez de una cadena para que se evalúe como un hash asegurándome de que la declaración anterior sea válida para esa cadena. – Waseem

+1

Sí, pero para hacer eso, o necesitas un analizador de Ruby completo, o necesitas saber de dónde vino la cadena en primer lugar y saber que solo puede generar cadenas, símbolos y números. (Consulte también la respuesta de Toms Mikoss acerca de la confianza en el contenido de la cadena). –

+6

Sea cuidadoso en donde usa esto. Usar 'eval' en el lugar incorrecto es un gran agujero de seguridad. Cualquier cosa dentro de la cadena, será evaluada. Así que imagínese si en una API alguien inyectó 'rm -fr' – Pithikos

116

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 implicaciones de seguridad graves.
Ejecuta todo lo que pasa, debe 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.

+10

Tengo un sable de luz conmigo. Puedo cuidar de esas criaturas y errores. :) – Waseem

+10

EL USO DE EVAL puede ser peligroso aquí, según mi profesor. Eval toma cualquier código ruby ​​y lo ejecuta. El peligro aquí es análogo al peligro de inyección de SQL. Gsub es preferible. –

+6

Ejemplo de cadena que muestra por qué el profesor de David es correcto: '{: sorpresa => "# {sistema \" rm -rf * \ "}}} –

17

Quizás YAML.load?

+0

(el método de carga admite cadenas) – silent

+4

Eso requiere una representación de cadenas totalmente diferente, pero es mucho, mucho más seguro. (Y la representación de la cadena es igual de fácil de generar: simplemente llame a #to_yaml, en lugar de a #inspect) –

+0

Wow. No tenía idea de que era tan fácil analizar cadenas con yaml. Toma mi cadena de comandos linux bash que generan datos y los convierte inteligentemente en un hash ruby ​​sin ningún formato de cadena de masaje. – labyrinth

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. Trabaja para una cadena con sólo un nivel de profundidad y los posibles valores vacíos (pero no cero), 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]} 
11

prefiero abusar 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 normales ya que los símbolos no son apropiados en JSON.

Sin embargo, su incapaces de manejar los hashes que tienen una cadena de fecha en ellos (nuestros cadenas de fecha terminan por no estar rodeado de cuerdas, que es donde entra en juego el gran problema):

cadena = '{' 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 alguna sugerencia sobre cómo manejar este caso

+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 utilicé: 'ActiveSupport :: JSON.decode (response.body, symbolize_keys: true)' –

19

Este breve pequeño fragmento lo hará, pero no puedo ver su funcionamiento con un resumen anidada.Creo que es muy lindo, aunque

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

Pasos 1. eliminar el '{', '}' y la ':' 2. divididos sobre la cuerda donde quiera que encuentre un '' 3. Divido cada una de las subcadenas que se crearon con la división, cada vez que encuentra un '=>'. Luego, creo un hash con los dos lados del hash. Simplemente me separé. 4. Me quedan una serie de hash que luego fusiono.

Ejemplo de entrada: "{: user_id => 11,: blog_id => 2,: comment_id => 1}" resultado de salida: { "user_id" => "11", "blog_id" => "2" "comment_id" => "1"}

+1

¡Ese es un oneliner enfermo! :) +1 – blushrt

+2

¿Esto no eliminará también los caracteres '{}:' de * values ​​* dentro del hash stringificado? –

88

Para cadena diferente, puede hacerlo sin necesidad de utilizar peligrosa eval método:

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

obras en los carriles 4.1 y símbolos de apoyo 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 
+0

Funciona en la línea de comandos, pero obtengo "nivel de pila a profundidad" cuando puse esto en un inicializador ... –

14

Las soluciones cubren hasta ahora algunos casos pero fallan algunos (ver a continuación). 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 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 code up on github as well. Este código se inicia con una cadena de prueba para ejercer 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í

+0

Solución muy buena. Podrías agregar un gsub de todo ': nil' a': null' para manejar esa rareza particular. – SteveTurczyn

+0

Esta solución también tiene la ventaja de trabajar en hash multinivel recursivamente, ya que aprovecha el análisis JSON #. Tuve algunos problemas con anidar en otras soluciones. –

0

Considere esta solución. Biblioteca + especificaciones:

del 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"}') 
     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 

del archivo: spec/lib/ext/hash/from_string_spec.rb:

require "ext/hash/from_string" 
require "rspec/match_result" # Get from https://github.com/dadooda/rspec_match_result. 

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| 
     match_result(input, expected) {|input| Hash.from_string(input)} 
    end 
    end # it 
end 
7

que tenían el mismo problema. Estaba almacenando un hash en Redis. Al recuperar ese hash, era una cadena. No quise llamar al 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 rubí.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: to_json uso y JSON.parse

+0

Esta es la mejor respuesta con diferencia. '' 'to_json''' y' '' JSON.parse''' – ardochhigh

+2

A quien me votó negativamente. ¿Por qué? Tuve el mismo problema, tratando de convertir una representación de cadena de un hash de rubí 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 forma 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 votas negativamente, envía tus comentarios para que todos podamos aprender juntos. –

+1

Downvoting sin un comentario explicativo es el cáncer de Stack Overflow. – ardochhigh

0

he construido una joya hash_parser que primero comprueba si un hash es seguro o no usando ruby_parser joya. Solo entonces, aplica el eval.

Se puede utilizar 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 es seguro eval.

Cuestiones relacionadas