GETとPOST

今日の目標!

今日作るもの

GETとPOSTで動きが変わる、おみくじアプリを作るよ。

おみくじ

今回のリクエスト:POST
        中吉
選択肢:大吉・中吉・小吉・末吉

いつものおみくじ(GET)
大吉・吉・凶の中から1つ選ぶよ。
[いつものおみくじを引く]

オリジナルおみくじ(POST)
選択肢(空白で区切って書こう)
[大吉 中吉 小吉 末吉        ]
[オリジナルおみくじを引く]
リクエスト おみくじ
GET 用意された「大吉・吉・凶」から引く
POST フォームに書いた選択肢から引く

ページは1枚だけ。同じURLでも、GETとPOSTのどちらで送るかによって、おみくじの中身が変わるよ。

GETとPOSTは、お願いの種類

ブラウザからサーバーへ送るリクエストには、GETやPOSTなどのHTTPメソッドを指定するよ。

メソッド 主な目的 今回の使い方
GET ページやデータを受け取る いつものおみくじを引く
POST データを送って、サーバーに処理してもらう 書いた選択肢を送って、おみくじを引いてもらう

「データをちょうだい!!!」というのがGET 「このデータを上げるから、こういうふうに表示してね」というのがPOST

ファイルを用意しよう

VS Codeで、以下のフォルダ構成を用意しよう

2_get_post
├── get_post.md       
├── app.py
├── templates
│   └── index.html
└── static
    └── style.css

前回のFlaskサーバーが動いていたら、そのターミナルでCtrl+Cを押して停止しておこう。

ターミナルで、Flaskが入っているか確認しよう。

python -m pip show flask

見つからない場合は、次のコマンドでインストールしよう。

python -m pip install flask

まずは完成コードを動かそう

最初に3つのファイルを用意しよう。遊んでみてから、コードを読んでいくよ。

app.py:リクエストを受け取って、おみくじを引く

import random

from flask import Flask, render_template, request


app = Flask(__name__)

# いつものおみくじの選択肢
DEFAULT_CHOICES = ["大吉", "吉", "凶"]


@app.route("/", methods=["GET", "POST"])
def omikuji():
    if request.method == "POST":
        # フォームに書いた選択肢を、空白で区切ってリストにする
        choices = request.form.get("choices", "").split()
    else:
        choices = DEFAULT_CHOICES

    # 選択肢が空のときは、いつものおみくじにする
    if not choices:
        choices = DEFAULT_CHOICES

    result = random.choice(choices)

    return render_template(
        "index.html",
        method=request.method,
        choices=choices,
        result=result,
    )


if __name__ == "__main__":
    app.run(debug=True, port=8000)

templates/index.html:結果と2つのフォーム

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>おみくじ</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
    <main>
        <h1>おみくじ</h1>

        <section class="result">
            <p>今回のリクエスト:{{ method }}</p>
            <p class="fortune">{{ result }}</p>
            <p>選択肢:{{ choices | join("・") }}</p>
        </section>

        <section>
            <h2>いつものおみくじ(GET)</h2>
            <p>大吉・吉・凶の中から1つ選ぶよ。</p>
            <form action="/" method="get">
                <button type="submit">いつものおみくじを引く</button>
            </form>
        </section>

        <section>
            <h2>オリジナルおみくじ(POST)</h2>
            <form action="/" method="post">
                <label for="choices">選択肢(空白で区切って書こう)</label>
                <input id="choices" name="choices" type="text" placeholder="例:大吉 中吉 小吉 末吉" required>
                <button type="submit">オリジナルおみくじを引く</button>
            </form>
        </section>
    </main>
</body>
</html>

static/style.css:見た目を整える

body {
    margin: 0;
    padding: 24px;
    background: #f5f3eb;
    color: #24352b;
    font-family: sans-serif;
    line-height: 1.8;
}

main {
    max-width: 680px;
    margin: 0 auto;
}

section {
    margin-bottom: 24px;
    padding: 16px 24px;
    background: white;
    border-radius: 8px;
}

h2 {
    font-size: 1.1rem;
}

.result {
    text-align: center;
    border: 3px solid #b23a2e;
}

.fortune {
    margin: 0;
    color: #b23a2e;
    font-size: 3rem;
    font-weight: bold;
}

form {
    display: grid;
    gap: 8px;
}

input, button {
    padding: 10px;
    font: inherit;
}

button {
    background: #b23a2e;
    color: white;
    border: none;
    cursor: pointer;
}

起動して遊んでみよう

