レコードの集合を永続的に保存させ,いつでも取り出せ,さらに加工が できるようにするシステムをデータベース管理システム(DBMS)という。 実用されているDBMSはデータの保存,取り出し以外の豊富な機能を含む 巨大なシステムであるが,簡単なものはRubyプログラムで容易に実装できる。
テキストファイルの解析と書き出しはopenメソッドで行なう(既習範囲)。
csvを読んで加工し,csvに書き出す処理は, csvライブラリ を利用する。ただし遅いので(Ruby1.8) 実用性を求めるなら FasterCSVやcsvscanをインストールして使うことになる。
Unixシステムの多くはdbmという汎用的なデータベースライブラリを 備えている。簡易データベースはこれで十分作成できる。Rubyでは 添付ライブラリの dbm を介して簡単に利用できる。
dbmによるデータベースは Hash と同様 key と value の対の集合をデータとする。ただし,Hashと 違い,keyとvalueがともに文字列(String)でなければならない。
csvで書かれたデータを読み込み,どんどんdbmに追加する例を示す。 csvのレコードは第1フィールドがキーと見なせるとする(重複するものが ないと保証できる)。
#!/usr/bin/env ruby # coding: euc-jp require 'dbm' # dbmクラスを利用 datafile = "./database" # データベース名 # CSVデータを読んでハッシュにいれておく data = Hash.new while csv=gets # 第1フィールドをキー,残りをごっそり文字列として値にする if /([^,]*),(.*)/ =~ csv data[$1] = $2 end end # ndbm形式のデータベースを開く DBM.open(datafile) do |x| # 開いた時点で既にデータがあれば x に入った状態でスタートする for key, val in data # data の key, val ペアを取り出して繰り返す x[key] = val end end
走らせるとデータベース名に .dir .pag
を付加したファイルができる(BSDの場合は *.db
)。
市のデータファイル
と町のデータファイル,
村のデータファイルを順次処理した推移を観察しよう。
./dbm-add.rb yama-city.csv ls -l data* -rw-r--r-- 1 yuuji wheel 16384 Apr 27 11:35 database.db strings database.* | less (中を確認して q で終了) ./dbm-add.rb yama-town.csv ls -l data* strings database.db | less ./dbm-add.rb yama-vil.csv
strings
コマンドは何をするものか調べよ。
(n)dbm形式のデータファイルは makedbm -u
で
テキスト部分を抽出できる。
●Solarisの場合 makedbm -u database ●NetBSDの場合 makedbm -u database.db
作成したデータベースを読み込むプログラムも簡単で,
DBM.open
を使えばよい。
#!/usr/bin/env ruby # coding: euc-jp require 'dbm' # dbmクラスを利用 datafile = "./database" # データベースファイル名 # ndbm形式のデータベースを開いて順次レコードを出力 DBM.open(datafile, 0666, DBM::READER) do |x| # DBM.open(datafile) だけで開いてもよいが, # 読み込みだけのときは第3引数を DBM::READER とする for key, val in x # x の key, val ペアを取り出して繰り返す printf("%s -> %s\n", key, val) end end
データ追加,ダンプ,検索の機能を選べるようにした プログラム例を示す。
#!/usr/bin/env ruby # coding: euc-jp require 'dbm' # dbmクラスを利用 datafile = "./database" # データベースファイル名 if ARGV[0] == nil then STDERR.puts "#{$0} -a [Data.csv] Add record" STDERR.puts "#{$0} -d Dump database" STDERR.puts "#{$0} -s Search on database" exit 1 end case ARGV.shift when "-a" # データの追加登録 # CSVデータを読んでハッシュにいれておく data = Hash.new DBM.open(datafile) do |x| # DBM.openしてからデータ入力を行なう処理は好ましくない(後述) while csv=gets # 第1フィールドをキー,残りをごっそり文字列として値にする if /([^,]*),(.*)/ =~ csv #data[$1] = $2 x[$1] = $2 end end end when "-d" # 一覧出力 DBM.open(datafile, 0666, DBM::READER) do |x| for key, val in x printf("%s -> %s\n", key, val) end end when "-s" # 検索 STDERR.print "検索キー: " kwd = STDIN.gets.chomp! # STDIN無しだと -s というファイルから読む reg = Regexp.new(kwd, nil, 'e') # 大文字小文字区別,EUCで検索 DBM.open(datafile, 0666, DBM::READER) do |x| for k in x.keys if reg =~ k then # マッチしたレコードのみ出力 printf("%s -> %s\n", k, x[k]) end end end end
dbm-ops.rb
を実行してみよ。
ただし,このプログラムの追加登録部分は 頻繁なデータ更新が必要な場合に効率低下を招く潜在的な問題を含んでいる。
データのキー削除には delete メソッドを用いる。
db.delete(key)
とすると該当するキーと値を削除できる。
Ruby固有の汎用的なデータ永続化クラスが PStore で,ほぼ全てのオブジェクトがファイルに保存できる。 また,dbmと違いひとつのデータファイルに複数のRubyオブジェクトを格納できる。
たとえば,ハッシュと配列の2つの集合値を保存したい場合, 以下のような手順で格納できる。
require "pstore" db変数 = PStore.new(保存ファイル) db変数.transaction do # db変数を利用した処理 end
db変数 は db変数[ルート名] の形で,ルート名 で代表される任意のオブジェクトにアクセスでき, そのオブジェクトは transaction 終了時に保存される。
以下の例は,
をそれぞれ格納し,データファイル ps-data に保存するものである。
#!/usr/bin/env ruby # coding: euc-jp require 'pstore' datafile = "ps-data" # 初期値のハッシュをPStoreファイルに保存 # このプログラムを3回実行したのち ps-list.rb を実行する db = PStore.new(datafile) db.transaction do m = db["山のデータ"] = db.fetch("山のデータ", Hash.new) # これでmが保存可能なハッシュになる m["富士山"] = 3776 m["月山"] = 1984 m["鳥海山"] = 2236 e = db["偶数"] = db.fetch("偶数", Array.new) # これで e が保存可能な配列になる e << 2 e << 4 e << 6 # 2,4,6を順次配列に追加する end
上記のプログラムを3回実行後,以下のプログラムを実行してみる。
#!/usr/bin/env ruby # coding: euc-jp require 'pstore' datafile = "ps-data" # datafileに登録されているものを取り出す。 db = PStore.new(datafile) db.transaction do m = db["山のデータ"] = db.fetch("山のデータ", Hash.new) e = db["偶数"] = db.fetch("偶数", Array.new) # 既存データがある場合でも同じ代入方法でよい puts("山のデータのハッシュ") # mを順次出力 for k, v in m printf("%s\t-> %4d\n", k, v) end puts("偶数の配列") # mを順次出力 puts e.join(", ") end
./ps-list.rb
山のデータのハッシュ
月山 -> 1984
富士山 -> 3776
鳥海山 -> 2236
偶数の配列
2, 4, 6, 2, 4, 6, 2, 4, 6
このように,前回の値が残ったまま代入されるので, 配列には値が積み重なってゆく。
「商品」と「単価」のkey, valueペアを持つハッシュを永続的に 持ち,追加で増やせるプログラム例を示す。
#!/usr/bin/env ruby # coding: euc-jp require 'pstore' datafile = 'pricedb' def showall(hash) for k, v in hash printf("%-20s%4d円\n", k, v) end end db = PStore.new(datafile) db.transaction do price = db["price-list"] = db.fetch("price-list", Hash.new) while true STDERR.print "商品=単価 の形式で入れて下さい(C-dで終了): " break if (line=gets) == nil redo if /([^=]*)=([\d ]*)/ !~ line # マッチしなかったら redo price[$1] = $2.to_i end puts "\n全商品リストです" showall(price) end
プログラム中で指定している "price-list"
は,
ルート名といい,任意のルート名に対応する値を定義し,それを保存できる。
ひとつのpstoreファイルに複数のルートを持たすことができる。