Ruby:如何将哈希转换为HTTP参数?

236

使用普通哈希算法很容易实现,比如

{: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参数...也许你需要使用不同的编码方式? - CookieOfFortune
嗯,这实际上不是Json,而是Ruby的哈希表……不确定为什么编码在这里很重要。 - Julien Genestoux
lmanners的答案应该得到推广。这里有很多很棒的自定义答案(其中许多得分很高),但是ActiveSupport已经添加了对此的标准化支持,使得这个讨论没有意义了。不幸的是,lmanner的答案仍然被埋在列表底部。 - Noach Magedman
2
依我之见,任何建议依赖于大量monkey patch核心类库的答案都应该被深埋。大量的这些补丁的理由至多站不住脚(请看一下Yehuda Katz在这篇文章中的评论),这是一个很好的例子。可能会因人而异,但对我来说,具有类方法或不打开Object和Hash的东西,并且作者不会说“只要不与我们冲突就行!”会更好得多。 - ian
14个回答

315

对于基本的非嵌套哈希,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


1
你为什么说它坏了?你展示的输出没问题,对吧? - tokland
我刚试了一下,你似乎是对的。也许我的说法最初是由于早期版本的Rails解析查询字符串的方式(我似乎记得它会覆盖先前的“b”值)。开始GET“/?a=a&b%5B%5D=c&b%5B%5D=d&b%5B%5D=e”为127.0.0.1在2011-03-10 11:19:40 -0600 处理SitesController#index作为HTML 参数:{"a" => "a", "b" => ["c", "d", "e"]} - Gabe Martin-Dempesy
如果有嵌套的哈希表会出现什么问题?为什么在有嵌套哈希表时我不能使用这个?对我来说,它只是对嵌套哈希进行了URL转义,在HTTP请求中使用这个应该没有问题。 - Sam
3
没有Rails:需要require 'active_support/all' - Dorian
1
至少在Rails 5.2中,“to_query”不能正确处理nil值。{ a: nil, b: '1'}.to_query == "a=&b=1",但是Rack和CGI都将a=解析为空字符串,而不是nil。我不确定其他服务器的支持情况,但是对于Rails来说,正确的查询字符串应该是a&b=1。我认为Rails无法生成自己可以正确解析的查询字符串是错误的... - jsmartt

174

如果您使用的是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使用的规范符合HTML5对application/x-www-form-urlencoded数据的定义。


11
+1,这绝对是最好的。它不依赖于 Ruby 以外的任何资源。 - Danyel
+1 在 '{:a => "a", :b => {:c => "c", :d => true}, :e => []}' 的例子中运行良好。 - Duke
1
似乎不能与 Ruby 2.0 一起使用 - 哈希{:c =>“c”,:d => true}似乎已被检查,因此作为字符串发送。 - user208769
1
这是上面较大代码片段的一部分 - ruby -ruri -e 'puts RUBY_VERSION; puts URI.encode_www_form({:a => "a", :b => {:c => "c", :d => true}, :e => []})' # 输出2.0.0 a=a&b=%7B%3Ac%3D%3E%22c%22%2C+%3Ad%3D%3Etrue%7D& - user208769
3
请注意,对于数组值,这与 Addressable::URI 和 ActiveSupport 的 Object#to_query 有不同的结果。 - Matt Huggins
显示剩余2条评论

89

更新:这个功能已经从宝石中删除。

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

1
谢谢!我的解决方案会在哪些边缘情况下失效?这样我就可以将其添加到规格说明中。 - Julien Genestoux
2
它无法处理布尔值,这显然是不可取的: {"a" => "a&b=b"}.to_params。 - Bob Aman
5
很抱歉告诉您,从 Addressable 2.3 开始,这种行为已被删除。(链接:https://github.com/sporkmonger/addressable/commit/f51e290b5f68a98293327a7da84eb9e2d5f21c62) - oif_vet
2
@oif_vet 你能说一下哪些行为已经被移除了吗?Bob建议使用addressable gem来解决原帖的问题,对我来说在addressable-2.3.2版本中是有效的。 - sheldonh
1
@sheldonh,不,@oif_vet是正确的。我已经移除了这个行为。在Addressable中,深度嵌套的结构不再支持作为“query_values”变异器的输入。 - Bob Aman

71
无需加载臃肿的ActiveSupport或自己编写代码,可以使用Rack::Utils.build_query和Rack::Utils.build_nested_query。这里有一篇好的博客文章提供了一个很好的例子:点击此处
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 变成了一个包含一个更大的哈希的数组。 - Ed Ruder
3
没有“properly”这个词,因为没有被广泛认可的标准。但从其他回答来看,这个回答显示出它比其他人的尝试更接近正确答案。我认为这是作者的意思。 - ian
1
此方法自Rails 2.3.8起已被弃用:http://apidock.com/rails/Rack/Utils/build_query - davidgoli
8
@ davidgoli,不在Rack里,详见 https://github.com/rack/rack/blob/1.5.2/lib/rack/utils.rb#L140。如果您想在Rails中使用它,只需简单地写`require 'rack'`即可。鉴于现在所有主要的Ruby Web框架都是基于Rack构建的,因此它肯定存在。 - ian
1
@EdRuder ActiveSupport的to_query方法也会合并这两个数组(v4.2)。 - Kelvin

12

如果你只需要支持简单的 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"

10

从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


1
不幸的是,当我们在参数中有一个嵌套的数组时(参见示例#2)这种方法不起作用... :( - Julien Genestoux
2
不会执行任何转义操作。 - Ernest

5
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

4

我知道这是一个老问题,但我想发表这段代码,因为我找不到一个简单的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

打包成gem文件发布在这里:https://github.com/simen/queryparams


1
URI.escape != CGI.escape 而对于 URL,你需要使用第一个。 - Ernest
2
实际上不是这样的,@Ernest。例如,将另一个URL嵌入作为参数到您的URL中(假设这是登录后要重定向到的返回URL),URI.escape将保留嵌入式URL的“?”和“&”,从而破坏周围的URL,而CGI.escape将正确地将它们存储为%3F和%26以备后用。`CGI.escape("http://localhost/search?q=banana&limit=7")` `=> "http%3A%2F%2Flocalhost%2Fsearch%3Fq%3Dbanana%26limit%3D7"` `URI.escape("http://localhost/search?q=banana&limit=7")` `=> "http://localhost/search?q=banana&limit=7"` - svale

4
{:a=>"a", :b=>"b", :c=>"c"}.map{ |x,v| "#{x}=#{v}" }.reduce{|x,v| "#{x}&#{v}" }

"a=a&b=b&c=c"

这里还有另一种方法。适用于简单的查询。

2
你真的应该确保你正确地对URI转义你的键和值。即使是简单的情况也是如此。否则会给你带来麻烦。 - jrochkind

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

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