【问题标题】:How do I convert a String object into a Hash object?如何将 String 对象转换为 Hash 对象?
【发布时间】:2010-12-12 15:55:21
【问题描述】:

我有一个看起来像哈希的字符串:

"{ :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 会在这里做点什么。让我先测试一下。我认为我发布的问题太早了。 :)
  • 哦,是的,只需将其传递给 eval。 :)

标签: ruby


【解决方案1】:

对于不同的字符串,你可以不使用危险的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('=>', ':')

【讨论】:

  • 应该选择这个答案以避免使用eval。
  • 你也应该替换 nils, f.e. JSON.parse(hash_as_string.gsub("=>", ":").gsub(":nil,", ":null,"))
【解决方案2】:

快速而肮脏的方法是

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

但它具有严重的安全隐患。
它执行传递的任何内容,您必须 110% 确定(例如,至少在此过程中任何地方都没有用户输入)它只会包含正确形成的哈希,否则来自外太空的意外错误/可怕的生物可能会开始弹出。

【讨论】:

  • 我有一把光剑。我可以照顾那些生物和虫子。 :)
  • 根据我的老师的说法,在这里使用 EVAL 可能很危险。 Eval 接受任何 ruby​​ 代码并运行它。这里的危险类似于 SQL 注入危险。 Gsub 更可取。
  • 显示大卫老师为什么正确的示例字符串:'{:surprise => "#{system \"rm -rf * \"}"}'
  • 我不能强调在这里使用 EVAL 的危险!如果用户输入可以进入您的字符串,这是绝对禁止的。
  • 即使你认为你永远不会更公开地公开它,其他人可能会这样做。我们都(应该)知道代码是如何以您意想不到的方式使用的。这就像把非常重的东西放在一个高架子上,让它变得头重脚轻。你不应该制造这种形式的危险。
【解决方案3】:

通过调用Hash#inspect 创建的字符串可以通过调用eval 转换回散列。但是,这要求哈希中的所有对象都相同。

如果我以散列 {:a => Object.new} 开头,那么它的字符串表示是 "{:a=>#<Object:0x7f66b65cf4d0>}",我不能使用 eval 将其转回散列,因为 #<Object:0x7f66b65cf4d0> 不是有效的 Ruby 语法。

但是,如果哈希中的所有内容都是字符串、符号、数字和数组,它应该可以工作,因为它们具有有效的 Ruby 语法的字符串表示。

【讨论】:

  • "如果哈希中的所有内容都是字符串、符号和数字,"。这说明了很多。因此,我可以通过确保上述语句对该字符串有效来检查字符串的有效性,以将evaluated 作为哈希。
  • 是的,但要做到这一点,您要么需要一个完整的 Ruby 解析器,要么首先需要知道字符串的来源,并且知道它只能生成字符串、符号和数字。 (另请参阅 Toms Mikoss 关于信任字符串内容的回答。)
  • 小心使用它。在错误的地方使用eval 是一个巨大的安全漏洞。字符串中的任何内容都将被评估。所以想象一下,如果有人在 API 中注入了rm -fr
【解决方案4】:

也许是 YAML.load ?

【讨论】:

  • (加载方法支持字符串)
  • 这需要完全不同的字符串表示,但它要安全得多。 (而且字符串表示同样容易生成——只需调用#to_yaml,而不是#inspect)
  • 哇。我不知道用 yaml 解析字符串是如此容易。它需要我的 linux bash 命令链来生成数据并智能地将其转换为 ruby​​ 哈希,无需任何字符串格式的按摩。
  • 这和 to_yaml 解决了我的问题,因为我可以控制字符串的生成方式。谢谢!
【解决方案5】:

我遇到了同样的问题。我在 Redis 中存储了一个哈希值。检索该哈希时,它是一个字符串。出于安全考虑,我不想打电话给eval(str)。我的解决方案是将哈希保存为 json 字符串而不是 ruby​​ 哈希字符串。如果可以选择,使用 json 会更容易。

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

TL;DR:使用 to_jsonJSON.parse

【讨论】:

  • 这是迄今为止最好的答案。 to_jsonJSON.parse
  • 致那些反对我的人。为什么?我遇到了同样的问题,试图将 ruby​​ 哈希的字符串表示形式转换为实际的哈希对象。我意识到我试图解决错误的问题。我意识到解决这里提出的问题容易出错且不安全。我意识到我需要以不同的方式存储我的数据并使用一种旨在安全地序列化和反序列化对象的格式。 TL;DR:我和 OP 有同样的问题,并意识到答案是问一个不同的问题。另外,如果您对我投了反对票,请提供反馈,以便我们一起学习。
  • 在没有解释性评论的情况下拒绝投票是 Stack Overflow 的毒瘤。
  • 是的,投反对票应该需要解释并显示谁投反对票。
  • 为了使这个答案更适用于 OP 的问题,如果您的哈希字符串表示称为“strungout”,您应该能够执行 hashit = JSON.parse(strungout.to_json) 然后像往常一样通过 hashit['keyname'] 在 hashit 中选择你的项目。
【解决方案6】:

这个简短的小 sn-p 可以做到,但我看不到它与嵌套哈希一起工作。不过我觉得还是蛮可爱的

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

【讨论】:

  • 那是一个病态的oneliner! :) +1
  • 这不会也从字符串化散列中的 values 中删除 {}: 字符吗?
  • @VladimirPanteleev 你是对的,它会的。不错的收获!您可以随时进行我的代码审查 :)
