Como converter um objeto String em um objeto Hash?

136

Eu tenho uma string que se parece com um hash:

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

Como faço para obter um Hash? gostar:

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

A cadeia pode ter qualquer profundidade de aninhamento. Possui todas as propriedades de como um Hash válido é digitado no Ruby.

Waseem
fonte
Eu acho que eval fará algo aqui. Deixe-me testar primeiro. Eu postei a pergunta muito cedo, eu acho. :)
Waseem
Ohh sim, apenas passe para o eval. :)
Waseem

Respostas:

79

A cadeia criada pela chamada Hash#inspectpode ser transformada novamente em um hash chamando eval-a. No entanto, isso exige o mesmo para todos os objetos no hash.

Se eu começar com o hash {:a => Object.new}, sua representação de string será "{:a=>#<Object:0x7f66b65cf4d0>}"e não posso usá eval-lo para transformá-lo novamente em um hash, porque #<Object:0x7f66b65cf4d0>não é uma sintaxe válida do Ruby.

No entanto, se tudo o que estiver no hash for seqüências de caracteres, símbolos, números e matrizes, deve funcionar, porque elas possuem representações de sequência de caracteres que são válidas na sintaxe Ruby.

Ken Bloom
fonte
"se tudo o que está no hash são strings, símbolos e números". Isso diz muito. Para que eu possa verificar a validade de uma string a ser evalatualizada como um hash, certificando-me de que a instrução acima seja válida para essa string.
Waseem
1
Sim, mas para fazer isso, você precisa de um analisador Ruby completo ou precisa saber de onde veio a string em primeiro lugar e saber que ela só pode gerar strings, símbolos e números. (Ver também a resposta de Toms Mikoss sobre confiar o conteúdo da string.)
Ken Bloom
13
Tenha cuidado onde você usa isso. Usar evalno lugar errado é uma enorme falha de segurança. Qualquer coisa dentro da string será avaliada. Então, imagine se em uma API alguém injetourm -fr
Pithikos
153

Para cadeias diferentes, você pode fazer isso sem usar o evalmétodo perigoso :

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
fonte
2
Esta resposta deve ser selecionada para evitar o uso de eval.
Michael_Zhang 4/18
4
você também deve substituir nils, feJSON.parse(hash_as_string.gsub("=>", ":").gsub(":nil,", ":null,"))
Yo Ludke
136

O método rápido e sujo seria

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

Mas tem implicações graves de segurança.
Ele executa o que quer que seja passado, você deve ter 110% de certeza (como, pelo menos, nenhuma entrada do usuário em qualquer lugar do caminho) que conteria apenas hashes adequadamente formados ou bugs inesperados / criaturas horríveis do espaço sideral podem começar a aparecer.

Toms Mikoss
fonte
16
Eu tenho um sabre de luz comigo. Eu posso cuidar dessas criaturas e insetos. :)
Waseem
12
USAR EVAL pode ser perigoso aqui, de acordo com meu professor. O Eval pega qualquer código ruby ​​e o executa. O perigo aqui é análogo ao perigo de injeção SQL. Gsub é preferível.
boulder_ruby
9
Exemplo de sequência que mostra por que o professor de David está correto: '{: surprise => "# {system \" rm -rf * \ "}"}' '
A. Wilson
13
Não posso enfatizar o perigo de usar EVAL aqui o suficiente! Isso é absolutamente proibido se a entrada do usuário puder entrar na sua string.
Dave Collins
Mesmo que você ache que nunca abrirá isso publicamente, outra pessoa pode. Todos nós (devemos) saber como o código é usado de maneiras que você não esperaria. É como colocar coisas extremamente pesadas em uma prateleira alta, tornando-as pesadas. Você nunca deve criar essa forma de perigo.
Steve Sether
24

Talvez YAML.load?

