データを永続させるCGI

アンケートなどの処理では過去に登録された値全てを累積的に 保存しておき、統計処理をしたい。ところがCGIでは、 CGIスクリプトが呼び出されて終了する間しか変数の値は 生きていない。つまり、<form>のあるWebページで入力された値は CGIスクリプトの終了とともに消えてしまう。入力名に入れられた値を残すためには、 そのときの変数などの状態をファイルに保存して、 次の起動時にそれを読み込むようにしなければならない。

Rubyでは、現在の変数の値をそのままの形でファイルに保存することが 容易にできる。これにはPStoreクラスを使う。

PStoreクラス

PStoreクラスを用いると数値、文字列、配列、ハッシュ、どんな値でも 直接ファイルに保存できる。文字列を保存することはこれまでも openで できたのでPStoreで保存しても面白くない。ここではハッシュの値を 保存する例を示す。以下の例は変数xを介して保存させる。

require 'pstore'
x = PStore.new("値保存ファイル")
x.transaction do
  x["foo"] = x["foo"] || Hash.new	# x["foo"] が空なら新規ハッシュ代入
  保存したい変数 = x["foo"]
     :
     :
end

実際に動く短い例を見ていこう。

PStoreクラスの利用法

最初はCGIではなく普通のRubyスクリプトで、途中で設定した変数を 保存させるようにしていく例を見てみよう。「名前」と「一言」を読み込んで出 力するだけのプログラムを作ってみる。あとから複数の値のペアを追加すること を見越して、ハッシュを利用する。

word.rb

#!/usr/koeki/bin/ruby
# coding: utf-8

word = Hash.new

print "名前は?: "
name = gets.chomp
print "ひとこと: "
word[name] = gets.chomp

for who, wd in word	# または word.each do |who, wd|
  printf("%sさんのひとこと「%s」\n", who, wd)
end

最後で、ハッシュに含まれるキーと値のペアを全て出力しているが、 プログラムを一度動かしても1つのキーと値しか登録されないので 何度実行しても1人の言葉しか出てこない。

% ./word.rb
名前は?: taro
ひとこと: hoge
taroさんのひとこと「hoge」
% ./word.rb
名前は?: hanako
ひとこと: やほー
hanakoさんのひとこと「やほー」
% ./word.rb
名前は?: John
ひとこと: Hey, Jude
Johnさんのひとこと「Hey, Jude」

これをPStoreを使って、そのときのハッシュ全体の値を別ファイル (以下の例ではword.db)に保存して毎回読み込むようにする。

word-pstore.rb

#!/usr/koeki/bin/ruby
# coding: utf-8

require "pstore"
x = PStore.new("word.db")
x.transaction do
  x["word"] = x["word"] || Hash.new	# x["word"] ||= Hash.new でも可
  word = x["word"]

  print "名前は?: "
  name = gets.chomp
  print "ひとこと: "
  word[name] = gets.chomp

  for who, wd in word
    printf("%sさんのひとこと「%s」\n", who, wd)
  end
end

このようにすると、以前に起動された値を word.db ファイル に自動的に保存し、前回の値を引き継ぐことができる。

% ./word.rb
名前は?: taro
ひとこと: hoge
taroさんのひとこと「hoge」
% ./word.rb
名前は?: hanako
ひとこと: やほー
hanakoさんのひとこと「やほー」
taroさんのひとこと「hoge」
% ./word.rb
名前は?: John
ひとこと: Hey, Jude
Johnさんのひとこと「Hey, Jude」
hanakoさんのひとこと「やほー」
taroさんのひとこと「hoge」

CGIでの利用

word-pstore.rbをCGIにしたものを示す。 次節で述べるが、このCGIを保存するディレクトリは別に作成する 必要がある。

word-pstore-cgi.rb

#!/usr/bin/env ruby
# coding: utf-8

myname="word-pstore-cgi.rb"
require "cgi"
c = CGI.new(:accept_charset => "UTF-8")
require "pstore"
x = PStore.new("data/word.db")	# 別ディレクトリにする

