Ruby で CGI > 送受信_フォームデータの扱い
ブログや掲示板などによくあるような、ページの記入欄に書き込まれて送信されたデータを、受け取って処理する方法を紹介します。
<html lang="ja"><body> <form method="get" action="http://www.google.com/search"> <input type="text" name="q" value=""> </form> </body></html>
#!/usr/local/bin/ruby -Ks
を #!ruby -Ks
に書き直して使ってくださいまず、GET メソッドでのフォームが入った html ファイルを作成してください。 使用する文字コードは、Shift_JIS とします。 データの送り先は、form.cgiとしてください。 よくわからない人は、下記サンプルをそのままコピーして、保存文字コードShift_JIS にて、ファイル名、form.html で保存してください。 ここでは、この html から送信されたデータを受け止めて一覧する CGI を紹介します。
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html lang="ja"> <head> <meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS"> <title>フォーム送信テスト</title> </head> <body> <h1>フォーム送信テスト</h1> <form action="form.cgi" method="GET"> <p>文字列: <input type="text" name="messe" value="ここに記入" tabindex="1" accesskey="1" size="36" maxlength="72"></p> <p>チェックボックス: <input type="checkbox" name="chk1" value="rabbit" tabindex="2" accesskey="2">うさぎ、 <input type="checkbox" name="chk2" value="panda" tabindex="3" accesskey="3">パンダ</p> <p>ラジオボタン: <input type="radio" name="seibetu" value="otoko" tabindex="4" accesskey="4">男、 <input type="radio" name="seibetu" value="onna" tabindex="5" accesskey="5">女、 <input type="radio" name="seibetu" value="non" tabindex="6" accesskey="6" checked>ノーコメント</p> <p>ボタン: <button type="submit" name="btn" value="Push1" tabindex="7" accesskey="7">送信1</button> <button type="submit" name="btn" value="Push2" tabindex="8" accesskey="8">送信2</button></p> </form> </body> </html>
以下をファイル名 form.cgi で、上記htmlと同じディレクトリ内に置きます
#!/usr/local/bin/ruby -Ks
@in = Hash.new # フォームデータをこのハッシュに格納します
for q in ENV['QUERY_STRING'].to_s.split(/[;&]/) do
key,val = q.split(/=/,2)
@in[key] = val.gsub(/\+/," ").gsub(/%[a-fA-F\d]{2}/){ $&[1,2].hex.chr } if val
end
# 出力します↓
print <<KEYWORD
Content-Disposition: filename="get.txt"
Content-type: text/plain;charset=Shift_JIS
ENV['QUERY_STRING'] = #{ ENV['QUERY_STRING'] }
name値 => value値
---------- ---------------------------------------
KEYWORD
@in.each do |key,val|
puts "%10s => %s" % [key,val]
end
@in[name値] = value値
という形に整理しますENV['QUERY_STRING']
で受け取りますENV['QUERY_STRING']
が空っぽ(nil)だった場合に備えて、.to_s
で文字列化しておきます.split(/[;&]/)
は、;
か又は、&
を区切りとして配列に分解します。
⇒ 参照:Ampersands in URI attribute valuesq
をさらに、最初の =
を区切りにして、name値とvalue値にに分解します
は+
に直されるので、まず.gsub(/\+/," ")
で+
を空白
に戻します/%[a-fA-F\d]{2}/
を抜き出して、通常の文字に置き換えますval.gsub(/正規表現/){置換文字列}
は、val
の中で正規表現にマッチした部分を置換文字列に逐次置き換えます$&
は、/正規表現/
にマッチした部分を表します"usagi"[1,2]
は"sa"
です。0番目から数えて、1番目の文字から2文字抜き出しています。要するに、"%ab"[1,2]
は"ab"
。.hex
は16進数を書き表している文字列を、数に直します。すなわち、"ab".hex
は、10進数で表すなら定数 10*16+11 = 171.chr
は定数を、その番号が割り振られた文字に直しますチェックボックスなど、同じ name 値で複数の value 値を持たせたい場合があります。
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html lang="ja"> <head> <meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS"> <title>フォーム送信テスト</title> </head> <body> <h1>フォーム送信テスト</h1> <form action="form.cgi" method="GET"> <p>文字列: <input type="text" name="messe" value="ここに記入" tabindex="1" accesskey="1" size="36" maxlength="72"></p> <p>チェックボックス: <input type="checkbox" name="chk" value="rabbit" tabindex="2" accesskey="2">うさぎ、 <input type="checkbox" name="chk" value="panda" tabindex="3" accesskey="3">パンダ</p> <p>ラジオボタン: <input type="radio" name="danjo" value="otoko" tabindex="4" accesskey="4">男、 <input type="radio" name="danjo" value="onna" tabindex="5" accesskey="5">女、 <input type="radio" name="danjo" value="non" tabindex="6" accesskey="6" checked>ノーコメント</p> <p>ボタン: <button type="submit" name="btn" value="Push1" tabindex="7" accesskey="7">送信1</button> <button type="submit" name="btn" value="Push2" tabindex="8" accesskey="8">送信2</button></p> </form> </body> </html>
そういうときは、値を配列に入れてみましょう。
#!/usr/local/bin/ruby -Ks
@in = Hash.new
for q in ENV['QUERY_STRING'].to_s.split(/[;&]/) do
key, val = q.split(/=/,2)
@in[key] = [ ] unless @in[key]
@in[key] += [ val.gsub(/\+/," ").gsub(/%[a-fA-F\d]{2}/){ $&[1,2].hex.chr } ] if val
end
print <<KEYWORD
Content-Disposition: filename="get2.txt"
Content-type: text/plain;charset=Shift_JIS
ENV['QUERY_STRING'] = #{ ENV['QUERY_STRING'] }
name値 => value値
---------- ---------------------------------------
KEYWORD
@in.each do |key,val|
for v in val do
puts "%10s => %s" % [key,v]
end
end
@in[name値] = [ value値1, value値2, ... ]
という形に整理しますhtml に関しては、method="GET" を method="POST" に書き直しておいてください
form.cgi は、GET メソッドの場合のENV['QUERY_STRING']
をSTDIN.read
と取り替えれば、POST メソッドでもそのまま使えます
#!/usr/local/bin/ruby -Ks
@in = Hash.new
query = STDIN.read(102400).to_s # 標準入力を読み込みます(数 102400 は、読み込みの上限とするバイト数)
for q in query.split(/&/) do
key, val = q.split(/=/,2)
@in[key] = [ ] unless @in[key]
@in[key] += [ val.gsub(/\+/," ").gsub(/%[a-fA-F\d]{2}/){ $&[1,2].hex.chr } ] if val
end
print <<KEYWORD
Content-Disposition: filename="post.txt"
Content-type: text/plain;charset=Shift_JIS
STDIN.read = #{ query }
name値 => value値
---------- ---------------------------------------
KEYWORD
@in.each do |key,val|
for v in val do
puts "%10s => %s" % [key,v]
end
end
.read(102400)
読み込むデータのサイズに上限を設けた。この場合、100kb が上限。簡潔に.gets
を使って逐次読み込む方法もあります
#!/usr/local/bin/ruby -Ks
@in = Hash.new
while q = STDIN.gets("&") do
key, val = q.chomp("&").chomp.split(/=/,2)
@in[key] = [ ] unless @in[key]
@in[key] += [ val.gsub(/\+/," ").gsub(/%[a-fA-F\d]{2}/){ $&[1,2].hex.chr } ] if val
end
print <<KEYWORD
Content-Disposition: filename="post.txt"
Content-type: text/plain;charset=Shift_JIS
name値 => value値
---------- ---------------------------------------
KEYWORD
@in.each{|key,val| val.each{|v| puts "%10s => %s" % [key,v] }}
.chomp
:区切りの文字が末尾にくっついてくるので削除正規表現でのパターンマッチを利用した例
#!/usr/local/bin/ruby -Ks
@in = Hash.new
while /^([A-Za-z_\d\-\.]+)=([^&]+)&?$/ =~ STDIN.gets("&").to_s[0,20480] do
key = $1 ; val = $2
@in[key] = [ ] unless @in[key]
@in[key] += [ val.gsub(/\+/," ").gsub(/%[a-fA-F\d]{2}/){ $&[1,2].hex.chr } ] if val
end
print <<KEYWORD
Content-Disposition: filename="post.txt"
Content-type: text/plain;charset=Shift_JIS
name値 => value値
---------- ---------------------------------------
KEYWORD
@in.each{|key,val| val.each{|v| puts "%10s => %s" % [key,v] }}
たまにある質問として、下記のように混ぜたらどうなるか?というのがあります。
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html lang="ja"> <head> <meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS"> <title>フォーム送信テスト</title> </head> <body> <h1>フォーム送信テスト</h1> <form action="form.cgi?hoge=HogeHoge;hello=World" method="POST"> <p>文字列: <input type="text" name="messe" value="ここに記入" tabindex="1" accesskey="1" size="36" maxlength="72"></p> <p>チェックボックス: <input type="checkbox" name="chk1" value="rabbit" tabindex="2" accesskey="2">うさぎ、 <input type="checkbox" name="chk2" value="panda" tabindex="3" accesskey="3">パンダ</p> <p>ラジオボタン: <input type="radio" name="seibetu" value="otoko" tabindex="4" accesskey="4">男、 <input type="radio" name="seibetu" value="onna" tabindex="5" accesskey="5">女、 <input type="radio" name="seibetu" value="non" tabindex="6" accesskey="6" checked>ノーコメント</p> <p>ボタン: <button type="submit" name="btn" value="Push1" tabindex="7" accesskey="7">送信1</button> <button type="submit" name="btn" value="Push2" tabindex="8" accesskey="8">送信2</button></p> </form> </body> </html>
データの受け取りは、単純に2段重ねするだけです。これで POST(添付書類なし) にも GET にも、両方対応できます。
#!/usr/local/bin/ruby -Ks
@in = Hash.new
query = ENV['QUERY_STRING'].to_s # GET からのクエリー
for q in query.split(/[;&]/) do
key, val = q.split(/=/,2)
@in[key] = [ ] unless @in[key]
@in[key] += [ val.gsub(/\+/," ").gsub(/%[a-fA-F\d]{2}/){ $&[1,2].hex.chr } ] if val
end
query = STDIN.read(102400).to_s # POST からのクエリー
for q in query.split(/&/) do
key, val = q.split(/=/,2)
@in[key] = [ ] unless @in[key]
@in[key] += [ val.gsub(/\+/," ").gsub(/%[a-fA-F\d]{2}/){ $&[1,2].hex.chr } ] if val
end
print <<KEYWORD
Content-Disposition: filename="get_and_post.txt"
Content-type: text/plain;charset=Shift_JIS
ENV['CONTENT_TYPE'] = #{ ENV['CONTENT_TYPE'] }
ENV['REQUEST_METHOD'] = #{ ENV['REQUEST_METHOD'] }
ENV['QUERY_STRING'] = #{ ENV['QUERY_STRING'] }
STDIN.read = #{ query }
name値 => value値
---------- ---------------------------------------
KEYWORD
@in.each do |key,val|
for v in val do
puts "%10s => %s" % [key,v]
end
end
同じことの2度繰り返しになるので、予めやることを定義しておいた方がすっきりするかもしれません
#!/usr/local/bin/ruby -Ks
def q2h(query="")
for q in query.to_s.split(/[;&]/) do
key, val = q.split(/=/,2)
@in[key] = [ ] unless @in[key]
@in[key] += [ val.gsub(/\+/," ").gsub(/%[a-fA-F\d]{2}/){ $&[1,2].hex.chr } ] if val
end
end
@in = Hash.new
q2h( ENV['QUERY_STRING'] ) # GET からのクエリー処理
q2h( STDIN.read(102400) ) # POST からのクエリー処理
print <<KEYWORD
Content-Disposition: filename="get_and_post.txt"
Content-type: text/plain;charset=Shift_JIS
ENV['CONTENT_TYPE'] = #{ ENV['CONTENT_TYPE'] }
ENV['REQUEST_METHOD'] = #{ ENV['REQUEST_METHOD'] }
name値 => value値
---------- ---------------------------------------
KEYWORD
@in.each do |key,val|
for v in val do
puts "%10s => %s" % [key,v]
end
end
フォームにファイルの添付タグをつけた html ファイル
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html lang="ja"> <head> <meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS"> <title>フォーム送信テスト</title> </head> <body> <h1>フォーム送信テスト</h1> <form action="form.cgi" method="POST" enctype="multipart/form-data"> <p>文字列: <input type="text" name="messe" value="ここに記入" tabindex="1" accesskey="1" size="36" maxlength="72"></p> <p>添付書類: <input type="file" name="tenpu" value="" tabindex="2" accesskey="2" size="36"></p> <p>チェックボックス: <input type="checkbox" name="chk" value="rabbit" tabindex="3" accesskey="3">うさぎ、 <input type="checkbox" name="chk" value="panda" tabindex="4" accesskey="4">パンダ</p> <p>ラジオボタン: <input type="radio" name="danjo" value="otoko" tabindex="5" accesskey="5">男、 <input type="radio" name="danjo" value="onna" tabindex="6" accesskey="6">女、 <input type="radio" name="danjo" value="non" tabindex="7" accesskey="7" checked>ノーコメント</p> <p>ボタン: <button type="submit" name="btn" value="Push1" tabindex="8" accesskey="8">送信1</button> <button type="submit" name="btn" value="Push2" tabindex="9" accesskey="9">送信2</button></p> </form> </body> </html>
form.cgi :たぶん、一番簡単な例
#!/usr/local/bin/ruby -Ks
@in = Hash.new
ct = ENV['CONTENT_TYPE'].to_s
boundary = nil # 境界の目印
boundary = $1 if /multipart/i =~ ct && /boundary=([^\r\n]+)\r?$/i =~ ct
STDIN.read(102400).to_s.each( boundary ) do |q|
q.sub!(/\A\r?\n[\r\n]*/,'') # 先頭の空行を削除
q.sub!(/(\r?\n)\-\-#{ boundary }\Z/,'') # 後端の余分を削除
head,q = q.split(/\r?\n\r?\n/,2) # 最初の空行でヘッダとボディに分離
cd = nil # Content-Disposition
cd = $1 if /^Content\-Disposition:\s*([^\n]+)\r?$/i =~ head.to_s
next unless cd # データ形式不備のものをスキップ(初端終端のゴミも排除)
key = nil # name 値
key = $1 if /\bname="([^"]+)"/ =~ cd
next unless key # name 値が拾えなかった場合はスキップ
@in[key] = Array.new unless @in[key] # 配列を準備
filename = nil # 添付されたファイルのファイル名
filename = $1 if /\bfilename="([^"]*)"/ =~ cd
next if filename && q.to_s.size == 0 # 添付ファイルの空振りはスキップ
ct = nil # 添付されたファイルのタイプ(送信者のブラウザが拡張子から判断)
ct = $1 if /^Content\-Type:\s*([^\n]+)\r?$/i =~ head
if ct then # 添付ファイルの場合
@in[key] += [{"filename",filename, "type",ct, "size",q.size , "body",q }]
else # 添付ファイルではない場合
@in[key] += [ q ]
end
end if boundary
print <<KEYWORD
Content-Disposition: filename="multipart.txt"
Content-type: text/plain;charset=Shift_JIS
ENV['CONTENT_TYPE'] = #{ ENV['CONTENT_TYPE'] }
ENV['REQUEST_METHOD'] = #{ ENV['REQUEST_METHOD'] }
name値 => value値
---------- ---------------------------------------
KEYWORD
@in.each do |key,val|
for v in val do
puts "%10s => %s" % [key,v] if v.class == String
puts "%10s => filename=%s, type=%s, size=%d" % [key,v["filename"],v["type"],v["size"]] if v.class == Hash
end
end
STDIN.read
で読み込んだデータを、ENV['CONTENT_TYPE']
から読み取った境界の目印で切り分け、それをさらにヘッダ部分(head)と内容(body = value値)に切り分けますENV['CONTENT_TYPE']
から境界のキーワードを抜き出します。
STDIN.read
で読み込んだデータを、境界のキーワードで切り分けて、逐次処理します。
.to_s
で文字列化してあります.each( boundary ) do |q|
を適用すると、boundaryを終端とする文字列に区切って、変数q
に逐次写し取ります.class
:添付されたファイルに関しては、内容を表示することができないので、ファイル名などを表示することにします。内容が文字列(String)になっているか、ハッシュ(Hash)になっているかどうかで、添付ファイルの情報かどうかを区別しています.read
で一度に読み込むことはせずに、.gets
で逐次読み込み、ファイルは一時的に保存してしまうのが良いでしょう。★改良されたサンプル ⇒ [ ZIP 圧縮ファイル form_mp.cgi ]
★以上全部組み合わせて、全部の送信形式に対応させると ⇒ [ ZIP 圧縮ファイル form_all.cgi ]
以上です。