PythonでCGIのその2です。
前回の「その1」では、Python の CGI を使って
HTTPリクエスト → 標準出力で HTML を返す
という Web の最小構造を確認しました。
以下です。
https://qiita.com/isy-nishida/items/28b18a4b883593f3f2b8
今回はその続きとして——
Web入出力(HTTP) と OS入出力(ファイル操作)が組み合わさると「Webアプリの基礎」ができることを体験します。
以下のようなテーマです。
〜 Web入出力(HTTP)と OS入出力(ファイル)をつなぐ 〜
そして、CGI処理とファイル操作処理は 別ファイルのモジュールにして、
“構造化設計”の最も基本的な形(入力処理とデータ処理の分離)を試します。
📁 ディレクトリ構成の例
/home/ubuntu/
├ cgi-bin/
│ ├ post_form.py → フォーム表示+投稿処理メイン(CGI側)
│ └ data_store.py → ファイル操作モジュール(OS側)
└ data/
└ messages.txt → 投稿データ保存用(初回は空ファイルでOK)
- cgi-bin 配下のスクリプトはWeb側の処理
- data 配下のファイルは OS 側の永続データ
- data_store.py を用意して処理をモジュール化
🧩 ファイル操作モジュール(data_store.py)
# data_store.py
import os
DATA_FILE = os.path.join(os.path.dirname(__file__), "../data/messages.txt")
def save_message(name: str, comment: str):
"""1件分をファイルに追記する"""
line = f"{name}\t{comment}\n"
with open(DATA_FILE, "a", encoding="utf-8") as f:
f.write(line)
def load_messages():
"""全メッセージを読み込んでリストで返す"""
messages = []
if not os.path.exists(DATA_FILE):
return messages
with open(DATA_FILE, "r", encoding="utf-8") as f:
for line in f:
items = line.strip().split("\t")
if len(items) == 2:
messages.append({
"name": items[0],
"comment": items[1]
})
return messages
✔ このモジュールの役割
- CGI とは無関係に 「データの保存」と「読み込み」だけを担当
- Webからでも、将来的に別のプログラムからでも再利用できる
- Web処理とデータ処理を分離する=構造化設計の基本形
🧩 CGI 本体(post_form.py)
✔ CGI本体の役割
- URLから起動されて、ページとフォームを表示
- POSTで投稿データを受け取りファイル操作モジュールへ渡す
- 表示用のデータもファイル操作モジュールから受け取る
#!/usr/bin/env python3
import cgi
import cgitb
import html
cgitb.enable() # CGI デバッグ用
from data_store import save_message, load_messages
print("Content-Type: text/html; charset=utf-8\n")
form = cgi.FieldStorage()
name = form.getfirst("name", "")
comment = form.getfirst("comment", "")
# POSTされたデータがあれば保存
if name and comment:
save_message(
html.escape(name),
html.escape(comment)
)
# 全データを読み出す
messages = load_messages()
# HTML 出力
print("""
<html>
<head>
<meta charset="utf-8">
<title>Python CGI Message Board</title>
</head>
<body>
<h1>Python CGI Message Board</h1>
<form method="POST" action="/cgi-bin/post_form.py">
名前: <input type="text" name="name"><br>
ひとこと: <input type="text" name="comment"><br>
<input type="submit" value="投稿">
</form>
<hr>
<h2>投稿一覧</h2>
<ul>
""")
for m in messages:
print(f"<li><strong>{m['name']}:</strong> {m['comment']}</li>")
print("""
</ul>
</body>
</html>
""")
▶ 実行方法
1. 実行権限の付与
$ sudo chmod +x cgi-bin/post_form.py
2. Python の簡易 CGI サーバ起動
これは、cgi-bin ディレクトリの一つ上で起動します。ディレクトリ構成の例では、/home/ubuntu で実行します。
$ python3 -m http.server 8080 --cgi
3. ブラウザでアクセス
http://localhost:8080/cgi-bin/post_form.py
※補足ですが、PaizaCloudで実行する場合は、PaizaCloudの地球アイコンであれば、localhostで起動出来ます。そこで表示されたURLを通常のブラウズにコピーして起動すれば外部のブラウザでも起動出来ます。
PaizaCloudでの実行方法のより詳細が必要な場合は前回の記事も確認してみてください。
また何か動かないけどという場合も前回の記事も確認してみてください。
保存されたファイルを表示した例

