【问题标题】:Ruby: How to turn a hash into HTTP parameters?Ruby:如何将散列转换为 HTTP 参数?
【发布时间】:2010-10-22 08:57:09
【问题描述】:

使用像

这样的普通哈希非常容易
{:a => "a", :b => "b"} 

这将转化为

"a=a&b=b"

但是你如何处理更复杂的事情,比如

{:a => "a", :b => ["c", "d", "e"]} 

应该翻译成

"a=a&b[0]=c&b[1]=d&b[2]=e" 

或者更糟糕的是,(做什么)类似:

{:a => "a", :b => [{:c => "c", :d => "d"}, {:e => "e", :f => "f"}]

感谢您对此提供的非常感谢的帮助!

【问题讨论】:

  • 听起来您想将 JSON 转换为 HTTP 参数...也许您需要不同的编码?
  • 嗯,这实际上不是 Json,而是一个 Ruby 哈希......我不明白为什么编码在这里很重要。
  • lmanners的回答应该推广。这里有很多很棒的自己动手的答案(很多得分很高),但 ActiveSupport 已经为此添加了标准化支持,从而使对话变得毫无意义。不幸的是,lmner 的答案仍然在列表中。
  • @Noach 在我看来,任何说依赖于大量猴子补丁核心类的库的答案都应该被埋没。大量这些补丁的理由充其量是不稳定的(看看Yehuda Katz's comments in this article),这是一个很好的例子。 YMMV,但对我来说,是带有类方法或不打开 Object 和 Hash 的东西,而且作者不会说“不要与我们发生冲突!”会好很多。

标签: ruby http parameters hashmap


【解决方案1】:

对于基本的非嵌套哈希,Rails/ActiveSupport 有Object#to_query

>> {:a => "a", :b => ["c", "d", "e"]}.to_query
=> "a=a&b%5B%5D=c&b%5B%5D=d&b%5B%5D=e"
>> CGI.unescape({:a => "a", :b => ["c", "d", "e"]}.to_query)
=> "a=a&b[]=c&b[]=d&b[]=e"

http://api.rubyonrails.org/classes/Object.html#method-i-to_query

【讨论】:

  • 为什么说它坏了?你显示的输出没问题吧?
  • 我刚试过,你似乎是对的。也许我的陈述最初是由于早期版本的 rails 解析查询字符串的方式(我似乎记得它覆盖了以前的 'b' 值)。在 2011 年 3 月 10 日 11:19:40 -0600 开始 GET "/?a=a&b%5B%5D=c&b%5B%5D=d&b%5B%5D=e" for 127.0.0.1 由 SitesController#index 处理HTML 参数:{"a"=>"a", "b"=>["c", "d", "e"]}
  • 如果存在嵌套哈希会出现什么问题?为什么有嵌套哈希时我不能使用它?对我来说,它只是 url 转义了嵌套的哈希,在 http 请求中使用它应该没有问题。
  • 不带 Rails:需要require 'active_support/all'
  • 至少 Rails 5.2 to_query 不能正确处理 nil 值。 { a: nil, b: '1'}.to_query == "a=&b=1",但 Rack 和 CGI​​ 都将 a= 解析为空字符串,而不是 nil。我不确定是否支持其他服务器,但是使用 rails,正确的查询字符串应该是 a&b=1。我认为 Rails 不能生成自己正确解析的查询字符串是错误的……
【解决方案2】:

如果您使用的是 Ruby 1.9.2 或更高版本,如果您不需要数组,则可以使用 URI.encode_www_form

例如(来自 1.9.3 中的 Ruby 文档):

URI.encode_www_form([["q", "ruby"], ["lang", "en"]])
#=> "q=ruby&lang=en"
URI.encode_www_form("q" => "ruby", "lang" => "en")
#=> "q=ruby&lang=en"
URI.encode_www_form("q" => ["ruby", "perl"], "lang" => "en")
#=> "q=ruby&q=perl&lang=en"
URI.encode_www_form([["q", "ruby"], ["q", "perl"], ["lang", "en"]])
#=> "q=ruby&q=perl&lang=en"

您会注意到数组值没有使用包含[] 的键名来设置,就像我们在查询字符串中已经习惯的那样。 encode_www_form 使用的规范符合application/x-www-form-urlencoded 数据的 HTML5 定义。

【讨论】:

  • +1,这是迄今为止最好的。它不依赖于 Ruby 本身之外的任何资源。
  • +1 适用于 '{:a => "a", :b => {:c => "c", :d => true}, :e => []} ' 例子
  • 似乎不适用于 ruby​​ 2.0 - 哈希 {:c => "c", :d => true} 似乎已被检查,因此作为字符串发送。
  • 这是上面较大的 sn-p 的一部分 - ruby -ruri -e 'puts RUBY_VERSION; puts URI.encode_www_form({:a => "a", :b => {:c => "c", :d => true}, :e => []})' # outputs 2.0.0 a=a&b=%7B%3Ac%3D%3E%22c%22%2C+%3Ad%3D%3Etrue%7D&
  • 请注意,这对于数组值的结果与 Addressable::URI 和 ActiveSupport 的 Object#to_query 不同。
【解决方案3】:

更新:此功能已从 gem 中删除。

Julien,你的自我回答很好,我无耻地借鉴了它,但它不能正确地转义保留字符,而且还有一些其他极端情况会崩溃。

require "addressable/uri"
uri = Addressable::URI.new
uri.query_values = {:a => "a", :b => ["c", "d", "e"]}
uri.query
# => "a=a&b[0]=c&b[1]=d&b[2]=e"
uri.query_values = {:a => "a", :b => [{:c => "c", :d => "d"}, {:e => "e", :f => "f"}]}
uri.query
# => "a=a&b[0][c]=c&b[0][d]=d&b[1][e]=e&b[1][f]=f"
uri.query_values = {:a => "a", :b => {:c => "c", :d => "d"}}
uri.query
# => "a=a&b[c]=c&b[d]=d"
uri.query_values = {:a => "a", :b => {:c => "c", :d => true}}
uri.query
# => "a=a&b[c]=c&b[d]"
uri.query_values = {:a => "a", :b => {:c => "c", :d => true}, :e => []}
uri.query
# => "a=a&b[c]=c&b[d]"

宝石是'addressable'

gem install addressable

【讨论】:

  • 谢谢!我的解决方案出现问题的边缘情况是什么?所以我可以将它添加到规格中吗?
  • 它不处理布尔值,这显然是不可取的:{"a" => "a&b=b"}.to_params
  • 仅供参考,不幸的是,从 2.3 (github.com/sporkmonger/addressable/commit/…) 起,此行为已从 Addressable 中删除
  • @oif_vet 你能说一下删除了什么行为吗?从 addressable-2.3.2 开始,Bob 建议的使用 addressable gem 解决原始海报问题的方法对我有效。
  • @sheldonh,不,@oif_vet 是正确的。我删除了这种行为。 Addressable 不再支持深度嵌套的结构作为query_values mutator 的输入。
【解决方案4】:

无需加载臃肿的 ActiveSupport 或自行运行,您可以使用 Rack::Utils.build_queryRack::Utils.build_nested_queryHere's a blog post 就是一个很好的例子:

require 'rack'

Rack::Utils.build_query(
  authorization_token: "foo",
  access_level: "moderator",
  previous: "index"
)

# => "authorization_token=foo&access_level=moderator&previous=index"

它甚至可以处理数组:

Rack::Utils.build_query( {:a => "a", :b => ["c", "d", "e"]} )
# => "a=a&b=c&b=d&b=e"
Rack::Utils.parse_query _
# => {"a"=>"a", "b"=>["c", "d", "e"]}

或者更难嵌套的东西:

Rack::Utils.build_nested_query( {:a => "a", :b => [{:c => "c", :d => "d"}, {:e => "e", :f => "f"}] } )
# => "a=a&b[][c]=c&b[][d]=d&b[][e]=e&b[][f]=f"
Rack::Utils.parse_nested_query _
# => {"a"=>"a", "b"=>[{"c"=>"c", "d"=>"d", "e"=>"e", "f"=>"f"}]}

【讨论】:

  • 您的嵌套示例表明它不能正常工作——当您开始时,:b 是一个包含两个哈希的数组。你最终会得到 :b 是一个更大的哈希数组。
  • @EdRuder 没有正确,因为没有公认的标准。从其他答案来看,它确实表明它比其他任何人的尝试都更接近。
  • 该方法自 Rails 2.3.8 起已弃用:apidock.com/rails/Rack/Utils/build_query
  • @davidgoli Erm,不在 Rack 中,它不是 github.com/rack/rack/blob/1.5.2/lib/rack/utils.rb#L140。如果你想在 Rails 中使用它,肯定就像require 'rack' 一样简单吗?考虑到现在所有主要的 Ruby Web 框架都构建在 Rack 之上,它肯定存在。
  • @EdRuder ActiveSupport 的 to_query 也合并了 2 个数组 (v4.2)。
【解决方案5】:

如果您只需要支持简单的 ASCII 键/值查询字符串,这里有一个简短而贴心的语句:

hash = {"foo" => "bar", "fooz" => 123}
# => {"foo"=>"bar", "fooz"=>123}
query_string = hash.to_a.map { |x| "#{x[0]}=#{x[1]}" }.join("&")
# => "foo=bar&fooz=123"

【讨论】:

    【解决方案6】:

    从 Merb 窃取:

    # File merb/core_ext/hash.rb, line 87
    def to_params
      params = ''
      stack = []
    
      each do |k, v|
        if v.is_a?(Hash)
          stack << [k,v]
        else
          params << "#{k}=#{v}&"
        end
      end
    
      stack.each do |parent, hash|
        hash.each do |k, v|
          if v.is_a?(Hash)
            stack << ["#{parent}[#{k}]", v]
          else
            params << "#{parent}[#{k}]=#{v}&"
          end
        end
      end
    
      params.chop! # trailing &
      params
    end
    

    http://noobkit.com/show/ruby/gems/development/merb/hash/to_params.html

    【讨论】:

    • 不幸的是,当我们在参数中有一个嵌套数组时,这个deos不起作用(参见示例#2)...... :(
    • 并且不做任何逃跑之王。
    【解决方案7】:
    class Hash
      def to_params
        params = ''
        stack = []
    
        each do |k, v|
          if v.is_a?(Hash)
            stack << [k,v]
          elsif v.is_a?(Array)
            stack << [k,Hash.from_array(v)]
          else
            params << "#{k}=#{v}&"
          end
        end
    
        stack.each do |parent, hash|
          hash.each do |k, v|
            if v.is_a?(Hash)
              stack << ["#{parent}[#{k}]", v]
            else
              params << "#{parent}[#{k}]=#{v}&"
            end
          end
        end
    
        params.chop! 
        params
      end
    
      def self.from_array(array = [])
        h = Hash.new
        array.size.times do |t|
          h[t] = array[t]
        end
        h
      end
    
    end
    

    【讨论】:

      【解决方案8】:
      {:a=>"a", :b=>"b", :c=>"c"}.map{ |x,v| "#{x}=#{v}" }.reduce{|x,v| "#{x}&#{v}" }
      
      "a=a&b=b&c=c"
      

      这是另一种方式。用于简单查询。

      【讨论】:

      • 你真的应该确保你正确地对你的键和值进行 URI 转义。即使是简单的情况。它会咬你的。
      【解决方案9】:

      我知道这是一个老问题,但我只是想发布这段代码,因为我找不到一个简单的 gem 来为我完成这项任务。

      module QueryParams
      
        def self.encode(value, key = nil)
          case value
          when Hash  then value.map { |k,v| encode(v, append_key(key,k)) }.join('&')
          when Array then value.map { |v| encode(v, "#{key}[]") }.join('&')
          when nil   then ''
          else            
            "#{key}=#{CGI.escape(value.to_s)}" 
          end
        end
      
        private
      
        def self.append_key(root_key, key)
          root_key.nil? ? key : "#{root_key}[#{key.to_s}]"
        end
      end
      

      在这里汇总为宝石:https://github.com/simen/queryparams

      【讨论】:

      • URI.escape != CGI.escape 和你想要的第一个 URL。
      • 实际上不是,@Ernest。当例如将另一个 url 作为参数嵌入到您的 url(假设这是登录后要重定向到的返回 url) URI.escape 将保留“?”并且嵌入 url 的 '&' 会破坏周围的 url,而 CGI.escape 将正确地将它们隐藏起来,以便稍后作为 %3F 和 %26。 CGI.escape("http://localhost/search?q=banana&amp;limit=7")=&gt; "http%3A%2F%2Flocalhost%2Fsearch%3Fq%3Dbanana%26limit%3D7"URI.escape("http://localhost/search?q=banana&amp;limit=7")=&gt; "http://localhost/search?q=banana&amp;limit=7"
      【解决方案10】:

      最好的方法是使用 Hash.to_params,它可以很好地处理数组。

      {a: 1, b: [1,2,3]}.to_param
      "a=1&b[]=1&b[]=2&b[]=3"
      

      【讨论】:

      • 不带 Rails:需要require 'active_support/all'
      【解决方案11】:
      require 'uri'
      
      class Hash
        def to_query_hash(key)
          reduce({}) do |h, (k, v)|
            new_key = key.nil? ? k : "#{key}[#{k}]"
            v = Hash[v.each_with_index.to_a.map(&:reverse)] if v.is_a?(Array)
            if v.is_a?(Hash)
              h.merge!(v.to_query_hash(new_key))
            else
              h[new_key] = v
            end
            h
          end
        end
      
        def to_query(key = nil)
          URI.encode_www_form(to_query_hash(key))
        end
      end
      
      2.4.2 :019 > {:a => "a", :b => "b"}.to_query_hash(nil)
       => {:a=>"a", :b=>"b"}
      
      2.4.2 :020 > {:a => "a", :b => "b"}.to_query
       => "a=a&b=b"
      
      2.4.2 :021 > {:a => "a", :b => ["c", "d", "e"]}.to_query_hash(nil)
       => {:a=>"a", "b[0]"=>"c", "b[1]"=>"d", "b[2]"=>"e"}
      
      2.4.2 :022 > {:a => "a", :b => ["c", "d", "e"]}.to_query
       => "a=a&b%5B0%5D=c&b%5B1%5D=d&b%5B2%5D=e"
      

      【讨论】:

        【解决方案12】:

        如果您处于 Faraday 请求的上下文中,您也可以将 params 哈希作为第二个参数传递,并且 faraday 会负责将正确的 param URL 部分用于其中:

        faraday_instance.get(url, params_hsh)
        

        【讨论】:

          【解决方案13】:

          我喜欢使用这个 gem:

          https://rubygems.org/gems/php_http_build_query

          示例用法:

          puts PHP.http_build_query({"a"=>"b","c"=>"d","e"=>[{"hello"=>"world","bah"=>"black"},{"hello"=>"world","bah"=>"black"}]})
          
          # a=b&c=d&e%5B0%5D%5Bbah%5D=black&e%5B0%5D%5Bhello%5D=world&e%5B1%5D%5Bbah%5D=black&e%5B1%5D%5Bhello%5D=world
          

          【讨论】:

            【解决方案14】:
            2.6.3 :001 > hash = {:a => "a", :b => ["c", "d", "e"]}
            => {:a=>"a", :b=>["c", "d", "e"]}
            2.6.3 :002 > hash.to_a.map { |x| "#{x[0]}=#{x[1].class == Array ? x[1].join(",") : x[1]}" 
            }.join("&")
            => "a=a&b=c,d,e"
            

            【讨论】: