【Perl講座 第3回】掲示板を作ろう(前編)!ファイル操作(読み書き)と制御構文の基礎完全ガイド

データは「流す」ものではなく、「残す」もの。

こんにちは!「LINUX工房」管理人の「リナックス先生」です。
前回は、HTMLフォームから送信されたデータをPerlで受け取り、画面に表示するところまで進みました。

しかし、前回のプログラムには致命的な弱点がありました。
それは、「せっかく入力したデータが、画面を閉じると消えてしまう」ことです。
これでは、他のユーザーとメッセージを交換する「掲示板」や、記録を残す「アンケート」は作れません。

コウ君

先生、そうなんです!
自分の画面に「こんにちは」って出ても、他の人には見えないし、リロードしたら消えちゃうし…。
みんなが書き込んだ内容を、ずっと残しておくにはどうすればいいんですか?
やっぱりデータベースってやつが必要なんですか?

リナックス先生

本格的なシステムならデータベース(SQL)を使うけど、基本は「ファイルへの読み書き」よ。
サーバー上にあるテキストファイル(data.txtなど)に書き込んでおけば、電源を切ってもデータは消えないわ。
今回は、プログラミングの最重要スキル「ファイル操作(I/O)」をマスターして、自分だけの掲示板を作りましょう!

本記事では、Perlを使ってテキストファイルを読み書きする方法、複数の人が同時に書き込んでもデータが壊れないようにする「排他制御」、そして保存されたデータを一覧表示するための「ループ処理」を解説し、実際に動く掲示板システムを構築します。

🐪 Perlサーバサイドプログラミング講座(バックナンバー)

現在地:【第3回】掲示板を作ろう(前編)!制御構文とファイル操作(読み書き)の基礎


第1章:ファイル操作の基本「open」と「close」

プログラムからファイルを扱うには、まず「ファイルを開く(open)」、そして用が済んだら「ファイルを閉じる(close)」という手順が必要です。

ファイルハンドルとは?

Perlでは、開いたファイルを操作するために「ファイルハンドル」という専用の変数を使います。
昔のPerlでは INOUT のような大文字の裸のワードを使っていましたが、現在は $fh(File Handleの略)のように、通常のスカラ変数を使うのがモダンな作法です(レキシカルファイルハンドル)。

open関数の基本構文

open(my $fh, "モード", "ファイル名") or die "エラーメッセージ: $!";
  • my $fh: ファイルハンドルを受け取る変数。
  • モード: 読み込むのか、書き込むのかを指定する記号。
  • or die ...: ファイルが開けなかった場合(ファイルがない、権限がないなど)に、プログラムを強制終了してエラーを表示する定型句。必ず書きましょう。

3つの重要なモード

モード記号 動作 内容
< 読み込み (Read) ファイルの内容を読み取るだけ。内容は変更されない。ファイルがないとエラー。
> 書き込み (Write) 新しい内容でファイルを上書きする。元の内容は消える。ファイルがなければ作る。
>> 追記 (Append) ファイルの末尾に新しい内容を追加する。元の内容は消えない。掲示板はこれを使う。

第2章:【書き込み編】掲示板への投稿機能を作る

まずは、フォームから送られてきたデータを、サーバー上の bbs.txt に保存する機能を作ります。
掲示板では、新しい投稿を「追記(Append)」していく必要があります。

1. データ形式の設計

1つのファイルに複数の情報を保存するには、ルールを決める必要があります。
今回はシンプルに、1行を1つの投稿とし、各項目を「タブ文字(\t)」で区切ることにします(TSV形式)。

名前 <タブ> メッセージ <タブ> 日時 <改行>

2. 排他制御(flock)の重要性

Webアプリケーションでは、複数のユーザーが「同時に」書き込みボタンを押す可能性があります。
もし同時にファイルを開いて書き込もうとすると、データが混ざったり、ファイルが壊れて中身が全部消えたりします。
これを防ぐために、「今、俺が書き込んでるから、他の人は待ってて!」とロックを掛ける仕組みが flock です。

3. 書き込みプログラムの作成

/var/www/cgi-bin/bbs_write.cgi を作成します。

#!/usr/bin/perl

use strict;
use warnings;
use CGI;
use Encode;
use Fcntl qw(:flock); # ロック用の定数を使う宣言

