ソースコードで学ぶWebプログラミング

[Perl]簡易メモ帳の作り方

Webプログラミングではデータの入力処理、データの変換処理、データの出力処理が基本となります。またデータをエスケープするポイントや扱い方もパターン化しているため、効率良く学べば直ぐに習得することも可能です。このページでは、Perl言語によるCGIプログラミングの学習として、簡易メモ帳の作り方を説明します。

Perlで簡易メモ帳のCGIを作成する

まず初めに、簡易メモ帳に必要となる機能をピックアップします。続いて、ピックアップした機能の詳細を決めていきます。システム開発では、制作する物の仕様を決定する工程を要件定義と言います。プログラミング学習ではあまり重視されませんが、実務では必須となります。

1.データ「ID、テキスト入力、日時」を受け取る ・データ「テキスト入力」は、複数行テキスト ・データ「日付」は、「2018/01/01 00:00」の書式(YYYY/mm/dd HH:ii) ・データ「ID」は、メモ毎に異なる英数字 2.データ「ID、テキスト入力、日時」を保存する ・保存は、テキストファイルとして保存 3.保存したデータ「ID、テキスト入力、日時」を表示する ・表示は、日時の新しい順に表示する 4.指定したデータを削除する ・削除は、チェックボックスでチェックしたものを削除

今回は、上の構成で作成することにします。

簡易メモ帳の入力データをPerlで受け取る方法

処理するデータは「ID、テキスト入力、日時」です。この内、テキスト入力は、ユーザーから受け取る必要があります。まずCGIプログラムの処理として、ブラウザからの入力を受け取れるようにします。ブラウザからの入力はエンコードされています。デコード用のモジュール CgiDecode.pmを使うので、こちらからダウンロードして下さい。

また受け取るテキスト入力のnameを決めておきます。このnameは、受け取り時のハッシュのキーになり、また表示タグのname属性になります。今回は memo にします。また入力フォームからはPOST送信で受け取ることにします。

#!/usr/bin/perl use strict; use warnings; require 'CgiDecode.pm'; CgiDecode->new; # our()で使えるように our(%_GET,%_POST,%_COOKIE,@_FILES); # POST送信の時の処理 if ($ENV{'REQUEST_METHOD'} eq 'POST') { # memoをキーとして取得できる my $memo = $_POST{'memo'}; }

ここで%_POST内にmemoのキーが存在しないケース、また別のキーでも受け取るケースも考えて、関数化することにします。

またID、日時も受け取る処理を行います。Perlでは、ユニークなIDを作成する関数は標準ではないため、他と重複しない一意の文字列を作成することにします。また日時は、localtimeで取得できます。

#!/usr/bin/perl use strict; use warnings; require 'CgiDecode.pm'; CgiDecode->new; # our()で使えるように our(%_GET,%_POST,%_COOKIE,@_FILES); # POST送信から受け取る関数 sub gPOST { my $key = shift; return exists $_POST{$key} ? $_POST{$key} : ''; } # UnixタイムスタンプとプロセスIDで作成 13文字 sub uniqid { return time . sprintf('%03d',substr($$,-3)); } # 現在日時を取得 sub date { my($sec,$min,$hour,$mday,$mon,$year,$wday) = localtime; return sprintf('%04d/%02d/%02d %02d:%02d',$year+1900,$mon+1,$mday,$hour,$min); } # POST送信の時の処理 if ($ENV{'REQUEST_METHOD'} eq 'POST') { # memoをキーとして取得できる my $memo = gPOST('memo'); # IDを作成する my $id = uniqid(); #現在日時を作成 my $date = date(); }

簡易メモ帳のデータをPerlで変換する方法

続いて、保存できるようにデータを整形したり、ブラウザへ表示できるようにデータを整形する処理を実装します。保存時のデータ構成は、取り出しやすいようにする事が基本となります。今回は、通常のテキストファイルで保存し、区切り文字としてタブを使うことにします。

テキストファイルの処理では、改行毎に処理が行われるため、改行は1レコードに1つにする必要があります。今回、複数行を入力として受け取るため入力データ内の改行は、保存用に別のデータに変換する必要があります。またタブも別のデータに変換します。それぞれ\\nと\\tのエスケープ表現に変換することにします。\が2つの場合は、その文字列自体として処理されます。

# 改行コードを\nに統一する $memo =~ s/\r\n/\n/g; $memo =~ s/\r/\n/g; # 改行コードを別の記号に変換する $memo =~ s/\n/\\n/g; # タブを別の記号に変換する $memo =~ s/\t/\\t/g; # 保存するレコードとなる文字列 my $line = $id ."\t". $memo ."\t". $date ."\n";

ブラウザへの表示では、HTMLとして表示させるため、入力データをエスケープする必要があります。また保存時のエスケープ表現から復元する処理も必要です。

# HTMLエスケープする関数 sub h { my $t = shift; $t =~ s/&/&amp;/g; $t =~ s/</&lt;/g; $t =~ s/>/&gt;/g; $t =~ s/"/&quot;/g; $t =~ s/'/&#39;/g; return $t; } # 改行を改行タグとして復元する $memo =~ s/\\n/<br>/g; # タブを復元する $memo =~ s/\\t/\t/g;

簡易メモ帳のデータをPerlで出力する方法

具体的に保存を行ったり、ブラウザに表示を行なう処理を実装します。実装の流れとして、変換と出力を分けているのは、フレームワークなどを利用した場合の開発と同じ流れにするためです。オブジェクト指向と関連しますが、フレームワークではデータ保存、保存データの取得はモデルと呼ばれるオブジェクトで行います。保存データの作成は、一般的にコントローラと呼ばれるオブジェクトに記述します。