【解决方案7】:

到目前为止,解决方案涵盖了一些情况,但遗漏了一些情况(见下文)。这是我进行更彻底(安全)转换的尝试。我知道这个解决方案无法处理的一个极端情况,它是由奇数但允许的字符组成的单字符符号。例如,{:> => :<} 是一个有效的 ruby​​ 哈希。

我把这个code up on github as well。此代码以一个测试字符串开始,以执行所有转换

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)

这里有一些关于其他解决方案的说明

【讨论】:

  • 非常酷的解决方案。您可以将所有 :nil 的 gsub 添加到 :null 来处理这种特殊的怪异现象。
  • 这个解决方案还具有递归处理多级哈希的好处,因为它利用了 JSON#parse。我在嵌套其他解决方案时遇到了一些麻烦。
  • 发现正则表达式有点难以理解,因此为此创建了一些测试用例的要点。 gist.github.com/akagr/0339fb80f1b268a48a43ffbd1606cb3b谢谢回答!
【解决方案8】:

我更喜欢滥用 ActiveSupport::JSON。他们的方法是将哈希转换为 yaml,然后加载。不幸的是,转换为 yaml 并不简单,如果您的项目中还没有 AS,您可能想从 AS 借用它。

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

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

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

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

希望有任何关于如何处理此案的建议

【讨论】:

  • 感谢指向 .decode 的指针,它对我很有用。我需要转换一个 JSON 响应来测试它。这是我使用的代码:ActiveSupport::JSON.decode(response.body, symbolize_keys: true)
【解决方案9】:

在 Rails 4.1 中工作并支持不带引号的符号 {:a => 'b'}

只需将其添加到初始化程序文件夹:

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

【讨论】:

  • 在命令行上工作,但是当我把它放在初始化程序中时,我得到“堆栈级别到深”......
【解决方案10】:

请考虑这个解决方案。库+规范:

文件: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

【讨论】:

  • it "generally works" 但不一定?在这些测试中我会更加冗长。 it "converts strings to object" { expect('...').to eql ... }it "supports nested objects" { expect('...').to eql ... }
  • 嘿@Lex,它的 RubyDoc 注释中描述了方法的作用。测试最好不要重新陈述它,它会创建不必要的细节作为被动文本。因此,“通常有效”是一个很好的公式来说明这些东西,嗯,通常有效。干杯!
  • 是的,归根结底,不管怎样都行。任何测试都比没有测试好。就我个人而言,我喜欢明确的描述,但这只是一种偏好。
【解决方案11】:

我构建了一个 gem hash_parser,它首先使用 ruby_parser gem 检查哈希是否安全。只有这样,它才会应用eval

你可以把它当作

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)

https://github.com/bibstha/ruby_hash_parser/blob/master/test/test_hash_parser.rb 中的测试为您提供了我为确保 eval 安全而测试的更多示例。