ターミナルがapp.pyのある2_get_postフォルダを開いていることを確認して、実行する。

python app.py

ブラウザで次のURLを開こう。index.htmlを直接ダブルクリックするのではなく、FlaskのURLへアクセスしてね。

http://127.0.0.1:8000/
  1. ページを開いただけで、もうおみくじの結果が出ている。「今回のリクエスト」はGETになっているね。
  2. 「いつものおみくじを引く」を何度か押す。大吉・吉・凶のどれかが出る。
  3. オリジナルおみくじの欄に大吉 中吉 小吉 末吉と書いて、「オリジナルおみくじを引く」を押す。「今回のリクエスト」がPOSTになり、書いた選択肢から結果が出る。
  4. 選択肢を自由に変えて引いてみよう。カレー ラーメン すしのように、今日の晩ごはんを決めるおみくじにもできるよ。

選択肢は半角スペースでも全角スペースでも区切れるよ。

GETでおみくじを引く仕組み

ページを開くのもGET

アドレス欄にURLを入力してページを開くと、ブラウザはGETのリクエストを送る。だから、ページを開いただけで「いつものおみくじ」が引かれていたんだ。

フォームでGETを送る

<form action="/" method="get">
    <button type="submit">いつものおみくじを引く</button>
</form>
書き方 役割
action="/" 送り先のURL
method="get" GETで送るという指定
type="submit" フォームを送るボタン

このフォームには入力欄がないので、送る値はない。ボタンを押すと/へGETのリクエストが送られ、ページを開き直したのと同じことになるよ。アドレス欄の最後に?が付くのは、「GETで送る値(ここでは空っぽ)」の目印なんだ。

POSTでオリジナルおみくじを引く仕組み

フォームでPOSTを送る

<form action="/" method="post">
    <input id="choices" name="choices" type="text" ... required>
    <button type="submit">オリジナルおみくじを引く</button>
</form>

method="post"なので、ボタンを押すとPOSTで送られる。送り先は、GETと同じ/だよ。

入力欄のname="choices"が、送るときの名前になる。

入力欄に「大吉 中吉 小吉 末吉」と書く
        ↓ ボタン
POST /
  choices = 大吉 中吉 小吉 末吉

POSTでは、入力した値はURLではなく、リクエストの本文(ボディ)に入って送られる。だからアドレス欄には選択肢が出てこないんだ。

requiredは、空欄のまま送らないようブラウザに確認してもらう指定だよ。

Flaskでrequest.formから受け取る

choices = request.form.get("choices", "").split()

requestには、ブラウザから届いたリクエストの情報が入っている。POSTで送られたフォームの値は、request.formから取り出すよ。

書き方 役割
request.form.get("choices", "") choicesという名前の値を取り出す。なければ空文字列""
.split() 文字列を空白で区切って、リストにする
"大吉 中吉 小吉 末吉"
        ↓ split()
["大吉", "中吉", "小吉", "末吉"]

入力欄のnameと、request.form.get()の名前は、そろえておく必要があるよ。

1つのURLで、GETとPOSTを受け分ける

@app.route("/", methods=["GET", "POST"])
def omikuji():
    if request.method == "POST":
        choices = request.form.get("choices", "").split()
    else:
        choices = DEFAULT_CHOICES

methods=["GET", "POST"]は、「/ではGETとPOSTの両方を受け付ける」という設定。書かなかった場合はGETだけを受け付けるよ。

request.methodには、届いたリクエストのメソッドが"GET"や"POST"という文字列で入っている。if文で、選択肢をどちらにするか分けているんだ。

ページを開く・いつものボタン → GET  / → choices = ["大吉", "吉", "凶"]
オリジナルのボタン           → POST / → choices = フォームに書いた選択肢

同じURLでも、メソッドが違えば別のお願いになる。これがGETとPOSTを区別する意味だよ。

選択肢が空っぽのとき

if not choices:
    choices = DEFAULT_CHOICES

入力欄にスペースだけを書いて送ると、split()の結果は空のリスト[]になる。空のリストからは選べないので、そのときは「いつものおみくじ」にしている。Pythonでは、空のリストは条件式で偽として扱われるよ。

おみくじを引く:random.choice()

result = random.choice(choices)

random.choice()は、リストから要素を1つランダムに選ぶ関数だよ。おみくじの仕組みは、この1行だけ!

結果をHTMLに表示する

return render_template(
    "index.html",
    method=request.method,
    choices=choices,
    result=result,
)

render_template()で、メソッド・選択肢・結果をHTMLへ渡している。HTML側では{{ }}で表示するよ。

