如何将一个String对象转换为Hash对象?

169
我有一个看起来像哈希值的字符串:
"{ :key_a => { :key_1a => 'value_1a', :key_2a => 'value_2a' }, :key_b => { :key_1b => 'value_1b' } }"

我怎样从中获取哈希值?就像这样:

{ :key_a => { :key_1a => 'value_1a', :key_2a => 'value_2a' }, :key_b => { :key_1b => 'value_1b' } }
字符串可以嵌套任意深度。它具有在Ruby中键入有效哈希的所有属性。

我觉得在这里使用eval会有所作用。让我先测试一下。我想我发问题的时机可能太早了。 :) - Waseem
哦,是的,只需将它传递给 eval。 :) - Waseem
16个回答

213

对于不同的字符串,你可以在没有使用危险的eval方法的情况下完成它:

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

3
这个答案应该被选中,因为它避免了使用eval函数。 - Michael_Zhang
7
你还应该替换 nils,例如 JSON.parse(hash_as_string.gsub("=>", ":").gsub(":nil,", ":null,")) - Yo Ludke
1
@YoLudke的回复是个好主意,但它只会替换后面跟着逗号的nil值,所以如果你的哈希以nil值结尾,它就会出错。使用单词边界更加灵活:JSON.parse(hash_as_string.gsub("=>", ":").gsub(/\bnil\b/, "null")) - take

155

快速且简单的方法是

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

但它具有严重的安全隐患。
它会执行任何传入的内容,你必须要做到110%的确定(至少在整个过程中没有用户输入),它只包含正确格式的哈希值,否则意外的bug/可怕的外星生物可能会开始出现。


17
我有一把光剑,可以处理那些生物和虫子。 :) - Waseem
12
根据我的老师说,这里使用 EVAL 可能会很危险。EVAL 会执行任何 Ruby 代码。这里的危险性类似于 SQL 注入的危险性。更好的选择是使用 GSUB。 - boulder_ruby
11
展示为什么 David 的老师是正确的例子字符串:'{:surprise => "#{system "rm -rf * "}"}' - A. Wilson
16
我无法强调在此处使用 EVAL 的危险性足够了!如果用户输入可以进入您的字符串,那么这是绝对禁止的。 - Dave Collins
即使你认为你永远不会将其更公开地打开,但其他人可能会这样做。我们都(应该)知道代码被使用的方式超出了你的预期。这就像把极重的东西放在高架上,使它变得顶重。你永远不应该创造这种危险形式。 - Steve Sether
@Waseem,光剑无法保护您免受病毒、恶意软件、黑客等的侵害。 - Sapphire_Brick

86
调用Hash#inspect方法生成的字符串可以通过调用eval方法转换回哈希表。但是,这要求哈希表中所有对象都满足这个条件。
如果我从哈希表{:a => Object.new}开始,它的字符串表示形式为"{:a=>#<Object:0x7f66b65cf4d0>}",我不能使用eval将其转换回哈希表,因为#<Object:0x7f66b65cf4d0>不是有效的Ruby语法。
然而,如果哈希表中只有字符串、符号、数字和数组,那么应该可以正常工作,因为这些对象都有有效的Ruby语法的字符串表示形式。

如果哈希表中只包含字符串、符号和数字,这就说明了很多问题。因此,我可以通过确保上述语句对该字符串有效来检查其作为哈希表进行“eval”评估的有效性。 - Waseem
1
是的,但要做到这一点,您需要一个完整的Ruby解析器,或者您需要知道字符串首先来自哪里,并知道它只能生成字符串、符号和数字。(请参阅Toms Mikoss有关信任字符串内容的答案。) - Ken Bloom
20
在使用此功能时需要小心,在错误的地方使用eval会造成巨大的安全风险。字符串中的任何内容都将被计算。因此,想象一下如果有人在 API 中注入了 rm -fr - Pithikos

34

我遇到了同样的问题。我在Redis中存储了一个哈希表,但在检索它时,返回的是一个字符串。由于安全原因,我不想调用eval(str)函数。我的解决方法是将哈希表保存为JSON字符串而不是Ruby哈希表字符串。如果可以选择,使用JSON更加容易。

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

简洁来说:使用to_jsonJSON.parse


3
这是目前为止最好的答案。to_jsonJSON.parse - port5432
3
给那些踩我赞的人。为什么?我曾经遇到过同样的问题,尝试将 Ruby 哈希结构的字符串表示转换为实际的哈希对象。我意识到我试图解决错误的问题。我认识到在这里回答所提出的问题是容易出错和不安全的。我认识到我需要以不同的方式存储我的数据,并使用一种旨在安全地序列化和反序列化对象的格式。简而言之,我曾经有与原帖相同的问题,并意识到答案是提出一个不同的问题。另外,如果您给我负面评价,请提供反馈,这样我们就可以共同学习。 - Jared Menard
3
在没有解释性评论的情况下进行负评是Stack Overflow的毒瘤。 - port5432
2
为了使这个答案更适用于OP的问题,如果您的哈希字符串表示被称为“strungout”,那么您应该能够执行hashit = JSON.parse(strungout.to_json),然后通过hashit ['keyname']正常选择哈希内的项目。 - cixelsyd
1
这个完美运行,谢谢。 - Vinirdishtith Rana
显示剩余3条评论

27

也许是使用 YAML.load ?


1
(load方法支持字符串) - silent
5
需要完全不同的字符串表示方式,但更加安全。 (字符串表示方式生成方式也很简单-只需调用#to_yaml,而不是#inspect) - Ken Bloom
哇,我从来不知道使用yaml解析字符串是如此容易。它可以将我的一系列生成数据的Linux bash命令智能地转换为Ruby哈希表,而无需进行任何字符串格式调整。 - labyrinth
这个和 to_yaml 解决了我的问题,因为我可以控制字符串生成的方式。谢谢! - mlabarca

26
迄今为止的解决方案覆盖了一些情况,但也存在一些遗漏(见下文)。以下是我更彻底(安全)转换的尝试。我知道这个解决方案无法处理一个特殊情况,即由奇数个允许字符组成的单个字符符号。例如,{:> => :<} 是一个有效的 Ruby 哈希表。
我还将这段代码放在 GitHub 上。这段代码从一个测试字符串开始,以测试所有的转换。
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)

以下是关于其他解决方案的一些注意事项


非常棒的解决方案。您可以添加一个gsub,将所有的:nil替换为:null来处理这种特殊情况。 - SteveTurczyn
1
这个解决方案的额外好处是可以递归地处理多级哈希,因为它利用了JSON#parse。在其他解决方案中,我遇到了一些嵌套问题。 - Patrick Read
发现正则表达式有点难以理解,因此创建了一个Gist,并添加了一些测试用例。https://gist.github.com/akagr/0339fb80f1b268a48a43ffbd1606cb3b谢谢回答! - Akash

25

这个简短的代码片段可以实现它,但我认为它在嵌套哈希表中无法正常工作。不过我觉得它还是很可爱的。

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

步骤 1. 我删除'{', '}'和':' 2. 我在字符串上查找逗号进行拆分 3. 对于每个子字符串,我在发现 '=>' 时进行拆分。然后,我用我刚刚拆分的哈希两侧创建一个哈希。 4. 我得到一个哈希数组,然后将它们合并在一起。

例子输入:"{:user_id=>11, :blog_id=>2, :comment_id=>1}" 结果输出:{"user_id" => "11", "blog_id" => "2", "comment_id" => "1"}


1
这是一个非常棒的单行代码!:) +1 - Simon Polak
3
这样做会不会也从字符串化的哈希值中删除值内的{}:字符? - Vladimir Panteleev
@VladimirPanteleev 你说得对,这样做会更好。很棒的发现!你可以随时帮我审查代码 :) - hrdwdmrbl

12

在Rails 4.1中可以使用没有引号的符号{:a => 'b'},只需将以下内容添加到initializers文件夹中:

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

在命令行上可以工作,但是当我将其放入初始化器中时,会出现“堆栈级别过深”的错误。 - Alex Edelstein

12

我倾向于滥用ActiveSupport :: JSON。他们的方法是将哈希转换为yaml,然后加载它。不幸的是,转换为yaml并不简单,如果您的项目中没有AS,则可能需要从AS中借鉴。

我们还必须将任何符号转换为常规字符串键,因为符号不适合JSON。

但是,它无法处理其中包含日期字符串的哈希(我们的日期字符串最终不会被包裹在字符串中,这就是问题所在):

字符串 = '{'last_request_at' : 2011-12-28 23:00:00 UTC }' ActiveSupport :: JSON.decode(string.gsub(/:([a-zA-z])/,'\\ 1')。gsub('=&gt;',':'))

当尝试解析日期值时,将导致无效的JSON字符串错误。

非常希望对如何处理此情况有任何建议。


2
感谢您指出.decode方法,对我非常有帮助。我需要将JSON响应转换为测试数据。这是我使用的代码:ActiveSupport::JSON.decode(response.body, symbolize_keys: true) - Andrew Philips

4
请考虑这个解决方案。图书馆+规范:
文件: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

文件: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" but not necessarily? I would be more verbose in those tests.it "converts strings to object" { expect('...').to eql ... }it "supports nested objects" { expect('...').to eql ... } - Lex
嘿@Lex,RubyDoc注释中描述了什么方法。测试最好不要重新陈述它,这会创建不必要的被动文本细节。因此,“通常工作”是一个不错的公式来说明这个东西,嗯,通常工作。干杯! - Alex Fortuna
是的,说到底,任何有效的测试都比没有测试好。个人而言,我喜欢明确的描述,但那只是一种偏好。 - Lex

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接