【讨论】:

    【解决方案12】:

    此方法适用于一级深度哈希

    
      def convert_to_hash(str)
        return unless str.is_a?(String)
    
        hash_arg = str.gsub(/[^'"\w\d]/, ' ').squish.split.map { |x| x.gsub(/['"]/, '') }
        Hash[*hash_arg]
      end
    

    例子

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

    【讨论】:

      【解决方案13】:

      我在为此目的编写了一个单行代码后提出了这个问题,所以我分享我的代码以防它对某人有所帮助。适用于只有一级深度和可能为空值(但不是 nil)的字符串,例如:

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

      代码是:

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

      【讨论】:

        【解决方案14】:

        遇到需要使用 eval() 的类似问题。

        我的情况是,我从 API 中提取一些数据并将其写入本地文件。然后能够从文件中提取数据并使用哈希。

        我使用 IO.read() 将文件的内容读入一个变量。在这种情况下,IO.read() 将其创建为字符串。

        然后使用 eval() 将字符串转换为 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
        

        还只是提到 IO 是 File 的祖先。因此,您也可以根据需要使用 File.read。

        【讨论】:

          【解决方案15】:

          这是一种使用whitequark/parser 的方法,它比gsubeval 方法都更安全。

          它对数据做出以下假设:

          1. 哈希键假定为字符串、符号或整数。
          2. 哈希值假定为字符串、符号、整数、布尔值、nil、数组或哈希值。
          # frozen_string_literal: true
          
          require 'parser/current'
          
          class HashParser
            # Type error is used to handle unexpected types when parsing stringified hashes.
            class TypeError < ::StandardError
              attr_reader :message, :type
          
              def initialize(message, type)
                @message = message
                @type = type
              end
            end
          
            def hash_from_s(str_hash)
              ast = Parser::CurrentRuby.parse(str_hash)
          
              unless ast.type == :hash
                puts "expected data to be a hash but got #{ast.type}"
                return
              end
          
              parse_hash(ast)
            rescue Parser::SyntaxError => e
              puts "error parsing hash: #{e.message}"
            rescue TypeError => e
              puts "unexpected type (#{e.type}) encountered while parsing: #{e.message}"
            end
          
            private
          
            def parse_hash(hash)
              out = {}
              hash.children.each do |node|
                unless node.type == :pair
                  raise TypeError.new("expected child of hash to be a `pair`", node.type)
                end
          
                key, value = node.children
          
                key = parse_key(key)
                value = parse_value(value)
          
                out[key] = value
              end
          
              out
            end
          
            def parse_key(key)
              case key.type
              when :sym, :str, :int
                key.children.first
              else
                raise TypeError.new("expected key to be either symbol, string, or integer", key.type)
              end
            end
          
            def parse_value(value)
              case value.type
              when :sym, :str, :int
                value.children.first
              when :true
                true
              when :false
                false
              when :nil
                nil
              when :array
                value.children.map { |c| parse_value(c) }
              when :hash
                parse_hash(value)
              else
                raise TypeError.new("value of a pair was an unexpected type", value.type)
              end
            end
          end
          

          这里有一些 rspec 测试验证它是否按预期工作:

          # frozen_string_literal: true
          
          require 'spec_helper'
          
          RSpec.describe HashParser do
            describe '#hash_from_s' do
              subject { described_class.new.hash_from_s(input) }
          
              context 'when input contains forbidden types' do
                where(:input) do
                  [
                    'def foo; "bar"; end',
                    '`cat somefile`',
                    'exec("cat /etc/passwd")',
                    '{:key=>Env.fetch("SOME_VAR")}',
                    '{:key=>{:another_key=>Env.fetch("SOME_VAR")}}',
                    '{"key"=>"value: #{send}"}'
                  ]
                end
          
                with_them do
                  it 'returns nil' do
                    expect(subject).to be_nil
                  end
                end
              end
          
              context 'when input cannot be parsed' do
                let(:input) { "{" }
          
                it 'returns nil' do
                  expect(subject).to be_nil
                end
              end
          
              context 'with valid input' do
                using RSpec::Parameterized::TableSyntax
          
                where(:input, :expected) do
                  '{}'                          | {}
                  '{"bool"=>true}'              | { 'bool' => true }
                  '{"bool"=>false}'             | { 'bool' => false }
                  '{"nil"=>nil}'                | { 'nil' => nil }
                  '{"array"=>[1, "foo", nil]}'  | { 'array' => [1, "foo", nil] }
                  '{foo: :bar}'                 | { foo: :bar }
                  '{foo: {bar: "bin"}}'         | { foo: { bar: "bin" } }
                end
          
                with_them do
                  specify { expect(subject).to eq(expected) }
                end
              end
            end
          end
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2011-02-07
            • 2013-10-16
            • 1970-01-01
            • 2017-03-27
            • 2012-04-04
            • 2021-11-17
            相关资源
            最近更新 更多