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

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

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

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

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

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

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

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

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

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

#!/usr/bin/ruby # coding: utf-8 require './CgiDecode.rb' CgiDecode.new # $_GET,$_POST,$_COOKIE,$_FILES が使えるように # POST送信の時の処理 if ENV['REQUEST_METHOD']=='POST' # memoをキーとして取得できる memo = $_POST.fetch('memo','') end

またID、日時も受け取る処理を行います。Rubyでは、ユニークなIDを作成する関数は標準ではないため、他と重複しない一意の文字列を作成します。今回は、UnixタイムスタンプとプロセスIDで作成することにします。タイムスタンプは、Time.now.to_iで、日時は、Time.now.strftime()で取得できます。

#!/usr/bin/ruby # coding: utf-8 require './CgiDecode.rb' CgiDecode.new # $_GET,$_POST,$_COOKIE,$_FILES が使えるように # UnixタイムスタンプとプロセスIDで作成 13文字 def uniqid() return Time.now.to_i.to_s << $$.to_s[-3,3] end # 現在日時を取得 def date() return Time.now.strftime("%Y/%m/%d %H:%M") end # POST送信の時の処理 if ENV['REQUEST_METHOD']=='POST' # memoをキーとして取得できる memo = $_POST.fetch('memo','') # IDを作成する id = uniqid() # 現在日時を作成 date = date() end

Unixタイムスタンプは、Time.now.to_iで取得できますが、数値なのでto_sメソッドで文字列にします。一旦、数値にせずにTime.now.to_sとすると、日時の文字列になります。またプロセスIDを$$で取得し、文字列にしてからスライスで末尾の3文字を取得し、<<で文字列結合しています。

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

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

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

# 改行コードを\nに統一する memo.gsub!("\r\n","\n") memo.gsub!("\r","\n") # 改行コードを別の記号に変換する memo.gsub!("\n","\\n") # タブを別の記号に変換する memo.gsub!("\t","\\t") # 保存するレコードとなる文字列 line = id +"\t"+ memo +"\t"+ date +"\n"

ブラウザへの表示では、HTMLとして表示させるため、入力データをエスケープする必要があります。また保存時のエスケープ表現から復元する処理も必要です。Rubyでは、基本的にメソッドに!を付けると対象を変更し、付けないと変更したデータを返します。

# HTMLエスケープする関数 def h(s) s = s.gsub('&','&amp;') s = s.gsub('<','&lt;') s = s.gsub('>','&gt;') s = s.gsub('"','&quot;') s = s.gsub("'",'&#39;') return s end # 改行を改行タグとして復元する memo = memo.gsub("\\n","<br>") # タブを復元する memo = memo.gsub("\\t","\t")

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

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

# メモデータを取得する def file_get_memo(fn) a = [] File.open(fn,'r:utf-8') do |f| f.each_line do |li| (id,memo,date) = li.split("\t") a << { id: id, memo: memo, date: date } end end return a end

保存したデータをファイルから取得しています。タブ区切りで保存したので、li.split("\t")で配列にしています。Rubyでは、() = で配列を項目ごとに受け取れます。また表示用の配列に連想配列(ハッシュ)で追加しています。key: valueの構成で作成した場合は、シンボル:keyで取り出せる連想配列になります。

# ファイル名を定数に FILE_NAME = 'memo.txt' # 表示データを取得 $DATA = file_get_memo(FILE_NAME) # 表示させるメモ memos = '' # データがある時 if $DATA.size memos =<<TAG <form method="post"> <table> TAG # 配列で順番に処理 $DATA.each do |li| # 表示用の変換 memo = h(li[:memo]) memo = memo.gsub("\\n","<br>") memo = memo.gsub("\\t","\t") memos <<=<<TAG <tr> <td>#{h(li[:date])}</td> <td>#{memo}</td> <td><label><input type="checkbox" name="id" value="#{li[:id]}">削除</label></td> </tr> TAG memos <<=<<TAG </table> <input type="submit" value="メモを削除"> </form>''' TAG end

取得したデータは、グローバル変数$DATAに配列で代入しています。表示を受け持つ処理に変数を渡す方法はいくつかありますが、このサイトではグローバル変数で渡す方針にしています。また今回は、表示用の文字列を変数memosに代入するかたちで実装しています。

Rubyでは、複数行の文字列をヒアドキュメントと呼ばれる方法で埋め込むことが出来ます。<<TAGから始まってTAGで終わります。TAGの文字列は任意です。

# ヘッダー情報、HTMLを指定 print "Content-Type: text/html; charset=utf-8\n\n" puts '<title>[Ruby]簡易メモ帳</title>' puts 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

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

最後に、指定したデータを削除する処理を実装します。チェックボックスからの入力は複数チェックされていた場合、CgiDecode.rbでは値が配列になっています。

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

# メモデータを保存する def file_put_memo(h) # ファイル名の指定がない時 return 0 unless h.key?(:f) # ファイルが存在しない場合、作成 unless File.exists?(h[:f]) File.open(h[:f],'w') do |f| f.write('') end end # 保存するデータ lines = h.key?(:line) ? h[:line]: '' lines = lines.force_encoding('utf-8') File.open(h[:f],'r+:utf-8') do |f| f.flock File::LOCK_EX f.each_line do |li| if h.key?(:del) (id,memo,date) = li.split("\t") unless h[:del].include?(id) lines << li end # 削除指定がない時 else lines << li end end f.seek(0,0) f.write(lines) f.flush f.truncate(f.pos) end end

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

# POST送信の時の処理 if ENV['REQUEST_METHOD']=='POST' h = {f:FILE_NAME} if $_POST.key?('id') h[:del] = $_POST.fetch('id','') else # memoをキーとして取得できる memo = $_POST.fetch('memo','') # IDを作成する id = uniqid() # 現在日時を作成 date = date() # 改行の統一とエスケープ memo.gsub!("\r\n","\n") memo.gsub!("\r","\n") memo.gsub!("\n","\\n") memo.gsub!("\t","\\t") h[:line] = memo=='' ? '': id +"\t"+ memo +"\t"+ date +"\n" end file_put_memo(h) puts "Status: 301 Moved Permanently" print 'Location: '+ENV['SCRIPT_NAME']+"\n\n" exit end