さあ、Windows依存環境からの脱出だ!まず、AccessからUNIXで作れるウェブアプリに家計簿を全面移行するぞ。この作業は不思議とあんまりドラマティックなヘマはなかったです。淡々と結果だけ書きます。
今までMS-Accessで作っていた家計簿をWebアプリケーションに移行することにした。そのためにWebサーバとしてApache、データベースはPostgreSQL、インターフェイス言語はPer5lと決めた。Perlについてはぜーんぜん知らなかったので7月中に2週間ほどかけて
「初めてのPerl(今更記すまでもないと思うがSchwartz, R.L., Christiansen.T著、近藤嘉雪訳、オライリー・ジャパン発行、オーム社発売)」
をおさらいした。とどめは
「オープンソースを使ったデータベースの構築(藤田泰徳・山崎文則共著、セレンディップ発行、小学館発売)」(通称「三位一体の書」)(って勝手に呼ぶな)
で、これら3者を用いたデータベースの開発に着手する大いなる自信を得た。
家計簿データのカテゴリ等の構造そのものは、Accessで作ったのとほとんど同じである。多少呼称などが変わっているが。
参考図面1 に、各ウェブページ(長方形)の関係とそこで処理されるデータベース(楕円形)、送信(緑色)もしくはURLに指定する形(ピンク色)で送られるパラメータを図示した。Index.htmlは見出し。そこから何種類かの入力フォームにリンクする。これらのフォームから形式別に3つのテーブル(黒線に赤字)に期日、品物、価格などのデータが入力される。
さて月の集計にあたり、該当月のデータだけを抽出するのがmonthlypro.cgiである。これによりそれぞれのテーブルから該当月分だけを選択する3つのビュー(青線に黒字)が生成される。ビューは月ごとに年月番号をつけて保存される。図では年月番号をsufで表している。具体的には2001年8月分だとsuf=200108となる。これから年月を経るにつれて年月番号のついたビューがどんどん蓄積されることになる。
ほとんどの集計計算を行うのはcalctotal.cgiである。ここで食費、雑貨、コンピュータ関係などの分類ごと出費額が集計され、まとめられる。集計の家庭で作られるのはビューだけであるが、参照用のテーブルや家賃のような定額出費をテーブルにしたものが集計に関わっている。ビューは同様年月番号つきで蓄積される。最終結果もビューで、これを表示するのがoutotal.cgiである。
さらに月間集計を行ったら月ごとの比較が必要である。さすがに複雑になるのでビューではなく実体テーブルを作ることにした。それを作るのがclmナントカ.cgiで、cumulationの略でcmlにしようと思ったのをスペル間違えたのだ(テーブルの名前はちゃんとなった)。そのcmlテーブルを表示するのがoutcmナントカ.cgiである。
では実際に何を何でどう処理するのか。
データベーステーブルpurchaseは、食料品や雑貨、本、コンピュータ、及び冬に買う灯油用にそれぞれ用意した入力フォームによりデータ入力される。
dailypro.cgiはそのうち分類2=食費、生活雑貨、趣味、その他の日用品の入力のためのフォームである。主に毎日スーパーで買い物をしたときにこのフォームから入力する。
分類2=食費はさらに分類1=主食、肉魚卵、野菜などに細分されそれぞれの出費を見ることも必要である。一方食費の場合それぞれの分類1ごとに、買う物はたいてい決まっている。たとえば主食=食パン、スパゲティ、マーガリン、ジャムなど。そこで今回は分類1ごとに頻出項目のスクローリングリストを作って1ページ内に全部配置する。生活雑貨も同様にリスト式にする。当然分類に関わらずリスト外の品目を入力するためのテキストボックスも用意し、これにスクローリングリストから該当すると思われる分類1を選択できるようにする。まとめると、この入力フォームでは一品ごとに、日にちと、スクローリングリストから品目を選択、もしくはテキストボックスに品目を入力と同時にスクローリングリストから分類1を選択、あとスクローリングリストから個数を選択する。
分類1ごとに用意したスクローリングリストは、それぞれの初期値を、分類1自身の名前にしておく。フォームを開いたときには分類1の名前を冠したスクローリングリストが並んでいるので、食パンを買った場合は「主食」が選択されているスクローリングリストをつついて「食パン」に選択し直して送信することになる。
処理系統が受け取るべきパラメータは年、月、日、品目、個数。しかしこの場合、実は1個のスクローリングリストから選択された品目と同時に、選択がなされなかった他のすべてのスクローリングリストの初期値がそれぞれ、一回の送信のたびに送られている。
処理系統は、月、日、個数の値はそのまま受け取る。あとすべてのスクローリングリストから送られた値についてひとつひとつ検討する。判断基準はそれが初期値と同じかどうかである。この場合、選択された「主食」のスクローリングリストだけ、初期値「主食」ではなく「食パン」という値が送られてきている。ゆえにこれを品目に代入し、該当する「主食」を分類1に代入する。分類2は自動的に「食費」になる。
テキストボックスへの入力は、すべてのスクローリングリストから初期値が送られてきたときに受け付けられる。入力があったときにこれを優先して受け付けるという考え方もあるがどっちでもいいかも。テキストボックスからの送信値が品目、付属のスクローリングリストからの送信値が分類1となり、分類1に応じて分類2が処理系統によって決定される。
Book.cgiは書籍用の入力フォームで、一応分類1として雑誌と書籍を用意し、ラジオボタンで選択する。あと雑誌はスクローリングリストから入力できるようにする。
comp.cgiはコンピュータ用の入力フォームで、分類1として既成(実は既製が正しいがAccessのころからずっと間違ったまま)マシン、外付け、パーツなどに細分される。品名はテキストボックスに入力し、分類1をスクローリングリストから選択する。
fuel.cgiは品名灯油、分類1灯油、分類2は光熱費と決まっているので、月日と価格を入力するだけである。
今回は加えて特別のフォームを作った。それがConvini.cgiである。休日のお昼は近くのコンビニで買う(作れよ)のだがだいたい買う物は決まっている。パンかおにぎりかコンビニランチを1,2個ずつにお飲み物2個、あとヨーグルト2個というところだ。これをわざわざ1個ずつ、同じようなものを選択・入力して送信するのは非常にめんどくさい。そこで「コンビニお買い物専用」のフォームを作った。
これはかなりわが家に限定されたフォームと言えよう。「パン」「おにぎり」「ランチ」「飲み物」「ヨーグルト」ごとに価格入力のためのテキストボックス、個数選択のためのスクローリングリストのペアを、それぞれ4つずつ用意する。4つというのは買う数が限られている上に同じ値段のものはひとつのテキストボックスで個数を複数にすればいいからまずこれを越えることはないという考えによる。これらのテキストボックス及びスクローリングリストの値は毎回送信されると、処理系統に用意された配列に代入される。配列は「パン配列」「おにぎり配列」などとして用意されてあり、「パン配列の偶数番目の要素=価格、その次の要素=個数」は、品目=パン、分類1=主食、分類2=食費、価格、個数、としてpurchaseテーブルに入力されるのである。Perlは配列に何も入力されない場合は黙って何もしないでいてくれるから問題ない。これでコンビニのレシート処理がぐっと楽になった。
以上がpurchaseテーブル用の入力フォームである。
drawingテーブルは引き落とし額のデータテーブルで、対応する入力フォームはdrawing.cgi一個。日にちと引き落とし額をテキストボックスに入力、スクローリングリストから電気、ガス、水道、電話会社などの項目を選択して送信する。この項目は分類1に該当する。
outgoneテーブルはおでかけしたときの日にち、おでかけ場所、おひる場所のデータテーブルである。入力フォームはやはりoutgone.cgi一個。しかしいつもおでかけする場所はあの電気街かこの電気街だしそこでおひるはマクドナルドかケンタッキーと決まっているので、日にちとスクローリングリストで場所とおひるを選択するだけ。実家に帰るなど特別な旅行があった場合は特別会計として算出する。
これらの入力フォームの大きな特徴は、アンサーページがないということである。多くのCGIアプリケーションでは入力フォームはhtmlファイルで、<FORM>タグ内にACTION="CGIファイル名"と書いて送信データを別のファイルに引き渡し、そこで処理してアンサーページを返す。だがそれだとデータを入力する度にもとの入力フォームのページに戻ってすべての値を入れ直さなければならない。その日スーパーでお買い物に行くと多くて10項目程度のデータを入れることになる。非常にめんどくさい。それに対して、「初めてのPerl」で何事もないように例示されているこの方法は、1個のcgiファイルで入力フォームの表示、データの入力、それの処理と処理結果の出力を全部やってしまうものである。その構造は
html開始
if (param()){
my $a = param("input1")
...
データ処理して結果を出力する
}
入力フォームを表示して入力させる
送信させる
フォーム終了
html終了
となっている。つまりページを開いたときにはif(param())の条件が満たされないのでそれはただスキップされ、入力フォームの表示から始まる。入力して送信するとif(param())以下のカッコの中の処理が実行され、結果が表示される。だがこのif文のあとにはelseがない。これはプログラミングになれた人には当たり前のことかもしれないがのに子のような初心者には感動もののワザに思える。つまり送信があってカッコの中に入っても、処理が終わったらまたカッコを抜けてその下の入力フォーム以下が実行される。送信しては次の入力、ができることになるのだ。おまけにファイルが読み直されているわけではないのでこのとき前の入力結果は保存される。だから日付などの部分は入れ直す必要がない。では品目や個数など別の値を入れたい部分はというとコントロールのパラメータにoverride=>1と書いておくと更新してくれる。非常に頭のいい人が考えた方式なのではないだろうか。
さらにPerlっていうかCGI.pmにはhtmlを表示させるための独自の関数がいろいろ入っているので、多くの場合は
print (<タグ> ぐだぐだぐだ</タグ>) ;
と書く代わりに、
print タグ "ぐだぐだぐだ";
ですませることができる。これは文字キーはブラインド慣れしているが記号や数字キーは練習をサボっている、つまり「激打」でシンなどは撃破できるくせに「名も無き街」でジードごときにコテンパンにやられるタイプの人間には、<だの/だのを打たなくていいので非常に助かる。この関数表記方式ではサイズなど細かいオプションの指定や、太字にしてかつリンクを張るとかいう複合ワザなど、かなり自由に書式を指定することができるようだが、残念ながらその詳細を書いた本がうちにはない。たぶん「プログラミングPerl」とか「CGIリファレンス」など他の動物本にあるのかもしれないが、今のところ付属の/man3/CGI3をつぶさに探すことによりほとんどの問題は解決できている。
その月の集計はまず日付を指定して送信するフォームから始まる。
日付の抽出、というのはAccess家計簿時にずいぶん悩まされた問題ではあった。だが今回それがいともあっさり片づいた・・・Accessの問題ではない。あれにはあれでクソのような問題がたくさんあるが、今の問題は違う。
家計簿集計はやっぱりどうしても、お給料日からお給料日までの一ヶ月にしたいのが人情である。そこでめんどくさくなるわけだ。8月分ならmonth="8"のデータを抽出する、というわけにいかないし。あと一ヶ月は30日だったり28日だったり、12の次は1だったり・・・だがこれらの問題はそもそも「日付関数」にとらわれていたのがいけないと思い始めた。日数差とか曜日がどうとか考えなくていいわたしの家計簿にとっては、なまじ日付関数を使うより日付をシリアル番号風にしてしまえばいいのだ。「風に」というのは同じ日に買い物したものまで番号分けする必要を認めないからだ(本当は生データの修正をしたいときに是非とも必要になるのだが、それは後に述べる方法で解決をつけた。そう何度も何度も修正するわけじゃないし)。2001年7月24日に買ったものには等しく20010724という番号をつけておく。こうすると、月別集計はむちゃくちゃ簡単になる。だって番号が20010625以上20010724以下のものだけを抽出してくればいいわけだからな。こうなると今年じたばたした正月の集計だって怖くない、20011225よりは20020124のほうが絶対大きいわけだから。入力フォームでは$ye,$mo,$daの3つの変数にわけて、yeは1年、moは一ヶ月ごとに規定値を書き換えて日付入力の労を省くことにするが、そこでtableに取り込まれる値は$deserial=$ye.$mo.$da、属性はint
である。
monthlypro.cgiはpurchase, drawing, outgoneの3つのテーブルから指定期日の値を抽出するためのプログラムだ。入力フォームは抽出する期間を開始年、月、日、終了年、月、日の6つのパラメータとして入力するのが主な作業だ。このうち年は該当年、開始と終了の日にはそれぞれ25と24を規定値としておく。開始の月は8月分なら07、終了の月は08と入力する。あやまって0を落とすとシリアル(風)番号が一桁変わってしまうので、規定値に「0」の数字を入れておいて注意を促すなどの心憎い(憎くねえよ)工夫などもする。もう開始の月は終了の月マイナス1とかせこい自動化をすると12月とかにひっかかるのでやらない。この入力によって、同じ例で行くとシリアル(風)番号が20010725から20010824までのデータをselectするviewがデータベース内に作成される。原則的にはこのcgiファイルでpurchase,
drawing, outgoneの3つのテーブルについてのセレクトビューを一気に作るが、ひとつだけ作りたいような場合もあるかと考えて、チェックボックスで候補を出し規定値は全選択、はずせばそれは作られない、という風にしておく。もっとも、集計データ削除の場合全部削除するようにしちゃったからこの選択はあんまり意味ない。セレクトビューは作ったらいちいち消さないで番号を変えてどんどん別のものを作る。保存しておけばあとから見たい場合もあるだろうし、ビューだからあんまり容量食わないだろうし。その番号というのは以後年と月だけのシリアル(今度はホントのシリアル)番号として他の処理プログラムに延々と渡されて行くことになる。これを$sufという変数にすることが多くなる。suffixの略ですな。
実はmonthlypro.cgiにはもうひとつ大きな役割がある。ダミーデータの作成だ。Accessのクソクエリはクエるフィールドにひとつでもnull値があるとクエリのすべての結果が無になるという笑っちゃう問題があり、null値の処理やダミーデータの作成が必須であったが、SQL文を素直に書いて行けばそんな問題は全くない。だが問題は結果の表示で、null値は表示されないからWeb上に出力される表がズレてしまうのだ。全く単純な問題ではあるがこれはよろしくない。考えた結果null値の場合の条件分岐とかするより一気にダミーデータを放り込んだほうが簡単という結論になった。それを放り込むのは集計のときに、ということでmonthlypro.cgiにこの処理が組み込まれた。null値の出そうな、ということはつまり、月によっては出費がないこともありうるカテゴリ(二月に一度の水道代とか、その他とか)について、日付はその月のついたち、商品名はdummy,値は0というのを該当テーブルにinsertしてやる。この作業は「ダミーデータを作成する」チェックボックスに規定で入っているチェックをはずせば行わないようにすることもできる。
それから日付で抽出してやる。という手順だ。
この処理をした結果として、次のものを記述する。
送信ボタンを押すと「データを抽出しました」というメッセージとともにこのリンクがぼぼんと現れる。?suf=$sufというところがミソで、2001年8月分の日付を入力して送信すると$sufの部分は200108となる。これが次に総計を行うcgiファイルに必要なパラメータsufとして受け渡されるのである。日付を入力して送信ボタンを押す感覚でこのリンクをクリックすることにより次のcgiファイルが開かれ、受け渡されたパラメータを使って処理が進むことになる。
計算は一気にoutotal.cgiファイル中で行われる。
monthlypro.cgiファイルで、purchase200108, drawing200108, outgone200108という3つのビューが作られ、これらを処理できるようにsuf=200108が受け渡されてきた。
まず、purchaseの抽出分から、分類2の食費、雑貨、趣味、comp、書籍、その他、灯油ごとに合計を出す。
このSQL文の内容を、一時calcpurchという名のビューに作ってしまう。あんまりビュー増やしたくないんだけど、SQL文もあんまり長くなるとわかんなくなっちゃうからな。
outgoneは、Accessでやっていたルックアップというのを、inner join 関数を使って行い、二つのビューに分ける。outgoneにはおでかけ場所とおひる場所のデータが入力されているが、おでかけの場合は、おでかけ場所とそこの往復交通費、あと単に「交通費」という文字列のフィールドが、別のテーブルtrafficにまとめられている。trafficを参照してoutgoneテーブルに入力されているおでかけ場所について交通費の総計を出せというかなり無理なご注文もSQL文の構造をきっちり書けば一気に出る。結局出るのは
cl2=交通費 sum=交通費の合計
という1行2列の値である。これを求めるビューもcalctrafという名前で保存する。
外食費もlunchという、マクドナルドならバリューセット二つで1050円、ケンタッキーならチキンなんとかセット1200円
などという別表から同じ方法で求める。こっちのほうはでももっと少ないから、purchaseでやったように入力したときにすでにプログラム内で決定しておいてもよかったかもしれない。
drawingの抽出分も、分類1と分類2の関係を作るのに、実はあんまりしなくてもいい上と同じワザを使ってみたかったばかりに使ってしまい、あとで文が長くなって困ったりしている。結果は通信費、光熱費とそれぞれの合計値として得られる。calcdrawという名のビューで保存する。
こうして作った
cl2 sum
という2列の関係を表すビューをunionで数珠繋ぎにしてやれば、総計を表すcalctotal200108というビューができる・・・と言いたいところだが実はひとつめんどくさいことがある。光熱費だ。calcdrawにある引き落としデータ、電気・ガス・水道を光熱費として合計したものと、calcpurchにある日用品データ、灯油をやはり光熱費として合計したものを、さらに合計してやらねばならないのである。だが、これはunionしてからgroup
by cl2でくくっちゃえばいい。それにしてもどうも泥縄くさい処理でなんとなく恥ずかしい。
もひとつ。家賃を忘れるところだった。これは毎月定額でありなおかつ請求書がこないのですぐ忘れる。それであらかじめ(cl2, sum)=(家賃、ひみつ)という、定額を入力した1行1列のテーブルをrentaroomという名で作っておき、これをここでunionする。
こうしてとにかく総計を表すビューcalctotal200108ができたので、添え字とともにこれを出力のためのoutotal.cgiに引き渡す。
出力はただできあがったビューcalctotalを出力するだけである。あとここで最終集計値も出していっしょに出力する。
今度は、この月ごと集計を表にまとめたい。
となると、今までは(cl2, sum)という2列のテーブルに、(食費,a円)、(雑貨, b円)、(comp, c円)と値を入れていたのを、今度は(年月シリアル番号
,食費、雑貨、comp・・・、総計)という10列以上の集計に、(200108、a、b、・・・)と入れていかなければならない。行と列を入れ替えた上に、頭に年月シリアル番号、尻に総計をくっつけてやらなければならないのである。Excelなどなら簡単にできそうだが、PostgreSQLで行と列の入れ替えとか、できるのー?
しかしだいじょうぶ。わたしたちにはPerlがあるのです。まずpsqlででも何でも、cmltotalという、上記の構造を備えたテーブルをDBに作成しておく。次にPerlで@values配列を用意して、これにまず年月シリアル番号を入れる。それからDBにアクセスし、calctotal200108から順番にデータを読み込んで行き、最後にデータの総計を計算して配列の一番最後に入れる。こうしてできあがった@values配列の要素を
の()内に入れてやればよい。ただしきちんとした書式になるように、join関数を使って
という形にしてから入れてやる。結果的にinsert文をinsert into cmltotal values (200108, a, b, ・・・・)という形にするためである。幸い入れるのは全部集計値つまり数値なので''で囲む必要がないのでやりやすい。
ただし、書き忘れたことがひとつ。calctotalを出力するときの順番は?
postgreSQLの列の出力は、きわめて当てにならない。といってもAccessのレコード表示だって当てにならんがな。cl2順に出すというのもテだが、2番目に「その他」が出てきたりするとすげえかっこわるい。ということでここで初めてindexというかプライマリキーを作った。indexxというテーブル、cl2の順番を大事なもの順に並べて通し番号をつけたテーブルであるが、これとcalctotalをinner
joinしてindexxテーブルのindex番号順に並べて出すようにしてある。
もちろん、この月ごと集計を並べたテーブルを表示するcgiファイルも作った。
分類1毎の集計 食費とcompはそれぞれ分類1に細かく分けられるので、やはりその動向を見たい。ゆえにmonthlypro.cgiで作った3
つの日付抽出ビューから、さらにcl2="食費"あるいは"comp"に限定したものを、今度はcl2ではなくcl1でグルーピングする。出力したり、同様年月シリアル番号と総計を頭とケツにつけて積算テーブルに入れたり、その結果を出力したりする。これらはまあcl1の名前でソートしていいだろと思った。
それから、引き落としについても引き落とし項目(cl1として入力されている)のひとつひとつを月ごとに比べたい。今月は携帯の通話料がかかったとか、夏と冬で電気代を比べるとどうとか。一見、こちらのほうが簡単に見えた。drawingのデータをそのまま出せばいいだけと思えたからである。だが思わぬところに落とし穴っていうか過去のデータを導入したときに、2ヶ月に1度しか落とされない水道のためのダミーデータがしつこく入っていたので、何も考えずに出力したら積算入力のテーブルの列数を越えてしまってエラー。結局食費やcompと同じようにcl1でグルーピングすることにより重複するcl1(水道)データがまとめられ、ダミーデータの扱いにも神経を使わなくてよくなった。
Access家計簿のときは(別にAccessだからというわけではないが。そうでなくてもAccessにはいろいろ文句をたれたいことがあるが)集計の時に一部の作業を行き当たりばったりの手作業でやったりしたので月の集計を何度かやり直すことが多かった。今度はほぼ完全自動化といっていいし、第一集計の過程で生成されるのがほとんどビューであるから、実体データの入っている3つのテーブルのほうを直せば次にこれらのビューで出力されてくる結果は直っているわけで、そういう問題は起こらないだろうとは思うが、それにしても一度作った集計関係のビューを削除して修正したいと思うことはあるだろう。そのときのためにrmvcalc.cgiというcgiファイルを作った。何年何月分の集計データを削除するかを入力、あとチェックボックスでダミーデータも削除するか、あとこれは実体データの入っている積算テーブルからも削除するかを選択できる(既定値は削除)。総計を計算する過程でできるビューはめんどくさいからみなまとめて削除することにしてしまった。ずいぶんたくさんあるので、いったん全部ビュー名を配列に入れてしまい、foreach文でその配列から一個一個取り出しては
のSQL文を実行させる。
ダミーデータはすべてついたちづけにして入力してあるので、送られてきた年と月のシリアル番号に01をくっつけて、たとえば日付番号が20010801で品目名が'dummy'であるものをテーブルから消すというSQL文を実行させる。
積算データは年月番号を指定することによって削除する。
これから実際使われることが多いかどうかはわからないが、集計システムの作成中にビューを作っては失敗してまた作り直してという作業にめちゃくちゃ役に立ったのは確かだ。
ていうか今ももいついたのだが、積算テーブルのデータを削除するプログラムにしといて、ビューも削除しますかときいたほうが効率よかったかもしれない。どっちみちクリック一発全自動で作り直せるから変わりないんだけどね。
間違ってヘンな値を送信してしまった場合などである。こっちはよく使うだろう。
だがここにきてわたしはかなり致命的な間違いをしてしまったことに今更のように気がついた。それは日用品のお買い物データにひとつひとつプライマリキーをつけるべきであったかも知れないよやっぱりということである。というのは、Accessのように表のデータを直接つついて削除することができないこのシステムでは、プライマリキーを指定して削除するのが一番手っ取り早そうだったからだ。しかしもうかなり遅そうだ。
それに、と自分を擁護する声も心の中でささやかれる。idを間違ったらどうするか。なにせ過去のデータを移行すると6000件になろうとするデータ群である。5232と5322を間違って削除指定してしまい、あとで削除するべきデータが残っているのに気がついて、じゃあ消したのはなんだったのなどということになりかねない。やはりAccessのように表のデータを直接つついて削除せよとやりたい。
そんなわたしが苦肉の策で思いついたのは、スクローリングリストだった。
まずテーブルpurchaseの一行ずつを、:でくっつけて一文字列にする。それを配列に読み込んでいく。その配列をもとに入力フォームのスクローリングリストを指定するのである。見やすいようにリストのサイズは10くらいにしておく。で、そのリストから削除したいデータを選んで「削除」ボタンでサブミットする。
すると今度はその文字列をsplit関数でもう一度配列要素にバラかす。そうしてSQL文で、日付番号が配列要素の0でかつ品目名が配列要素の1で・・・個数が配列要素の5であるものを、消せ、というのを実行するのだ。たぶん日付番号と品目を指定すればほぼ唯一に決まるだろうが乗りかかった船だから全部指定してやった。
非常に手が込んでいるが、これでちゃんと削除される。送信したあとにもう一度テーブルからの値の読み込みをやり直して表示させる命令を付け加えたので、削除されたデータを除外したリストが再表示され、続けて削除したい場合もOKである。
こここまで来たところで、Accessからのデータを移行することにした。そのためにはまずAccessデータをExcelデータに書き出すことが必要である。
「お買い物」テーブルデータをExcelに移そうとする。その前に、日付関数で表されたデータをテキストに直さなければならない。これは前もやって大丈夫なことは確認してある。デザインビュー(今(今日)となってはなつかしい言葉だ)にして「月日」のところを「日付・時刻型」から「テキスト型」に変えるのみだ。でも実はその前に「地域のプロパティ」で、日付の表記法がyyyy/mm/dd型であることを確認してからである。
次に悲劇が起こった。通貨型の金額を、整数型に変える。イルカが出てきて「エラー!15件のデータが失われました。OK?」と聞いてくる。このとき(気づいたところで遅かったのだろうが)わたしはてっきり、消費税込みなのでホントは小数点以下の桁がある金額を今回整数化するにあたり小数点などのデータが失われた、という意味だと思っていてさほど気にとめなかったのだ。ところがあとで見たらなぜか既製マシンやSCSI-HDDなど非常に重要なcompお買い物データの金額が消えている。データが失われましたってこのことかよ・・・
そりゃ前もってバックアップをとっておかなかったわたしも悪いと言えば悪いけど、たかがちょっとデータ型を変換しただけでなぜデータが失われなければならないのか。エラーって何なの。
だがわたしとあの爬虫類野郎(とまで言われるようになった)との縁はこのデータ消失事件以前にかなり完全に切れている。というのもそうやって形式を変えてからこのデータをxls形式で書き出そうとしたところ「このファイルは読み取り専用なのでデータの変更はできません」と言ってきやがったのだ。一応、かなり辛抱してこのテーブルデータのプロパティとか探してみたがもちろんそんな設定はどこにもされてない。
・・・クソイルカだの食うぞだのカツシメにするぞだの言っていた間はまだ愛情が残っていたのだ。完全にそれが怒りに昇華した今、わたしはやはりこのまえの×ル子事件同様自分が深く静かにキレるタイプであることを再確認した。わたしはこのテーブルデータを全選択してコピーしてからExcelを立ち上げてそこにペーストした。データはちゃんと移った。そのとき実はこの前Officeを再セットアップしてからExcelを使うのは初めてだったのでヤツが現れ「ようこそMicrosoft
Excelへ!ヘルプを使いますかアシを紹介しますかとりあえずExcelを使ってみますか」と言ってきやがった。それをわたしは完全に無視したのである。97以前のOfficeではこの最初の尋問に答えないとヤツはアプリを使わせてくれないが2000ではシカトオッケーらしくそれは改善点かも知れない。なんかキューとかケケとか言ってるのを、わたしは目をやることすらしなかった。黙って眺めると金額の部分にヤギのクソのように小数点以下のゼロが並んでいるが、わたしの心に湧いたのはなぜか、あまりにも使えないこのソフトに対する哀れみのような感情だった。黙ってその部分の書式を「文字列」に変えた。寝る時間も迫っていたのでそのまま保存して終了。ヤツがまたキューとか言いながら去っていくのを妙に冷めた気持ちでわたしは眺めた。上のエラーが見つかったのは翌日もう一度Excelを立ち上げなおしたときだったがそのときもすでにわたしの心の中からOffice=魚野郎(だから違うって)という関係式はすでに無効状態になっていたのでヤツに当たることすらしなくなっていた。幸い消えたデータの主要な部分は先週とったバックアップから値を補填できた。タブ区切りテキストに書き出した。そしてさっさとPeggyに乗り換えた。
今度は、テキスト形式で2001/08/01とかなってる部分の/を検索置換で取り除く(置換文字列を空白)。そしてタブ区切りのタブをコロン:に変える。タブと指定するのに、正規表現を使うことにした。このわたしが正規表現なんてものにまともに向き合うことになるなんて。これもPerlで勉強したおかげである。タブを表す\tを:に変えれば、タブ区切りテキストがコロン区切りに早変わりである。
それにしても、そこでデータが消失したという事実は、8月まではとりあえずAccessでの家計簿を続け、9月からはAccessと併行で処理を行い値を比べたりしながら徐々に移行して行こうと思っていたわたしの計画を一気に変更させ、本日をもって二度とAccess家計簿には手を出すまいという気にさせたのである。おかげで一気に引き落としやらおでかけやらクレジットやら日記やらのすべてのデータをここで一気にコロン区切りのテキストデータに変えることになった。そのコロン区切りデータをこんどはPerlでスプリットして配列にしてpurchaseにインサートしたりしたが、よく考えたら最初からカンマ区切りにしたらすんなりinsert文に入ったのかも知れないけどやっぱり文字列には''をつけなきゃならないからやっぱり一回配列要素に分離したほうがいいんでこれでよかったのか・・・。
6000件のデータがこうして、purchaseテーブルに入力された。同様に、160件のおでかけデータとたぶんそのくらいはある引き落としデータがそれぞれoutgoneテーブルとdrawingテーブルに入力され、かつ600件の日記データがコロン区切りテキストデータとして読み込まれた。そうしてcgiにより一年ごとにデータを抽出、集計する作業により、1999年7月からの家計簿データは、軽い怪我程度のまあまあ無事な形で、SolarisのWebDBに移行されたのである。これでもうクソ両生類(だから違うっての)のツラを拝むこともあるまい。さようならイルカ。こんにちわラクダ(実はリャマらしい)。
こうして魚類のなり損ない野郎との完全決裂を急遽果たさざるを得なくなったわたしは、日記やクレジットの入力、出力システムも急遽作らざるを得なくなってしまった。まず、クレジットである。
これは、Accessでは使用月日と請求月日と引き落とし月日をまめに入れていたが、結局来りゃいいんで請求と引き落としはチェックマークだけにした。だが、こっちの場合レシートIDは、これからもつけつづけたほうがいいと思う。Access
ではオートナンバー機能というのがあったが、こっちにはない。ていうかそんな安機能は自分で作れというんだろう。えーと。
まず考えられるのは入力する前にセレクトして行数を数えておいてそれに1足して、入力のときのIDにするということだ。だがあとで欠番とか起きた場合に混乱しないだろうか。
結局最適と判断されたのは、それまでのIDの中で最大のものを探してそれに1足すという方法だった。こりゃ簡単である。なにせselect max(ID)とすればいいんだから。
これには、削除の他に追加入力用フォームを用意することが必要である。つまり請求・引き落とし確認用である。それはこれまで工夫をこらしてきた削除用フォームを利用すればよい。これこそ変更データの指定はプライマリキーであるレシートIDで行えばいいので楽だ。このフォームではまず、月日ではなく、"請求チェック"or"引き落としチェック"がnull値であるものを選んで表示させる。
ところがここでも小さなトラブルがあった。Accessで入力してある項目のうち最新の2つが未請求だったのだが、テキスト経由で入力されたこれらのレコードは、null値検索にひっかかって来なかったのだ。2項目だけだったから削除して入れ直したら今度はひっかかった。外部からのデータのインポートの場合のnull値というのは取り扱いめんどくさそうである。たくさんあったら泣けてたかも。
SQLで扱える文字列のサイズというのは最大何字なのか?調べりゃ無論わかるんだろうが、Access時代から表計算型のデータベースにあまり長々とした文を入れるのはよしたがいいんじゃないのかというのはよくダンナ様に言われていたことである。そこで移行にあたって日記はPostgreSQLのデータベースに入れるのはやめた。Perlを使ってテキストファイルから読み出したり書き出したりすればいいじゃないか。
ということで、以前Perl本の最後の練習問題だった「掲示板」を日記用に作れないかやってみた。確かちょうど一ヶ月前この課題をやったときはよくわけもわからず本の通りに打って動く動かないと騒いでいたが、今回はさすがに何をやっているのか意味が全部わかった。そこで、ファイルのロックとかエラーへの対応とか、ファイルセーブの件数制限とか、のに子個人の日記としては必要ない機能を削ることもできた。だが、問題はやはり前にもやったようにこの方法だとセーブした内容を直接見れないことだ。英字はいいけど日本語がなぜかどのコードでも文字化けして納められているのだ。また読み出せばちゃんとした日本語になってWeb上に表示されるのだが、これではさすがに怖い。あと、これまでのデータをどうやって移行する?できないことはない。まず旧データのテキストファイルからPerl配列に読み込んで、次にパラメータとしてcgiに渡してやればいいわけで・・・
旧データの件数を見る。650件とある。なるほど2年弱の日記データだもんな・・・
今Perl本を見ながら書いたこの掲示板プログラムは、「オブジェクト指向型」という新しい(10年前くらいという意味での最近らしい)手法によるものだ。今までコチコチの関数指向型でプログラムを書いて来たわたしにとってはなんつーか変幻自在感の強い、それはそれでおもしろい方法だとは思ったが・・・とにかく今日明日中に日課システムを整えてそろそろわたしやあの人のWebページの更新などに入らなければならないわたしとしては、使いなれた旧来型で作っちまうことにした。
となればやり方は簡単。すでにコロン区切り形式に整えてある旧データのファイルをオープンして、一行ずつ読み込んではコロンでsplitして、各配列要素をhtml
のここに書きなさいと指定してやる。非常に単純なhtmlファイルとして読めるわけだ。開いてみると650件のデータの読み込みにはかなり時間がかかった。これからデータの細分化とか検索システムとか考えなければならないのだろうが、まずはあと回し。あとこれからのデータは別のファイルに書き込んだほうがよさそうだ。
日用品やコンピュータなど買ってレシートをもらってくるお買い物のレシートデータを入れるテーブルpurchaseはたとえばこんな構造になっている。
| 品目(good) | 分類1(cl1) | 分類2(cl2) | 価格(price) | 個数(no) |
| パン | 主食 | 食費 | 170 | 1 |
| HP Kayak | 既製マシン | comp | 65000 | 1 |
| マノウォー新作CD | 趣味 | 趣味 | 2500 | 1 |
| 卵 | 肉魚卵 | 食費 | 120 | 1 |
| プリンタのインク | サプライ | comp | 1500 | 1 |
| プラモデル用塗料 | 趣味 | 趣味 | 80 | 3 |
あくまでたとえばである。マノウォーCDが趣味とかいうのもたとえばである。これを
| 分類2(cl2) | 価格小計(sum) |
| 食費 | 290 |
| comp | 66500 |
| 趣味 | 2740 |
という形に集計するのだが、それにはcl2で値を分類しそれぞれの合計を求める。さらに合計というのは価格*個数である。でもSQL文一発書けばへいちゃらさ。
でいいのだ。結果を目にしたときは涙が出た。なんか、Accessのクソクエリじゃこれだけのことをやるのに5,6手順は踏んでいたような気がしたが・・・Accessでもできたことを、わたしがやり方を知らなくてやたらに手順を踏んだだけなんだろうか?
なんて言うとあんな偽データベースと比較するなと怒られそうだが、Access家計簿ではあのルックアップと言うヤツを結構たくさん使っていたので、それをPostgreSQLではどうやってやるのかなと思ったら、inner
join関数というのがあった。
テーブルoutgoneは次のような構造になっている。
| place | lunch |
| A地 | X店 |
| B地 | X店 |
| A地 | Y店 |
これ と、trafficテーブルから、交通費の合計を計算する
| place | fee | cl2 |
| A地 | 1000 | 交通費 |
| B地 | 500 | 交通費 |
これも
cl2が「分類2」、feeがテーブルtrafficの往復交通費、placeがおでかけ場所である。ここでgroup by cl2はいらないかもしれない。全部「交通費」しかないからだ。ていうかそこをめんどくさくなくするためにわざわざtrafficテーブルにcl2の欄を作っちゃったのだ。答えは
| cl2 | sum |
| 交通費 | 2500 |
calcdrawにあるcl2=光熱費と、日用品データの中にあるcl2=光熱費を、さらに合体させるためにcalcpurchとcalcdrawかcalcpdというビューを作らなければならない。このためには
というひちめんどくさい表記になるが、裏を返せばこれだけでちゃんと引き落とし分の光熱費と灯油分の光熱費が合体してくれるんだから儲け物だ。
as doronawaというのは、最初つけなかったときにpostmaster様から「selectしたものからさらにselectしたときは、どっからselectするのかわかりにくいから、カッコでくくるだけじゃなくasナントカって適当に名前つけとけ」と言われて「ホントに名前つけるだけでいいの、知らないわよ」と適当な名前をつけたらあっさり命令をきいてくださったという事情によるものだ。その後このdoronawaという名前はどこでも必要とされなかった。
cl2とsumの関係を表すcalctotalにはIDがないので、表示の段階で付け焼き刃的にIDをつけた。IDはindexxというテーブルに、cl2をそれっぽく並べたものを番号とともに表示する。
| 分類2(cl2) | ID |
| 食費 | 0 |
| comp | 1 |
| 趣味 | 2 |
| 分類2(cl2) | 価格小計(sum) |
| 趣味 | 100 |
| 食費 | 200 |
| comp | 300 |
・・・・
で、
とやれば、結果は
| 分類2(cl2) | 価格小計(sum) |
| 食費 | 200 |
| comp | 300 |
| 趣味 | 100 |
と、特にIDを表示しなくても、ID順にいつも並ばすことができる。
月の総計を積算テーブルに追加する過程でずいぶん苦労した話だが、その月入力がなかった場合その項目はnull値になる。するとperlではその項目は暗黙のうちに無視される。ゆえに読み込んだ配列要素群をそのまま積算テーブルにinsertする命令を実行させると、テーブルの列数と配列要素群の数が合わなくなってエラーとなる。エラーはpostgresのコンソールに表示されるがApacheのエラーログには表示されず、ただ丸坊主の結果が表示されるなので最初のうちなかなかわからなかった。
price(価格)とno(個数)をパン配列に読み込んで、good=「パン」cl1=「主食」cl2=「食費」を指定してテーブルpurchaseに入力する。
価格、個数ごとに配列を用意するのは無駄なので、@bread配列1個に、ペアにして2個ずつ読み込み、2個ずつ取り出す。
という配列を用意しておく。bprはbreadのprice, bnoはbreadのno.という意味。パラメータが送信されたら、以下を実行する。
while (@bread[$i]) { ・・・0番目から始めて偶数番目の要素、つまりpriceの要素がある限り。
$good = "パン";
$cl1 = "主食";
$cl2 = "食費";
($price, $no) = @bread[$i,$i+1];
my $insertstr = "insert into ";
$insertstr .="$tablename values($dserial, '$good', '$cl1', '$cl2',$price,
$no)";
$result = $conn->exec($insertstr);
$i=$i+2;・・・・2個ずつ飛んでいく。
}
| mserial | 200108 |
| 食費 | 100 |
| 生活雑貨 | 200 |
| 書籍 | 300 |
| comp | 400 |
| ・・・ | ・・・ |
| 総計 | 10000 |
というテーブルcalctotal200108を、以後calctotal200109・・・と作成していくたびに、
| mserial | 食費 | 生活雑貨 | 書籍 | comp | ・・・ | 総計 |
| 200108 | 100 | 200 | 300 | 400 | ・・・ | 10000 |
| 200109 | 150 | 180 | 310 | 500 | ・・・ | 15000 |
のように積算していきたいのだ。Excelとかと違って、簡単に行見出し、列見出しをつけて天地入れ替えてみたいなわけにはいかなそうだ。もっともPostgreSQLには配列をそのままセルに読み込めるという機能があるからそれを使えばいいのだろうが。でもPerlを使えば結構簡単に行くのでそっちにした。
積算テーブルcmltotalはあらかじめpsqlなどで作っておく。
まず、calctotalのほうを、perlで用意した配列に読み込む。
my selectstr="select * from clmtotal";
$result = $conn->exec($selectstr);
とやっといて、
my @cal;
my $n = $result->ntuples;
my $j;
for ($j=0; $j < $n; $j++) {
my $gv=$result->getvalue($j,0);
@cal=(@cal, $gv);
}
@calという配列に、0行からn行までの値を読み込んでいるところ。nは一定なのだがめんどくさいのでPerlに数えてもらっている。列は0だけ。
my $val=join(",",@cal);
my $insertstr = "insert into cmltotal values($val)";
$result = $conn->exec($insertstr);
$valは"200108, 100, 200, 300,400....10000"という文字列になり、ゆえにinsertstrは"insert
into cmltotal values(200108, 100, 200, 300,400....10000)"というステートメントになる。データがみんな数値だったのが幸いだった。
実際は頭のmserialとケツの総計は泥縄式にあとからつけたりしたのだが、大きな流れは変わらない。
結果をテーブルとして出力するのに、htmlタグを書くと非常にめんどくてやんだぐなる。しかしperlの関数を使うと、あまりサイズなどの指定を細かくしなくてよければ、非常に簡単に記述することができるようだ。
関数によるテーブルの書かせ方は、man3のcgi3をじーと見つめるに、
print start_table({-border=>1});
print Tr({-align=>center},
[
th(['title0', 'title1', 'title2']),
td([val0-0, val0-1, val0-2]),
td([val1-0, val1-1, val1-2])
]
);
print end_table;
と書けば、
| title0 | title1 | title2 |
| val0-0 | val0-1 | val0-2 |
| val1-0 | val1-1 | val1-2 |
というテーブルが得られるようだ。
Tr([.....])というところをつきつめていくと、
$th = th(['title0', 'title1', 'title2'])
$td0 = td([val0-0, val0-1, val0-2])
$td1 = td([val1-0, val1-1, val1-2])
と考えれば、
Tr([$th, $td0, $td1])
と書ける。さらに、$thは見出しにあたる文字列の配列だからいいとして、@tdn=($td0, $td1)とすれば
Tr([$th,@tdn])
と書けることになる。
一方、中身のほうtd([val0-0, val0-1, val0-2]) は、
@val=(val0-0, val0-1, val0-2)
とすると、
td(\@val)
と書けるようだ。配列やハッシュを[]でくくった状態を表すには、この\つまりバックスラッシュが必要らしいということをPerlの本から発掘したのはかなり奇跡入ってると思った(html形式についてはかなりズブの入った素人なのに)
これでPostgreSQLのテーブルをかなり簡単に出力することができるようになった。実際には、
my $th= th(['mserial','食費','生活雑貨','書籍','comp',・・・・,'総計']);
my (@tdn);
for ($i=0; $i < $n; $i++) {
my(@val);
for($j=0; $j < $m; $j++) {
@val=(@val,$result->getvalue($i,$j));
}
$td=td(\@val);
@tdn=(@tdn, $td);
}
と作っておいて、
print start_table({-border=>1});
print Tr({-align=>right},[$th, @tdn]);
print end_table;
簡単だ。Perlすげえ。
生データを削除するためのcgiファイルの話である。あなたは2001年8月1日の今日、本日の日付と価格と個数、そして品目を送信した。あッHPカヤックをHPコニャックと書いたのに気づかず(気づけよ)送信してしまった、削除しなければ。ということでテーブルのデータを表示させて削除させるcgiファイルrmvpurch.cgiを呼び出す。だが、6000件もある過去のデータを全部呼び出すのに、すげえ時間がかかる〜。
・・・というようなときのために、表示・削除のために呼び出すデータは、最低本日分の入力データだけでいいではないか。今、送信したわけだからパラメータとして本日の日付が受け取られている。それを使って、
へリンクして、dserial=$ye.$mo.$da=20010801のデータだけを呼び出せばよい。
かくして本日のデータだけが読み込まれてきた。ではHPコニャックと書いてあるデータを指定して、これを削除してもらえるように送信ボタンを押す・・・と、エラーが起きた。検討してみると、さっきちゃんと受け取ったはずの$ye,$mo,$daのパラメータが消えている。なぜなぜなぜ〜?
当たり前の話だった。「送信」ボタンは、すべてのパラメータをフォームで入力したとおりに送信し直すという意味だ。つまり、フォームで入力されなかったデータはnull値として送信し直されてしまう。で、日付データの入力は確かにフォームに用意してなかった。
そこでこのように書き直したのである。
print hr, start_form;
print textfield (
-name => "ye",
-value => $ye,
-size => 5,
);
・・・・
つまり、このプログラムではまず最初にリンクされた時点で
$ye=param('ye');
などで、年や月のパラメータが受け取られている。それからフォームを開始するので、フォームに日付データを入力するテキストボックスを配置し、その既定値を今受け取ったパラメータの値にするのだ。すると次に送信したときは日付データももう一度送信されるから消えてもダイジョブよ、というわけだ。なんか、バケツリレー的データの受け渡しって気がするが・・・一応快調に動いてるからいいとしよう。
「初めてのPerl」では「オブジェクト指向型プログラム」の書き方は最後の「掲示板レッスン」にしかまず書いていないといっていいだろう(「プログラミングPerl」にしこたま書いてあるみたいだ)。「関数指向型」の考えであたまがコチコチになっていたわたしは、それまで実際には使っていただろうオブジェクト指向型の命令($result=conn->exec(selectstr)とかでしょ)も、そういうものだと丸暗記して使ってきたのだが、「掲示板レッスン」にあたって初めて、それに正面からとっくんでみた。
ふーむ
なんとなくおもしろいことはわかった。
たとえば、CGIでパラメータを受け取る場合だ。3つのパラメータを受け取ってそれを配列にセーブしておいて、また書き出すという場合(3つならその場で書き出せ、という話はナシヨ。あくまで例だから)「関数指向型」だと、
$one=param('one');
$two=param('two');
$three=param('three');
@array=($one,$two,$three);
foreach (@array) { print $_."\n";}
みたくなるはずだ。それを「オブジェクト指向型」だと
$cur = CGI->new();
とやっておいて、
$cur->param('one');
$cur->param('two');
$cur->param('three');
で、
@array=($cur);
か〜?と思っちゃったよわたしは。最初の行がoneというパラメータをとってくることだというのはわかるが、次にtwoというパラメータをとってきたときにそれは上書きされちゃうんじゃないの?・・・どうやらされないらしい。たぶん、次に
$cur=CGI->new();
とやったときに、全部すっからかんに初期化されるんじゃないのかな。そして「oneをとってきてtwoをとってきてthreeをとってきた」というお仕事がそのまま@arrayに入るって感じなのかなあ。果物屋さんで何を買ったかを記録するのに、かごに入った果物の写真を撮るのと、お店で棚からみかんやリンゴをとってかごにいれる風景を全部ビデオで撮るのとの違いみたいなものなのかな?
じゃあとってきた値を出力するときはどうするのかというと、
foreach $out(@array){
print $out->param('one');
print $out->param('two');
print $out->param('three');
}
とやるんだそーな。ていうかやったらできた。う〜ん。なんかわかるよーなわからないよーな?・・・なんかまともに勉強するとすごい時間かかりそうなので、とりあえず家計簿関係は今の調子でやろ。
dailypro.cgiで出てくる分類1ごとの頻出項目リストは、初めて作成した時点ではcgiプログラムに直接書いていた。いわく、
print scrolling_list (
-NAME => "beva",
-VALUES => [qw(「飲料」 牛乳 ビール コーヒー マリーム 缶ジュース パックジュース お茶パック) ],
);
みたいな感じに。だが、ちかごろ豆乳を買う頻度が多くなってきたのでこのリストに「豆乳」を付け加えようということになると、わざわざこのcgiプログラムを開いて直接編集しなければならない。めんどくさいし、編集を間違ったりするとサーバエラーで泣きを見る。
Accessで家計簿を作っていたときは、こうしたリストは別のテーブルにしてフォームのコンボボックスに呼び込んでいた。むろんこのテーブルを編集するフォームも作った。今回も「頻出品目リスト更新フォーム」を作りたい。
上述のAccessのときは、品目と分類1の関係(たまねぎなら野菜という)の一覧表を使い回していたのだが、今回は個別のデータベースにした。
理由はちゃんとあって、どのリストもトップはnullデータを与える「主食」「野菜」などのタイトルでなければならないからだ。テーブルに新しい値を追加しようが削除しようが、これがいつもリストの一番上にちゃんと読み込まれるための保証に、idをつけてselectのときはid順に読み込ませる必要がある。そのためにこれらのタイトルのidは全て不動の0にしたい。となると一緒のテーブルにはしないほうがいいわけだ。
ということで
(1)Psqlで、該当のテーブルをつくり、
(2)品目リストをテーブルに入力する。
(3)今度はそのテーブルからperlで配列に読み込んで、その配列をスクローリングリストの値に指定する。
というめんどくさい過程を、11個のカテゴリについてしなければならない。(概念図)
もちろん、テーブルを作ったりそれに値を読ませたりするのはPerlを使って引数を変えて繰り返せばよい。
だが、変数名そのものを順次変えて同じ処理をしたい場合、どーするんだろうとしばらく迷ってしまった。つまりPgSQL上のshushokutbというテーブルの値を
Perl上の@hshushoku配列に、souptbのテーブルを@hsoup配列に読み込むという処理だ。答えはサブルーチンだった。
そんなわけで、スクリプトを示すのが一番明白であろう。
あとは、別にcgiスクリプトを書いて、これらのテーブルに値を入力するフォームを作ればいい。idの入力法はクレジットと同じである。
2001年10月09日
家計簿データベースはその後もなんとか毎日続き、月末の集計も1分以内でできるので助かっている。だが、集計は全部cgiなので、同じ集計データでも開くたびに同じ計算をしていることになる。
一度集計したデータは変更することはない(出てしまったアカは決して取り消せない)のだから、集計して間違いなしとなったら、その場でhtmlファイルに書き出してしまい、以後はそのファイルをブラウズすればいーじゃないか。
ということで、月の集計データをhtmlに書き出す機能をつけてみた。
コードの主要な部分はここ。それほどの工夫はない。ただクヲーテーションマークとタグをいっぱい書くという退屈で根性のいる作業というだけだ。でもこれで集計データを印刷したり、うちの外へ持っていくことができるようになった。
2002年05月08日