<p>今回のリクエスト:{{ method }}</p>
<p class="fortune">{{ result }}</p>
<p>選択肢:{{ choices | join("・") }}</p>

choices | join("・")は、リストの要素を「・」でつないで1つの文字列にする書き方。["大吉", "吉", "凶"]が大吉・吉・凶と表示されるよ。

GETとPOSTを比べてみよう

実験1:アドレス欄と履歴を見る

いつものおみくじとオリジナルおみくじを引いた後、アドレス欄を見てみよう。どちらも/(GETでは最後に?)で、オリジナルの選択肢はどこにも出ていないね。

実験2:再読み込みする

「いつものおみくじ」を引いた後に再読み込みすると、何も聞かれずに新しい結果が出る。

「オリジナルおみくじ」を引いた後に再読み込みすると、「フォームを再送信しますか?」のような確認が出るよ。POSTは「データを送って処理してもらう」お願いなので、ブラウザが「同じ内容をもう一度送っていい?」と確認してくれるんだ。

「続行」を押すと、同じ選択肢がもう一度POSTで送られ、新しい結果が出る。

実験3:POSTのフォームをGETに変えてみる

index.htmlのオリジナルおみくじのフォームを、method="get"に書き換えて保存しよう。

<form action="/" method="get">

ブラウザを再読み込みしてから、選択肢を書いて「オリジナルおみくじを引く」を押す。

http://127.0.0.1:8000/?choices=カレー+ラーメン+すし

アドレス欄に選択肢が出てきたね。GETでは、送る値がURLの?より後ろに付くんだ。

でも、出てくる結果は大吉・吉・凶のどれか。Flaskはrequest.methodが"GET"なので、いつものおみくじにしているんだ。

確かめたら、method="post"に戻して保存しよう。

まとめ

GET POST
値を入れる場所 URLの?より後ろ リクエストの本文
アドレス欄・履歴 値が見える・残る 値は見えない
再読み込み そのまま送り直す 再送信の確認が出る
Flaskでの受け取り方 request.args request.form
向いている使い方 ページを開く、検索する ログイン、登録、送信

もしパスワードをGETで送ると、URLごと履歴に残り、画面をのぞいた人にも見えてしまう。パスワードや個人情報、登録する内容はPOSTで送るのが基本だよ。

GETでURLに付いた値は、Flaskではrequest.argsで取り出せる。今回は使わないけれど、検索ページなどでよく使うよ。

検証ツールでGET・POSTを見てみよう

ブラウザの検証ツールを開いて、「Network(ネットワーク)」を選ぼう。

  1. 「いつものおみくじを引く」を押し、一番上のリクエストを選ぶ。
  2. 「Headers(ヘッダー)」のRequest MethodがGETになっているか見る。
  3. オリジナルおみくじを引き、同じようにリクエストを選ぶ。
  4. Request MethodがPOSTになっているか見る。
  5. 「Payload(ペイロード)」の「Form Data」で、choicesと書いた選択肢を探す。

CSSなどのリクエストも表示されるので、localhostや127.0.0.1という名前のリクエストに注目してね。

POSTでも、検証ツールを開けば送った値が見えるね。POSTは「アドレス欄や履歴に残さない」送り方で、「誰にも見えない」送り方ではないんだ。

通信そのものを守るのは、URLがhttps://で始まるときの仕組みの役目だよ。

練習してみよう

1.いつものおみくじを増やす

DEFAULT_CHOICESに"中吉"、"小吉"、"末吉"を追加しよう。HTMLの「大吉・吉・凶の中から1つ選ぶよ。」の文も、合わせて直してね。

2.出やすさを変える

オリジナルおみくじに大吉 大吉 大吉 凶と書いて、何度か引いてみよう。大吉が出やすくなるね。random.choice()はリストの要素を同じ確率で選ぶので、同じ言葉を多く入れると出やすくなるんだ。

3.どちらを使う?

次の場合、GETとPOSTのどちらが向いているか考えよう。

  1. 天気予報サイトで、都市名を入れて検索する
  2. ゲームのアカウントにログインする
  3. 掲示板にコメントを書き込む
  4. 動画サイトで、おすすめの動画一覧を見る

発展:画像おみくじ

文字の代わりに画像で引ける、おみくじの画像版だよ。GETでは大吉・吉・凶の画像から、POSTではフォームで選んだ画像から1枚を選ぶ。早く終わった人は、動かして遊んでみよう。

