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

我有一个看起来像散列的字符串:

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

我如何从中得到一个 Hash? 比如:

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

字符串可以具有任意深度的嵌套。它具有 Ruby 中有效 Hash 类型的所有属性。

236922 次浏览

快速和肮脏的方法将是

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

但它具有严重的安全隐患。
无论传递什么,它都会执行,你必须110% 确定(至少在这个过程中没有用户输入)它将只包含正确形式的散列或者来自外太空的意外的错误/可怕的生物可能开始弹出。

通过调用 Hash#inspect创建的字符串可以通过调用 eval返回到散列。但是,这要求散列中的所有对象都为真。

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

但是,如果散列中的所有内容都是字符串、符号、数字和数组,那么它应该可以工作,因为这些内容具有有效的 Ruby 语法的字符串表示形式。

也许是 YAML 加载?

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

"{ :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]}

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

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

然而,它无法处理包含日期字符串的散列(我们的日期字符串最终没有被字符串包围,这就是最大的问题所在) :

String =’{‘ last _ request _ at’: 2011-12-2823:00:00 UTC }’ ActiveSupport::JSON.decode(string.gsub(/:([a-zA-z])/,'\\1').gsub('=>', ' : '))

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

我想听听你对如何处理这个案子的建议

这个短小的代码片段可以做到这一点,但是我看不到它与嵌套散列一起工作。不过我觉得挺可爱的

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

步骤 1. 去掉“{”、“}”和“ :” 2. 我在字符串中找到一个“ ,” 3.每当它发现一个“ = >”时,我就拆分用拆分创建的每个子字符串。然后,我创建一个散列,其中包含刚刚分离的散列的两边。 留下一个散列数组,然后将它们合并在一起。

示例输入: “{ : user _ id = > 11,: blog _ id = > 2,: comments _ id = > 1}” 结果输出: {“ user _ id”= > “11”,“ blog _ id”= > “2”,“ comments _ id”= > “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('=>', ':')

工程在轨道4.1和支持符号没有引号{ : a = > ‘ b’}

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

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

到目前为止,解决方案涵盖了一些情况,但遗漏了一些(见下文)。以下是我试图进行更彻底(安全)转换的尝试。我知道一个角落的情况,这个解决方案不处理,这是单个字符符号组成的奇数,但允许字符。例如,{:> => :<}是一个有效的 Ruby 散列。

这段代码以一个测试字符串开头,用来练习所有的转换

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)

这里有一些关于其他解决方案的注释

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

档案: 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

我也有同样的问题。我在 Redis 储存大麻。在检索该散列时,它是一个字符串。出于安全考虑,我不想打电话给 eval(str)。我的解决方案是将散列保存为 json 字符串,而不是 Ruby 散列字符串。如果可以选择,那么使用 json 会更容易。

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

使用 to_jsonJSON.parse

我构建了一个 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 安全的方法。

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

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

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

然后使用 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。

此方法适用于一级深散列


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




这里有一种使用 Whitequark/解析器的方法,它比 gsubeval两种方法都要安全。

它对数据作出以下假设:

  1. 假定哈希键为字符串、符号或整数。
  2. 假定散列值为字符串、符号、整数、布尔值、零、数组或散列。
# 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

在 Ruby 中尝试将字符串转换为散列时,我遇到了类似的问题。

我的计算结果是这样的:

{
"coord":{"lon":24.7535,"lat":59.437},
"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04d"}],
"base":"stations",
"main":{"temp":283.34,"feels_like":281.8,"temp_min":282.33,"temp_max":283.34,"pressure":1021,"humidity":53},
"visibility":10000,
"wind":{"speed":3.09,"deg":310},
"clouds":{"all":75},
"dt":1652808506,
"sys":{"type":1,"id":1330,"country":"EE","sunrise":1652751796,"sunset":1652813502},
"timezone":10800,"id":588409,"name":"Tallinn","cod":200
}

我使用下面的命令检查了类型值并确认它是 绳子类型:

result =
{
"coord":{"lon":24.7535,"lat":59.437},
"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04d"}],
"base":"stations",
"main":{"temp":283.34,"feels_like":281.8,"temp_min":282.33,"temp_max":283.34,"pressure":1021,"humidity":53},
"visibility":10000,
"wind":{"speed":3.09,"deg":310},
"clouds":{"all":75},
"dt":1652808506,
"sys":{"type":1,"id":1330,"country":"EE","sunrise":1652751796,"sunset":1652813502},
"timezone":10800,"id":588409,"name":"Tallinn","cod":200
}


puts result.instance_of? String
puts result.instance_of? Hash

我是这样解决这个问题的:

我所要做的就是运行下面的命令将它从 绳子转换成 大麻:

result_new = JSON.parse(result, symbolize_names: true)

然后使用下面的命令再次检查类型值:

puts result_new.instance_of? String
puts result_new.instance_of? Hash

这一次,它返回 true大麻