# メモデータを取得する sub file_get_memo { my $f = shift; my @a = (); open FH, $f; while (<FH>) { my($id,$memo,$date) = split /\t/; # リファレンスで追加 push @a, { 'id'=>$id, 'memo'=>$memo, 'date'=>$date }; } close FH; return \@a; }

ファイルから保存したデータを取得します。タブ区切りで保存したので、split /\t/で配列にしています。Perlでは、my() = で配列を項目ごとに受け取れます。また表示用の配列に連想配列(ハッシュ)で順番に追加していますが、Perlでは配列の要素には数値、文字列、リファレンスしか代入できないため、ハッシュのリファレンスで追加しています。

# ファイル名を定数に use constant FILE_NAME => 'memo.txt'; # 表示データを取得 my $DATA = file_get_memo(FILE_NAME); # 表示させるメモ my $memos = ''; # データがある時 if ($#$DATA != -1) { $memos = '<form method="post"> <table> '; # 配列のリファレンスから元の変数に foreach (@$DATA) { # 表示用の変換 $_->{'memo'} = h($_->{'memo'}); $_->{'memo'} =~ s/\\n/<br>/g; $_->{'memo'} =~ s/\\t/\t/g; $memos .= '<tr> <td>'.h($_->{'date'}).'</td> <td>'.$_->{'memo'}.'</td> <td><label><input type="checkbox" name="id" value="'.$_->{'id'}.'">削除</label></td> </tr> '; } $memos .= '</table> <input type="submit" value="メモを削除"> </form>'; }

表示用の文字列を変数$memosに代入するかたちで実装しています。配列内のハッシュはリファレンスになっていますが、元の変数として扱う場合は、->{'キー'}になります。続いて、HTMLを表示します。Perlでは、複数行の文字列をヒアドキュメントと呼ばれる方法で埋め込むことが出来ます。print <<HTML; から始まって HTML で終わります。HTMLの文字列は任意です。

# ヘッダー情報、HTMLを指定 print "Content-Type: text/html; charset=utf-8\n\n"; print '<title>[Perl]簡易メモ帳</title>'; print $memos; print <<HTML; <form method="post"> <textarea name="memo"></textarea> <input type="submit" value="記録"> </form> <style> table { width: 100%; } form { margin: 50px auto; width: 80%; } input[type='submit'] { display: block; margin: 20px auto; padding: 5px; width: 50%; } textarea { height: 100px; width: 100%; } td { border-bottom: 1px solid #333; } tr:first-child td { border-top: 1px solid #333; } .c2 { width: 160px; } .c3 { width: 70px; } </style> HTML

簡易メモ帳のデータをPerlで削除する方法

最後に、指定したデータを削除する処理を実装します。チェックボックスからの入力は複数チェックされていた場合、CgiDecode.pmでは値が配列のリファレンスになっています。Perlには、配列の要素をチェックする関数が標準ではないため、in_array()を自作します。

# 配列に含まれるか判定する関数 sub in_array { my($v,$a) = @_; if (ref $a eq 'ARRAY') { foreach(@$a){ return 1 if $_ eq $v; } # 配列以外 }else{ return $a eq $v; } }

ファイルの変更や削除を行なう場合は、読み書きモードで開いてデータを取得した後、再度ファイルの先頭に移動してからデータを書き込みます。また変更後にファイルサイズが小さくなっている場合もあるため、truncateでファイルサイズを調節します。

# メモデータを保存する sub file_put_memo { my $h = shift; # ファイル名の指定がない時 return 0 unless exists $h->{'f'}; # ファイルが存在しない場合、作成 touch($h->{'f'}) unless -e $h->{'f'}; # 保存するデータ my $lines = exists $h->{'line'} ? $h->{'line'}: ''; open FH, '+<'.$h->{'f'}; flock FH, 2; while (<FH>) { # 削除指定がある時 if (exists $h->{'del'}) { my($id) = split /\t/; # 削除指定の配列にIDが含まれない時、保存 unless (in_array($id, $h->{'del'})) { $lines .= $_; } # 削除指定がない時 } else { $lines .= $_; } } seek FH, 0, 0; print FH $lines; select((select(FH), $|=1)[0]); truncate FH, tell FH; close FH; }

あとはPOST送信を受け取った際に、作成した関数にデータが渡るようにします。またPOST送信で作成された画面は、再読み込みを行なうとPOSTの再送信が行われてしまいます。対策として、GETアクセスの状態にするためにリダイレクトします。

# POST送信の時の処理 if ($ENV{'REQUEST_METHOD'} eq 'POST') { my %h = ('f'=>FILE_NAME); # 削除指定がある時 if (exists $_POST{'id'}) { $h{'del'} = gPOST('id'); } else { # memoをキーとして取得できる my $memo = gPOST('memo'); # IDを作成する my $id = uniqid(); #現在日時を作成 my $date = date(); # 改行の統一とエスケープ $memo =~ s/\r\n/\n/g; $memo =~ s/\r/\n/g; $memo =~ s/\n/\\n/g; $memo =~ s/\t/\\t/g; # 保存するレコードとなる文字列 $h{'line'} = $memo ne '' ? $id ."\t". $memo ."\t". $date ."\n": ''; } file_put_memo(\%h); # リダイレクトでGETアクセスに print "Status: 301 Moved Permanently\n"; print 'Location: '.$ENV{'SCRIPT_NAME'}."\n\n"; exit; }