Rubyのnet/httpで重複するキーをもつパラメータをPostする
net/http を使うと、単純にPostするのは至って簡単だ。
Net::HTTP.post_formメソッドを叩くだけで事足りる。
require 'net/http' require 'uri' #例1: POSTするだけ res = Net::HTTP.post_form(URI.parse('http://www.example.com/search'), {'q'=>'ruby', 'max'=>'50'}) puts res.body #例2: 認証付きで POST する res = Net::HTTP.post_form(URI.parse('http://jack:pass@www.example.com/todo.cgi'), {'from'=>'2005-01-01', 'to'=>'2005-03-31'}) puts res.body #例3: より細かく制御する url = URI.parse('http://www.example.com/todo.cgi') req = Net::HTTP::Post.new(url.path) req.basic_auth 'jack', 'pass' req.set_form_data({'from'=>'2005-01-01', 'to'=>'2005-03-31'}, ';') res = Net::HTTP.new(url.host, url.port).start {|http| http.request(req) } case res when Net::HTTPSuccess, Net::HTTPRedirection # OK else res.value end
キーが重複するパラメータ問題
フォームで配列を送信するとしよう。
例えば、
somekey[] => "hoge", somekey[] => "fuga", somekey[] => "piyo",
といったパラメータだ。
post_formメソッドは、uriとparamsの引数を取る。第一引数のuriはURIで、第二引数のparamsはHashだ。
つまり、こういう呼び出しになるだろう。
Net::HTTP.post_form(URI.parse('http://example.com'), { "somekey[]" => "hoge", "somekey[]" => "fuga", "somekey[]" => "piyo", })
ここで問題が浮上する。Hashは重複するキーを持つことができない。上記の呼び出しは、このように変換されてしまう。
Net::HTTP.post_form(URI.parse('http://example.com'), { "somekey[]" => "piyo", })
解決法
HTTPリクエストbodyにPostしたいデータを直接セットしてやることで回避した。
その際は、自分でPostするパラメータを文字列化し、URLエンコードする必要がある。具体的には、URI.encode_www_formを使う。
3行目を参照。
url = URI.parse('http://www.example.com/todo.cgi') req = Net::HTTP::Post.new(url.path) req.body = URI.encode_www_form [ ["somekey[]", "hoge"], ["somekey[]", "fuga"], ["somekey[]", "piyo"] ] res = Net::HTTP.new(url.host, url.port).start {|http| http.request(req) } case res when Net::HTTPSuccess, Net::HTTPRedirection # OK else res.value end
キモは、URI.encode_www_formでURLエンコードする際に、配列の配列を渡すことだ。Hashじゃないからキーの重複も問題ない。
http://docs.ruby-lang.org/ja/2.1.0/class/URI.html#S_ENCODE_WWW_FORM
URI.encode_www_formメソッドの返り値はこうなる。
"somekey%5B%5D=hoge&somekey%5B%5D=fuga&somekey%5B%5D=piyo"
tips
Net::HTTP.post_formはHashを受け取ると書いたが、それはあくまでドキュメント上のはなしだ。
ソースを追えばわかるが、内部ではURI.encode_www_formを呼んでいるだけである。
(post_formの中でNet::HTTPHeader#set_form_dataが呼ばれ、その中でURI.encode_www_formしている。)
だから、post_formメソッドに(Hashではなく)配列の配列を渡しても動く。
Net::HTTP.post_form(URI.parse('http://example.com'), [ ["somekey[]", "hoge"], ["somekey[]", "fuga"], ["somekey[]", "piyo"] ])
不安なら、set_debug_output($stderr)を使ってリクエストを覗いてみればいい。
ただundocumentedなので、こういう風に使っていいのかアヤシイ。
バーコードジェネレータをゆるーくRubyで
ドットインストール Ruby on Railsの基礎 #36 でAjaxによるPost削除処理が動かない
メモ
$('a[data-method="delete"]').live('ajax:success', function(e, data, status, xhr) { $('#post_'+data.post.id).fadeOut("slow"); });
data-method="delete"って属性を持つaタグに、イベントハンドラを設定するコード。
JSデバッガコンソールに'undefined is not a function'とのエラーが表示され、正しく動作しなかった。
jQuery v1.9.0では、jQueryオブジェクトのliveメソッドが非推奨を経て削除されており、定義されていない(undefined)ため。
live関数の代わりに追加されたon関数を使う。
書き直すと以下のようになる。
$(document).on('ajax:success', 'a[data-method="delete"]', function(e, data, status, xhr) { $('#post_'+data.post.id).fadeOut("slow"); });
まあ3行ならコピペするけどね。
とある業者からのメール 「下記日程のいずれかにて、ヒアリングいただけますでしょうか。 6月5日(火) 14:00以降 6月6日(火) 14:00以降 6月7日(火) 14:00以降」 皆様,これがコードクローンとその弊害です.
こんな呟きが流れてきたので、
この三行をコードで出力するとして、Objective-CでDRYに即したコードを書くことを考えるとブルーになる
NSArray* week; week = [NSArray arrayWithObjects:@"月", @"火", @"水", @"木", @"金", @"土", @"日", nil]; for (int i = 5; i <= 7; i++) { NSLog(@"6月%d日(%@) 14:00以降", i, [week objectAtIndex:(i+3)%7]); }
ついでにRuby
(5..7).each {|i| puts "6月%d日 (%s) 14:00以降" % [i, %w!月 火 水 木 金 土 日![(i+3)%7]]}
Objective-Cは配列周りの記述が冗長になるのは仕方ない。Objective-Cでも一行で書けるけど、Objective-Cは丁寧に、Rubyはなるたけ簡潔に書くほうが読みやすい気がします。
これは文字列を単純に出力するだけのコードだけど、実際にブルーになるのは日付計算です。
Cocoaの日付計算は覚えることも少なく直感的に書けて、かつ柔軟で優秀だと思うんだけど、どうしても行数はかさむ。
Rubyはどんな感じなのかな。今日は眠いので次にでも書き比べてみます。
The new Audi Q3 Decode Challenge用ツール
codes = [ [0,0,1], [2,2,2] ] ans = '' codes.each do |code| byte = ?a.getbyte(0) code.each_with_index do |c, idx| byte += c * (3 ** (2-idx)) end ans << (byte-1).chr end p ans.upcase #=> 'AZ'
ちなみに僕は解けませんでした。これでも使って皆様頑張ってください。
追記
解けました。。
6番(及びその右上)はx=2なんですね。。なんていうトラップ。。
皆様気をつけてください。
もっとハノってみた
ハノイの塔が途中経過状態でも解を出せるように改良してみた。
state = [[5, 3, 1], [4], [2]] hanoi(5, state, "A", "B", "C")
で、何がしたいのかは分かるでしょ。
def create_counter count = 0 return Proc.new do count += 1 end end $counter = create_counter() def hanoi(n, state, from, work, dest) return if n.zero? # Get index index = 0 state.each_with_index do |item, idx| index = idx if item.include? n end case index when 0 from_state, work_state, dest_state = state # Move obstacle if work_state.include? n-2 hanoi(item, [work_state, from_state, dest_state], work, from, dest) end hanoi(n-1, [from_state, dest_state, work_state], from, dest, work) # Move obstacle if dest_state.include? n-2 hanoi(item, [dest_state, from_state, work_state], dest, from, work) end puts "#{"%04d" % $counter.call}: move #{n}, from #{from} to #{dest}" dest_state.push(from_state.pop) hanoi(n-1, [work_state, from_state, dest_state], work, from, dest) when 1 from_state, work_state, dest_state = state hanoi(n, [work_state, from_state, dest_state], work, from, dest) when 2 hanoi(n-1, state, from, work, dest) end end state = [[5, 3, 1], [4], [2]] hanoi(5, state, "A", "B", "C") # Output 0001: move 1, from A to C 0002: move 3, from A to B 0003: move 1, from C to A 0004: move 2, from C to B 0005: move 1, from A to B 0006: move 5, from A to C 0007: move 1, from B to A 0008: move 2, from B to C 0009: move 1, from A to C 0010: move 3, from B to A 0011: move 1, from C to B 0012: move 2, from C to A 0013: move 1, from B to A 0014: move 4, from B to C 0015: move 1, from A to C 0016: move 2, from A to B 0017: move 1, from C to B 0018: move 3, from A to C 0019: move 1, from B to A 0020: move 2, from B to C 0021: move 1, from A to C
まあ解けてるみたいなんですが、問題は
- 「これが最短解である証明」
- 「どの場合でもこのプログラムで解けるかのテストを通過していない」
の二点です。ていうかホントにあってんのかコレ。
あと、相変わらずRubyっぽい書き方になっていないような気がする。
コードをスマートに書き、可読性を高めるための道具が色々と揃っているってのが、初心者から見たRubyの大きな魅力。
メタプロ等を可能にする柔軟性や言語設計のセンスの高さなどを実感するにはまだまだ修行が足りない。
まずは「Rubyらしい書き方」を身に着けたい。自分のコードは「Cを部分的にRubyに置き換えただけ」な感が拭えない。
ハノってみた
書いてみた
def create_counter count = 0; return Proc.new do count += 1; end end $counter = create_counter() def hanoi(n, from, work, dest) return if n.zero? hanoi(n-1, from, dest, work) puts "#{"%04d" % $counter.call}: move #{n}, from #{from} to #{dest}" hanoi(n-1, work, from, dest) end hanoi(4, "A", "B", "C") # output 0001: move 1, from A to B 0002: move 2, from A to C 0003: move 1, from B to C 0004: move 3, from A to B 0005: move 1, from C to A 0006: move 2, from C to B 0007: move 1, from A to B 0008: move 4, from A to C 0009: move 1, from B to C 0010: move 2, from B to A 0011: move 1, from C to A 0012: move 3, from B to C 0013: move 1, from A to B 0014: move 2, from A to C 0015: move 1, from B to C
解法自体はいくつか解説サイトを見た。理解はできたが、似たような問題をまた見た時にサクッと解けるかは別問題。もう少し慣れが必要だろう。
出力に行番号が欲しかったので少し手を加えた。もっとスマートに出来ないものかなあ。
グローバル変数を使わなくて済むように、こんなのも書いてみた。
class Hanoi def initialize def create_counter count = 0 return Proc.new do count += 1 end end @counter = create_counter end def showAnswer(n, from, work, dest) return if n.zero? self.showAnswer(n-1, from, dest, work) puts "#{"%04d" % @counter.call}: move #{n}, from #{from} to #{dest}" self.showAnswer(n-1, work, from, dest) end end hanoi = Hanoi.new hanoi.showAnswer(4, "A", "B", "C")
うーん。全くRubyを使いこなせてない気がする。