# 文字化け対策
binmode STDOUT, ":utf8";

my $q = CGI->new;
my $file = "data.txt";

# パラメータ取得とデコード
my $name = decode('UTF-8', $q->param('name'));
my $message = decode('UTF-8', $q->param('message'));

# 日時の取得
my $time = localtime;

# 入力チェック
if ($name ne "" && $message ne "") {
    
    # ファイルを「追記モード」で開く
    open(my $fh, ">>", $file) or die "Cannot open file: $!";
    
    # ファイルをロックする(排他ロック: LOCK_EX)
    flock($fh, LOCK_EX);
    
    # ファイルの末尾に移動(念のため)
    seek($fh, 0, 2);
    
    # データの結合(タブ区切り)と改行
    # 書き込み時はエンコードする
    my $line = "$name\t$message\t$time\n";
    print $fh encode('UTF-8', $line);
    
    # ファイルを閉じる(同時にロックも解除される)
    close($fh);
}

# 完了画面の表示(リダイレクトしても良いが今回は表示)
print $q->header(-charset => 'UTF-8');
print "<html><body>";
print "<h2>書き込みました</h2>";
print "<a href='bbs_read.cgi'>掲示板を見る</a>";
print "</body></html>";

4. 権限設定(重要!)

CGIがファイルに書き込むためには、データファイルに対する「書き込み権限」が必要です。

# 空のデータファイルを作成
sudo touch /var/www/cgi-bin/data.txt

# 権限を「誰でも読み書きOK(666)」にする
# 本来はApacheユーザー(apache)に所有権を持たせるのが安全ですが、
# 学習用として手っ取り早い666を使います。
sudo chmod 666 /var/www/cgi-bin/data.txt

# SELinux対策(読み書き可能なコンテキストを付与)
sudo chcon -t httpd_sys_rw_content_t /var/www/cgi-bin/data.txt

⚠️ SELinuxの罠
第1回でCGIファイルには httpd_sys_script_exec_t を付けましたが、CGIが書き込むファイルには httpd_sys_rw_content_t (Read/Write Content) という別のラベルが必要です。
これを忘れると、Permission denied で書き込めません。


第3章:制御構文「ループ処理」をマスターする

書き込みができたら、次はそれを読み込んで表示します。
ファイルには何行データがあるか分かりません(1行かもしれないし、1000行かもしれない)。
そんな時は「データがある限り繰り返す」というループ処理を使います。

1. whileループ(ファイル読み込みの定番)

条件が真(True)である間、ひたすら繰り返します。
ファイル操作では「次の行が読み込めたら(真)、処理する。読み込めなかったら(偽)、終わる」という使い方が定石です。

open(my $fh, "<", "data.txt");

# 1行ずつ読み込んで $line に入れる
while (my $line = <$fh>) {
    print $line;
}
close($fh);

2. foreachループ(配列の処理)

すでに配列に入っているデータを、最初から最後まで順番に処理します。

my @users = ("田中", "鈴木", "佐藤");

foreach my $u (@users) {
    print "こんにちは、$u さん\n";
}

第4章:【読み込み編】掲示板の表示機能を作る

data.txt を読み込んで、HTMLの表(Table)として表示するプログラム bbs_read.cgi を作ります。

1. 文字列操作関数(chomp, split)

ファイルから読み込んだデータは、そのままでは使えません。

  • chomp: 行末の改行文字を削除する関数。
  • split: 文字列を特定の区切り文字で分割して、配列にする関数。

例:"田中\tこんにちは\n" → chomp → "田中\tこんにちは" → split(\t) → ("田中", "こんにちは")

2. 読み込みプログラムの作成

sudo nano /var/www/cgi-bin/bbs_read.cgi

コード記述:

#!/usr/bin/perl

use strict;
use warnings;
use CGI;
use Encode;

binmode STDOUT, ":utf8";
my $q = CGI->new;
my $file = "data.txt";

print $q->header(-charset => 'UTF-8');
print "<html><body>";
print "<h1>Perl掲示板</h1>";

# 入力フォームも一緒に表示する
print <<HTML;
<form action="bbs_write.cgi" method="POST">
    名前: <input type="text" name="name"><br>
    内容: <textarea name="message"></textarea><br>
    <input type="submit" value="書き込む">