このデータは、Webサーバー(python)を終了した後も保存されていて、再度起動された際には読み込まれます。
💡 今回の学習テーマ
✅ ① Web入出力(HTTP → CGI)
- GET / POST を
cgi.FieldStorage()で受け取る - HTMLを
printで標準出力に返す - Webは「文字列の入出力」で動くことが理解できる
✅ ② OS入出力(ファイル)
- ファイルの読み書きによる 永続データの保存
- Webリクエストの一時情報と、OS上のデータがつながる
✅ ③ モジュール化(構造化設計)
- Web側の処理とデータ側の処理を分離
- 処理の役割が明確になり、拡張しやすくなる
- フレームワーク構成の基礎になる考え方
📝 補足(初心者向け)
本記事では 初学者でも理解しやすいように、厳密な仕様より「分かりやすさ」を優先 しています。HTTP や CGI、セキュリティには本来さまざまな注意点がありますが、 ここでは WebとOS入出力の連携のイメージをつかむこと を目的にしています。
💡POSTで保存したデータを「検索」して表示する
引き続きもう少し以下のように拡張してみます。
〜 CGI入出力 × ファイル検索で、少しアプリっぽくする 〜
ということで、
「保存されている投稿データを検索する」
という機能を追加します。
🎯 拡張追加テーマ
- 保存されている投稿データから
「名前」で検索 する - 入力された名前に一致(部分一致)する投稿だけを表示
- 検索処理は ファイル操作モジュール側 に実装
- CGI側は「入力の受け取り」と「結果の表示」に専念
検索機能を追加することで、
Web入出力・データ処理・設計 が一通りそろった
“小さなWebアプリ” になります。
🧩 追加したcgi-bin配下の構成
cgi-bin/
├ post_form.py # 登録+一覧表示(前回)
├ search_form.py # 検索用CGI(今回追加)
└ data_store.py # ファイル保存・読込・検索ロジック実装版
🧠 設計の考え方(重要)
| 処理内容 | 担当 |
|---|---|
| HTTP入力(GET/POST) | CGI |
| HTML出力 | CGI |
| データ保存 | data_store |
| データ検索 | data_store |
検索ロジックを CGI に書かない
→ 実務ではとても重要な考え方です。
🧩 data_store.py(検索機能を追加)
# data_store.py
import os
DATA_FILE = "messages.txt"
def save_message(name, comment):
with open(DATA_FILE, "a", encoding="utf-8") as f:
f.write(f"{name}\t{comment}\n")
def load_messages():
messages = []
if not os.path.exists(DATA_FILE):
return messages
with open(DATA_FILE, "r", encoding="utf-8") as f:
for line in f:
name, comment = line.strip().split("\t")
messages.append({"name": name, "comment": comment})
return messages
def search_by_name(keyword):
results = []
messages = load_messages()
for m in messages:
if keyword in m["name"]:
results.append(m)
return results
✔ ポイント
- 検索条件は 部分一致
- 保存形式はそのまま
- CGI側は関数を呼ぶだけ
🧩 search_form.py(検索用CGI)
#!/usr/bin/env python3
import cgi
import cgitb
from data_store import search_by_name
cgitb.enable()
print("Content-Type: text/html; charset=utf-8\n")
form = cgi.FieldStorage()
keyword = form.getvalue("name", "")
print("<html><body>")
print("<h1>投稿検索</h1>")
print("""
<form method="get">
名前:<input type="text" name="name">
<input type="submit" value="検索">
</form>
<hr>
""")
if keyword:
results = search_by_name(keyword)
print(f"<h2>検索結果({len(results)}件)</h2>")
for r in results:
print(f"<p>{r['name']} : {r['comment']}</p>")
print("</body></html>")
🌍 実行方法
CGIサーバ起動
cgi-binの上のディレクトリで起動します。PaizaCloudでそのままのファイル構成の場合は、/home/ubuntu 配下で起動します。
python3 -m http.server 8080 --cgi
ブラウザでアクセス
http://localhost:8080/cgi-bin/search_form.py
実行例
このように保存されたデータに対して、西田で検索すると以下のような結果になります。

📌 CGIとして見た「検索」の正体
- ブラウザ
↓(HTTPリクエスト) - CGI
↓(GETパラメータ) - Python関数
↓(ファイル検索) - 標準出力
↓(HTTPレスポンス) - ブラウザ表示
検索も結局は
「文字列を受け取って、文字列を返しているだけ」
🧠 なぜこの構成が“実務向け”なのか
- 検索条件が変わっても
→ CGIをほぼ修正しなくてよい - 保存形式を変えても
→ CGIに影響しない - 後から
→ データベースに置き換えやすい
これは、
フレームワークで言うところの
Model / Controller 分離を
素の Python + CGI で体験している形です。
📘 この拡張のまとめ
- CGIは「入力と表示」に専念
- 検索ロジックはモジュール化
- 検索が入るだけでアプリらしくなる
- Webは
入出力+データ処理の組み合わせ
フレームワークを使う前に
一度「中身を素で作ってみる」
これはとても良い練習になります。
✅ 全体的なまとめ
- CGI は Web の入出力(HTTP)を最小構成で体験できる
- Python を使えば短いコードで安全に扱える
- Web側の入出力と OS側のファイル入出力が連携する
- モジュール分割により「構造化設計の基礎」が体験できる
「Webって、入力を受け取って、ファイルに書いて、HTML返すだけで動くんだ」
——そう感じられたら今回の目的は達成です。
次回は今回の内容を踏まえて実務的な単体テストという考え方についてです。
Python CGI その3:単体テストという考え
次回の分も含めたソース一式は以下に置きました。
https://github.com/ISYNishida/python-cgi-sample
以上、ありがとうございます。