print "Content-type: text/html; charset=UTF-8\n\n"

print "<!DOCTYPE html>
<html>
<head><title>Word</title></head>
<body>"

# 値入力フォームもこのCGIで出力する。
# formのactionをこのCGIプログラムに指定している。
# (mynameはこのスクリプト名)
printf("<form method=\"POST\" action=\"./%s\">\n", myname)
print ' <p>
おなまえ: <input name="name" maxlength="40"><br>
ひとこと: <input name="word" maxlength="80"><br>
<input type="submit" value="GO">
<input type="reset" value="reset">
</p></form>'

x.transaction do
  x["word"] ||= Hash.new
  word = x["word"]
  if c["name"] > "" && c["word"] > ""
    name = c["name"]
    word[name] = c["word"]
  end
  print "<pre>\n"
  for p, w in word
    # フォーム入力値を出力するときは必ず CGI.escapeHTML() する
    person = CGI.escapeHTML(p)
    wrd = CGI.escapeHTML(w)
    printf("%sさんのひとこと「%s」\n", person, wrd)
  end
  print "</pre>"
end
puts "</body></html>"

データを保存するCGIの実行

CGIプログラムは自分のユーザ権限で動かない。Webサーバプログラムは 違うユーザ権限で動いているため、自分の保有するディレクトリに書き込むこと がそのままではできない。CGIプログラムにデータファイルを書かせるためには 当該ディレクトリを他人でも書き込めるように設定しておかなければならない。

データを書き込むディレクトリを別途作成する。作成中の CGI プログラムが ~/public_html/mycgi であると仮定するとその中に data/ ディレクトリを作成する。

mkdir ~/public_html/mycgi/data
chmod 1777 ~/public_html/mycgi/data

chmodの 1777 は、 誰にでも書き込みできるが既存ファイルを消すのはファイル所有者のみ、 という属性をもつディレクトリに設定することを意味する。 ~/public_html/mycgi/word-pstore-cgi.rb を保存し chmod +x しておく。

実際に動かす例を word-pstore-cgi に示す。

実用CGIスクリプトへ

自分用データベース

PStoreクラスを利用して、Webで入力できる自分用のデータベースを作ろう。

ここでは、何かの飲み物を飲んだときの感想を登録できるデータベースを 作る。入力名として、

飲み物の名前item
感想comment

を利用する。また、入力フォームの出力と、 現在登録されているデータの出力を両方とも行なうようなCGIプログラムとする。 大まかな構成は以下のようになる。

  1. HTMLヘッダ等の出力
  2. データ入力用フォームの出力
  3. その時点で何か値が入力されていたらそれをハッシュに追加
  4. 既存データの値一覧を出力

これをプログラム化した例が cancoffee.rb である。

#!/usr/bin/env ruby
# coding: utf-8

require "cgi"
require "pstore"
myname = "cancoffee.rb"

c = CGI.new(:accept_charset => "UTF-8")
item = c["item"]
cmt  = c["comment"]
time  = Time.now		# 時刻を保持するTimeクラス代入。nowは現時刻

print 'Content-type: text/html; charset=UTF-8

<!DOCTYPE html>
<html>
<head><title>飲んだものメモ</title>
<link rel="stylesheet" type="text/css" href="simple.css">
</head>
<body>
<h1>飲み物メモ</h1>
'				# HTTPヘッダと冒頭部分
db = PStore.new("data/coffee.db")
db.transaction do		# PStoreは db.transaction do ... end で使う
  db["root"] ||= Hash.new
  data = db["root"]		# ここまではおきまり

  if item >"" && cmt > ""	# 名前とコメント、両方値があるなら登録
    data[item] = [time, cmt]	# 今日の日付とコメント
  end

  # フォーム出力
  printf("<form method=\"POST\" action=\"./%s\">\n", myname)
  print '<p>