silencioso
fonte
(método carga suporta cordas)
silencioso
5
Isso requer uma representação de string totalmente diferente, mas muito, muito mais segura. (E a representação da cadeia é tão fácil de gerar - apenas #to_yaml chamada, em vez de #inspect)
Ken Bloom
Uau. Eu não tinha ideia de que era tão fácil analisar cadeias de caracteres com yaml. Ele pega minha cadeia de comandos linux bash que geram dados e os transforma de maneira inteligente em um Hash ruby ​​sem qualquer formato de massagem de string.
labirinto
Isso e to_yaml resolvem meu problema, pois tenho algum controle sobre a maneira como a string é gerada. Obrigado!
mlabarca 26/02
23

Este pequeno trecho curto fará isso, mas não consigo vê-lo trabalhando com um hash aninhado. Eu acho que é bem fofo

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

Etapas 1. Eu elimino o '{', '}' e o ':' 2. Divido a string onde quer que encontre um ',' 3. Divido cada uma das substrings criadas com a divisão, sempre que encontrar a '=>'. Em seguida, crio um hash com os dois lados do hash que acabei de separar. 4. Fico com uma série de hashes que depois mesclo.

EXEMPLO DE ENTRADA: "{: user_id => 11,: blog_id => 2,: comment_id => 1}" RESULTADO DA SAÍDA: {"user_id" => "11", "blog_id" => "2", "comment_id" = > "1"}

hrdwdmrbl
fonte
1
Esse é um oneliner doente! :) +1
blushrt
3
Isso também não removerá os {}:caracteres dos valores dentro do hash string?
Vladimir Panteleev
@VladimirPanteleev Você está certo, seria. Boa pegada! Você pode fazer minhas revisões de código em qualquer dia :)
hrdwdmrbl
20

Até agora, as soluções abrangem alguns casos, mas faltam alguns (veja abaixo). Aqui está minha tentativa de uma conversão mais completa (segura). Conheço um caso de canto que esta solução não lida com símbolos de caracteres únicos compostos por caracteres ímpares, mas permitidos. Por exemplo, {:> => :<}é um hash ruby ​​válido.

Eu coloquei esse código no github também . Esse código começa com uma sequência de teste para exercitar todas as conversões

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)

Aqui estão algumas notas sobre as outras soluções aqui

gene_wood
fonte
Solução muito legal. Você pode adicionar um gsub de tudo :nilpara :nulllidar com isso em particular estranheza.
SteveTurczyn
1
Essa solução também tem o bônus de trabalhar em hashes de vários níveis recursivamente, pois utiliza a análise JSON #. Eu tive alguns problemas ao aninhar outras soluções.
Patrick Leia
17

Eu tive o mesmo problema. Eu estava armazenando um hash em Redis. Ao recuperar esse hash, era uma string. Não quis ligar eval(str)por questões de segurança. Minha solução foi salvar o hash como uma string json, em vez de uma string ruby ​​hash. Se você tiver a opção, usar o json é mais fácil.

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

TL; DR: uso to_jsoneJSON.parse

Jared Menard
fonte
1
Esta é a melhor resposta de longe. to_jsoneJSON.parse
ardochhigh 2/09/17
3
Para quem me rebaixou. Por quê? Eu tive o mesmo problema, tentando converter uma representação de string de um hash rubi em um objeto hash real. Percebi que estava tentando resolver o problema errado. Percebi que resolver a pergunta feita aqui era propenso a erros e inseguro. Percebi que precisava armazenar meus dados de maneira diferente e usar um formato projetado para serializar e desserializar objetos com segurança. TL; DR: Eu tinha a mesma pergunta que OP e percebi que a resposta era fazer uma pergunta diferente. Além disso, se você me fizer voto negativo, forneça feedback para que todos possamos aprender juntos.
Jared Menard
3
O voto negativo sem um comentário explicativo é o câncer do Stack Overflow.
ardochhigh
1
Sim, a redução de votos deve exigir uma explicação e mostrar quem diminui.
Nick Res
2
Para tornar essa resposta ainda mais aplicável à pergunta do OP, se sua representação de string de um hash for chamada 'strungout', você poderá fazer hashit = JSON.parse (strungout.to_json) e selecionar seus itens dentro de hashit via hashit [ 'keyname'] como normal.
precisa saber é o seguinte
11