画像おみくじのコード
画像おみくじのフォルダ
├── app.py
├── templates
│   └── index.html
└── static
    ├── style.css
    └── img
        ├── daikichi.png
        ├── kichi.png
        └── kyou.png

static/imgには、大吉・吉・凶の画像を用意しよう。

app.py

import base64
import random

from flask import Flask, render_template, request


app = Flask(__name__)

# いつものおみくじの選択肢(名前と画像の場所)
DEFAULT_CHOICES = [
    {"name": "大吉", "src": "/static/img/daikichi.png"},
    {"name": "吉", "src": "/static/img/kichi.png"},
    {"name": "凶", "src": "/static/img/kyou.png"},
]


def to_data_url(image):
    # 画像のデータを文字に変換して、<img>でそのまま表示できる形にする
    data = base64.b64encode(image.read()).decode()
    return f"data:{image.mimetype};base64,{data}"


@app.route("/", methods=["GET", "POST"])
def omikuji():
    if request.method == "POST":
        # フォームで送られた画像を、1枚ずつ選択肢に加える
        choices = []
        for image in request.files.getlist("images"):
            if image.filename:
                choices.append({"name": image.filename, "src": to_data_url(image)})
    else:
        choices = DEFAULT_CHOICES

    # 画像が送られていないときは、いつものおみくじにする
    if not choices:
        choices = DEFAULT_CHOICES

    result = random.choice(choices)

    return render_template(
        "index.html",
        method=request.method,
        choices=choices,
        result=result,
    )


if __name__ == "__main__":
    app.run(debug=True, port=8000)

templates/index.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>画像おみくじ</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
    <main>
        <h1>画像おみくじ</h1>

        <section class="result">
            <p>今回のリクエスト:{{ method }}</p>
            <img class="fortune" src="{{ result.src }}" alt="{{ result.name }}">
            <p>{{ result.name }}</p>

            <p>選択肢</p>
            <div class="choices">
                {% for choice in choices %}
                    <img src="{{ choice.src }}" alt="{{ choice.name }}">
                {% endfor %}
            </div>
        </section>

        <section>
            <h2>いつものおみくじ(GET)</h2>
            <p>大吉・吉・凶の中から1つ選ぶよ。</p>
            <form action="/" method="get">
                <button type="submit">いつものおみくじを引く</button>
            </form>
        </section>

        <section>
            <h2>画像でオリジナルおみくじ(POST)</h2>
            <form action="/" method="post" enctype="multipart/form-data">
                <label for="images">画像を選ぼう(何枚でもOK)</label>
                <input id="images" name="images" type="file" accept="image/*" multiple required>
                <button type="submit">オリジナルおみくじを引く</button>
            </form>
        </section>
    </main>
</body>
</html>

static/style.css

body {
    margin: 0;
    padding: 24px;
    background: #f5f3eb;
    color: #24352b;
    font-family: sans-serif;
    line-height: 1.8;
}

main {
    max-width: 680px;
    margin: 0 auto;
}

section {
    margin-bottom: 24px;
    padding: 16px 24px;
    background: white;
    border-radius: 8px;
}

h2 {
    font-size: 1.1rem;
}

.result {
    text-align: center;
    border: 3px solid #b23a2e;
}

.fortune {
    width: 240px;
    max-width: 100%;
    height: 240px;
    object-fit: contain;
}

.choices {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 8px;
}

.choices img {
    width: 64px;
    height: 64px;
    object-fit: contain;
    border: 1px solid #ddd;
    border-radius: 4px;
}

form {
    display: grid;
    gap: 8px;
}

input, button {
    padding: 10px;
    font: inherit;
}

button {
    background: #b23a2e;
    color: white;
    border: none;
    cursor: pointer;
}

うまく動かないとき

症状 確認すること
No module named 'flask' 今使っているPythonでpython -m pip install flaskを実行したか
TemplateNotFound app.pyの隣のtemplatesフォルダにindex.htmlがあるか
{{ ... }}がそのまま表示される HTMLファイルを直接開かず、FlaskのURLを開いているか
405 Method Not Allowed @app.route()にmethods=["GET", "POST"]を書いたか
オリジナルで引いても大吉・吉・凶になる フォームがmethod="post"か。入力欄のnameとrequest.form.get()の名前が一致しているか
Address already in use 前回のFlaskが動いていないか。先にそのターミナルでCtrl+Cを押そう

今日の振り返り

授業の最後はターミナルでCtrl+Cを押して、テストサーバーを停止しよう。

次回は、今日の「送る・受け取る」を使って、川柳APIとつながるWebサイトを作るよ!