飲んだもの: <input name="item" type="text" maxlength="40"><br>
コメント <br>
<textarea name="comment" cols="40" rows="5">
</textarea><br>
<input type="submit" value="OK">
<input type="reset" value="reset"><br>
</p><hr>'

  # 既存のコメント出力(キー毎)
  print "<dl>\n"		# 定義環境開始
  for i in data.keys.sort{|x, y|
      data[y][0] <=> data[x][0]	# 日付の新しい順にソート
    }
    day = data[i][0]		# 第0要素が日付
    msg = data[i][1]		# 第1要素がコメント、それぞれ取り出す
    printf(" <dt>%s</dt>\n", i)	# キー(つまり飲んだものの名前)
    printf(" <dd>記載日: %s<br>\n", day.strftime("%Y年%m月%d日"))
    printf("     %s</dd>\n", CGI.escapeHTML(msg))
  end
  print "</dl>\n"		# 定義環境終了
end				# db.transaction 終わり

print "</form><hr></body>\n</html>"

実際に登録してみよう。

項目修正が容易なデータベース

cancoffee.rb では既存項目を入力すると、古い項目のコメントが消され、新しく入力されたコ メントに置き換えられる。これにより後で修正することは可能だが、 入力ミスしたものなどを直す場合、 既存の項目の値を初期値として代入した状態 で入力フォームが出力されていると 都合がよい。そのように直したものを cc2.rb に示す。

#!/usr/bin/env ruby
# coding: utf-8

require "cgi"
require "kconv"			# utf-8への変換のため(toutf8メソッド)
require "pstore"
myname = "cc2.rb"

c = CGI.new(:accept_charset => "UTF-8")
item = c["item"]
cmt  = c["comment"]
time  = Time.now		# 時刻を保持するTimeクラス代入。nowは現時刻

edit_item = ARGV[0].to_s.toutf8	# 修正モードの場合の修正項目名
oldcomment = ""			# 修正モードの場合の既存コメント

print 'Content-type: text/html; charset=UTF-8

<!DOCTYPE html>
<html>
<head><title>飲んだものメモ</title>
<link rel="stylesheet" type="text/css" href="simple.css">
</head>
<body>
<h1>飲み物メモ</h1>
'				# HTTPヘッダ
db = PStore.new("data/coffee.db")
db.transaction do		# PStoreは db.transaction do ... end で使う
  db["root"] ||= Hash.new
  data = db["root"]		# ここまではおきまり

  if item >"" && cmt > ""	# 名前とコメント、両方値があるなら登録
    data[item] = [time, cmt]	# 今日の日付とコメント
  end

  if edit_item > "" && data[edit_item]
    oldcomment = data[edit_item][1]
  end
  # フォーム出力
  printf("<form method=\"POST\" action=\"./%s\">\n", myname)
  print '<p>
飲んだもの: <input name="item" type="text" value="' +
    edit_item + '" maxlength="40"><br>
コメント <br>
<textarea name="comment" cols="40" rows="5">' + oldcomment + '
</textarea><br>
<input type="submit" value="OK">
<input type="reset" value="reset"><br>
</p><hr>'

  # 既存のコメント出力(キー毎)
  print "<dl>\n"		# 定義環境開始
  for i in data.keys.sort{|x, y|
      data[y][0] <=> data[x][0]	# 日付の新しい順にソート
    }
    day = data[i][0]		# 第0要素が日付
    msg = data[i][1]		# 第1要素がコメント、それぞれ取り出す
    # <a href="./cc2.rb?ITEM">ITEM</a> を出力
    printf(" <dt><a href=\"%s?%s\">%s</a></dt>\n", myname, i, i)
    printf(" <dd>記載日: %s<br>\n", day.strftime("%Y年%m月%d日"))
    printf("     %s</dd>\n", CGI.escapeHTML(msg))
  end
  print "</dl>\n"		# 定義環境終了
end				# db.transaction 終わり

print "</form><hr></body>\n</html>"

目次