Eu prefiro abusar do ActiveSupport :: JSON. A abordagem deles é converter o hash em yaml e carregá-lo. Infelizmente, a conversão para yaml não é simples e você provavelmente deseja emprestá-la do AS se ainda não o tiver no seu projeto.

Também precisamos converter quaisquer símbolos em chaves de string regulares, pois os símbolos não são apropriados no JSON.

No entanto, é incapaz de lidar com hashes que contenham uma string de data (nossas strings de data acabam não sendo cercadas por strings, que é onde entra o grande problema):

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

Resultaria em um erro de cadeia JSON inválido ao tentar analisar o valor da data.

Gostaria de sugestões sobre como lidar com este caso

c.apolzon
fonte
2
Obrigado pelo ponteiro para .decode, funcionou muito bem para mim. Eu precisava converter uma resposta JSON para testá-la. Aqui está o código que eu usei:ActiveSupport::JSON.decode(response.body, symbolize_keys: true)
Andrew Philips
9

trabalha nos trilhos 4.1 e suporta símbolos sem aspas {: a => 'b'}

basta adicionar isso à pasta inicializadores:

class String
  def to_hash_object
    JSON.parse(self.gsub(/:([a-zA-z]+)/,'"\\1"').gsub('=>', ': ')).symbolize_keys
  end
end
Eugene
fonte
Obras na linha de comando, mas eu recebo "nível de pilha para profunda" quando eu colocar isso em um intializer ...
Alex Edelstein
2

Criei uma gema hash_parser que primeiro verifica se um hash é seguro ou não está usando ruby_parsergemas. Só então, aplica-se oeval .

Você pode usá-lo 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)

Os testes em https://github.com/bibstha/ruby_hash_parser/blob/master/test/test_hash_parser.rb oferecem mais exemplos das coisas que testei para garantir que a avaliação seja segura.

bibstha
fonte
2

Por favor, considere esta solução. Biblioteca + especificações:

Arquivo 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

Arquivo 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
fonte
1
it "generally works" mas não necessariamente? Eu seria mais detalhado nesses testes. it "converts strings to object" { expect('...').to eql ... } it "supports nested objects" { expect('...').to eql ... }
Lex
Hey @Lex, o que o método faz é descrito em seu comentário do RubyDoc. É melhor não testar o teste, pois ele criará detalhes desnecessários como texto passivo. Assim, "geralmente funciona" é uma boa fórmula para afirmar que essas coisas funcionam bem. Felicidades!
Alex Fortuna
Sim, no final do dia, o que quer que funcione. Qualquer teste é melhor que nenhum teste. Pessoalmente, sou fã de descrições explícitas, mas isso é apenas uma preferência.
Lex
1

Cheguei a essa pergunta depois de escrever uma linha para esse fim, então compartilho meu código para o caso de ajudar alguém. Funciona para uma string com apenas um nível de profundidade e possíveis valores vazios (mas não nulos), como:

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

O código é:

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
fonte
0

Deparamos com um problema semelhante que precisava usar o eval ().

Minha situação, eu estava puxando alguns dados de uma API e gravando-os em um arquivo localmente. Em seguida, é possível extrair os dados do arquivo e usar o Hash.

Eu usei IO.read () para ler o conteúdo do arquivo em uma variável. Nesse caso, IO.read () o cria como uma String.

Em seguida, use eval () para converter a string em um 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

Apenas para mencionar que o IO é um ancestral do File. Portanto, você também pode usar o File.read, se desejar.

TomG
fonte