</form>
<hr>
<table border="1">
<tr><th>名前</th><th>メッセージ</th><th>日時</th></tr>
HTML

# ファイルを開く(読み込みモード)
if (open(my $fh, "<", $file)) {
    
    # 最新の投稿を上にしたい場合、一度配列に読み込んで逆順にするテクニック
    my @lines = <$fh>;   # 全部読み込む
    close($fh);
    @lines = reverse @lines; # 逆順にする

    foreach my $line (@lines) {
        # 文字コード変換(ファイルから読んだらデコード!)
        $line = decode('UTF-8', $line);
        chomp($line); # 改行削除
        
        # タブで分割して、それぞれの変数に入れる
        my ($name, $msg, $time) = split(/\t/, $line);
        
        # HTMLエスケープ(XSS対策:第7回で詳しくやりますが、簡易的に)
        # 本来はモジュールを使うべきですが、今回は基本動作優先で省略
        
        print "<tr>";
        print "<td>$name</td>";
        print "<td>$msg</td>";
        print "<td>$time</td>";
        print "</tr>\n";
    }
} else {
    print "<p>まだ投稿はありません。</p>";
}

print "</table>";
print "</body></html>";

3. 権限設定

sudo chmod 755 /var/www/cgi-bin/bbs_read.cgi
# 必要なら chcon も

第5章:動作確認とデータ管理の注意点

ブラウザで http://サーバーIP/cgi-bin/bbs_read.cgi にアクセスしてみましょう。
入力フォームと、投稿一覧が表示されましたか?
適当に入力して「書き込む」を押し、一覧に追加されれば成功です!

データファイルが見えてしまう問題

現在の設定では、ブラウザで http://サーバーIP/cgi-bin/data.txt に直接アクセスすると、生データが丸見えになってしまいます。
これを防ぐ方法はいくつかあります。

  1. Web公開領域の外に置く:
    /var/www/data/ など、DocumentRootやScriptAlias以外の場所にファイルを置き、CGIからはフルパスで指定する。これが一番安全です。
  2. ドットファイルにする:
    ファイル名を .data.txt のようにドットから始めると、Apacheの設定によってはアクセス拒否されます(.htaccessと同様)。
  3. 拡張子を変える:
    data.dat などにして、Apacheの設定で .dat へのアクセスを拒否する。

実務では「1. 公開領域の外に置く」が鉄則です。


第6章:トラブルシューティング

うまくいかない場合のチェックリストです。

Q. 書き込みボタンを押しても何も増えない

  • 権限エラー: data.txt のパーミッションは 666 になっていますか?
  • SELinux: data.txt のコンテキストは httpd_sys_rw_content_t になっていますか?
  • パス間違い: プログラム内のファイル名は合っていますか?

Q. 文字化けする

  • Perlスクリプトの保存形式: UTF-8になっていますか?
  • binmode: binmode STDOUT, ":utf8"; を書いていますか?
  • 入出力のエンコード: 書き込み時は encode、読み込み時は decode をしていますか?

まとめ:ファイル操作はすべての基本

お疲れ様でした!
これで、データを受け取り、保存し、表示するというWebアプリケーションの基本サイクルが完成しました。

今回の重要ポイント:

  • open 関数でファイルを開き、モード(<, >, >>)を指定する。
  • flock で排他制御をしないと、データが壊れるリスクがある。
  • while (<$fh>) でファイルを1行ずつ読み込む。
  • data.txt への書き込み権限とSELinux設定を忘れない。

しかし、今回はすべての機能を「ゼロから手書き」しました。
「HTMLのエスケープ処理」や「メール送信」、「画像のアップロード」など、よくある機能を毎回手書きするのは大変ですし、バグの元です。

次回、第4回は「車輪の再発明を防ぐ!CPANモジュールの導入とCGI.pmの活用」です。
世界中のPerlハッカーたちが作った便利なライブラリ群「CPAN(シーパン)」を使って、もっと楽に、もっと高度なプログラミングをする方法を学びます。
お楽しみに!

▼ エンジニアとしてのキャリアを加速させる ▼

掲示板を公開するなら
「VPS」で自分専用環境

おすすめVPSを見る

サーバー知識を年収に
「ITエンジニア転職」

転職エージェントを見る

コメント