Как мне преобразовать объект String в объект Hash?

У меня есть строка, которая выглядит как хэш:

"{ :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 вводится правильный хэш.

16 ответов

Решение

Строка, созданная путем вызова Hash#inspect можно превратить обратно в хеш, позвонив eval в теме. Однако это требует того же, чтобы быть верным для всех объектов в хэше.

Если я начну с хэша {:a => Object.new}то его строковое представление "{:a=>#<Object:0x7f66b65cf4d0>}"и я не могу использовать eval превратить его обратно в хеш, потому что #<Object:0x7f66b65cf4d0> неверный синтаксис Ruby.

Однако, если все, что находится в хэше, это строки, символы, числа и массивы, это должно работать, потому что у них есть строковые представления, которые являются допустимым синтаксисом Ruby.

Для другой строки, вы можете сделать это без использования опасного 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("{ :key_a => { :key_1a => 'value_1a', :key_2a => 'value_2a' }, :key_b => { :key_1b => 'value_1b' } }") 

Но это имеет серьезные последствия для безопасности.
Он выполняет все, что ему передают, вы должны быть уверены на 110% (как, по крайней мере, по крайней мере, никакого ввода пользователя в любом месте пути), он будет содержать только правильно сформированные хеши, или неожиданные ошибки / ужасные существа из космоса могут начать появляться.

У меня такая же проблема. Я хэшировал в Redis. При получении этого хэша это была строка. Я не хотел звонить eval(str) из-за проблем безопасности. Мое решение состояло в том, чтобы сохранить хэш как строку json вместо строки хэша ruby. Если у вас есть возможность, использовать json проще.

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

TL; DR: использовать to_json а также JSON.parse

Может быть, YAML.load?

Решения пока охватывают некоторые случаи, но пропускают некоторые (см. Ниже). Вот моя попытка более тщательного (безопасного) преобразования. Я знаю один угловой случай, который не обрабатывается этим решением, который состоит из односимвольных символов, состоящих из нечетных, но разрешенных символов. Например {:> => :<} является действительным хэш рубин

Я также разместил этот код на 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)

Вот некоторые заметки о других решениях здесь

Этот короткий фрагмент кода сделает это, но я не вижу, чтобы он работал с вложенным хешем. Я думаю, что это довольно мило, хотя

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

Я предпочитаю злоупотреблять 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 при попытке анализа значения даты.

Буду рад любым предложениям о том, как справиться с этим делом

Работает в rails 4.1 и поддерживает символы без кавычек {:a => 'b'}

просто добавьте это в папку инициализаторов:

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

Пожалуйста, рассмотрите это решение. Библиотека + спецификации:

Файл: 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

Вот метод, использующий whitequark/parser , который безопаснее, чем оба gsubа также evalметоды.

Он делает следующие предположения о данных:

  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

Этот метод работает для одноуровневого хеша.

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


Я построил гем hash_parser, который сначала проверяет, является ли хеш безопасным или не использует ruby_parser драгоценный камень. Только тогда он применяет 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() создает его как строку.

Затем использовал eval() для преобразования строки в хэш.

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, если хотите.

Я пришел к этому вопросу после того, как написал для этой цели однострочник, поэтому делюсь своим кодом на случай, если он кому-нибудь поможет. Работает для строки с глубиной только одного уровня и возможными пустыми значениями (но не с нулем), например:

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

У меня была похожая проблема при попытке преобразовать строку в хэш в 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
 }

Я проверил значение типа и подтвердил, что он имеет тип String , используя приведенную ниже команду:

      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для хэша

Другие вопросы по тегам