今週で最後の CS50。その集大成として今まで学んだ言語と技術を結集させてウェブアプリの開発をするのが今週の講義内容。前回も JavaScript を使ってある程度の動的なウェブアプリを開発したが、これはクライアントサイドのみ。今回は、データベースを含むサーバーとのやり取りであるバックエンドを含めたウェブアプリの開発を行う。
Web programming
前回の講義では、CS50 IDEで http-server
と実行すれば、ウェブサーバーが立ち上がった。今回は、これを Flask というライブラリー(または一般的にはフレームワークと呼ばれている)を使って自分で実装してみるところから。

Flask(フラスク)
では、Flask とは何か? Flask は Python のライブラリーで、ウェブサーバーを作るためのフレームワークである。フレームワークとは、どのようにファイルを整理し(フォルダの構造など)、どこに何を記述すべきかなどをある程度決めたもののことを言う。
まず、基本的なことは、4つのフォルダ・ファイルを作成することから。
application.py
requirements.txt
static/
templates/
application.py
は、Python のコードを記述するもの(コントローラー)requirements.txt
は、アプリに必要なライブラリーの一覧を保存するものstatic/
は、画像ファイルなどの静的データ、CSS、JavaScript などのファイルを保存するディレクトリtemplates/
は、HTML を作成するのに必要なファイルを保存する場所
ウェブサーバーのフレームワークは、Flask 以外にも Python 用の Django などがあるが、この講義では動作が軽い Flask を扱う。フレームワークは一般的に MVC
と呼ばれる Model
(モデル)、View
(ビュー)、Controller
(コントローラー)という3つの要素から成り立つ。コントローラーが今まで書いてきていたような Python のプログラムのこと。View は、ユーザーインターフェースとなるところ、故に HTML や CSS など。モデルは、アプリのデータがある SQL データベースや CSV ファイルなど。

Flask を使った最も簡単なプログラムは次のようになる。
from flask import Flask
app = Flask(__name__)
@app.route("/")
def index():
return "hello, world"
- まず、
Flask
をflask
ライブラリーから読み込む - 次に、
app
変数にファイル名をFlask
関数の引数として渡す @app.route
は、ファイル名のルートに"/"
を指示。@
は Python のデコレーターと呼ばれている- そして、最後に
index()
を実行する。これは、デフォルトパス/
が呼ばれたときに実行される
サーバーを立ち上げるコマンド flask run
を実行すると、hello, world を返す。ただし、これは文字列の hello, world を返しているだけであって、HTML 構文の形では返していない。Chrome を使っている場合は、ディベロッパーツールズより確認可能だ。
では、HTMLを返すようにプログラムを修正してあげるために、render_template
ライブラリーを読み込み、それを使って index.html
を返す。よって、index.html
ファイルに返したい HTML 構文を記述すれば OK。ファイルは、templates
ディレクトリに保存することに注意(これは Flask
を使う上でのルール)。
from flask import Flask, render_template
app = Flask(__name__)
@app.route("/")
def index():
return render_template("index.html")
さらに、render_template
は複数の引数を取ることができる。
from flask import Flask, render_template, request
app = Flask(__name__)
@app.route("/")
def index():
return render_template("index.html", name=request.args.get("name", "world"))
変数 name
を宣言し、HTML を動的に書き換えることができる。これで、ウェブブラウザ上で URL の最後に /?name=David
と入力すれば、結果は、hello, David
となる。
<!DOCTYPE html>
<html lang="en">
<head>
<title>hello</title>
</head>
<body>
hello, {{ name }}
</body>
</html>
Google 検索も同じことをしている。例えば、cat の検索結果を表示させたい場合は、/?q=cat
をサーバーに渡し、その結果を返している。
Forms(フォーム)
次に、ユーザーの名前に対応したページを作るために greet.html
を作り、index.html
の中にそこに向かうルートと、入力された名前を新ページに渡すプログラムを書く。
<!DOCTYPE html>
<html lang="en">
<head>
<title>hello</title>
</head>
<body>
<form action="/greet" method="get">
<input name="name" type="text">
<input type="submit">
</form>
</body>
</html>
/greet
ルートを使い、name
に名前を代入するapplications.py
コントローラーでも/greet
を追加する必要がある
@app.route("/")
def index():
return render_template("index.html")
@app.route("/greet")
def greet():
return render_template("greet.html", name=request.args.get("name", "world"))
POST
Form の例では、GET メソッドを使ったことによって、Form に入力したデータが URL に含まれてしまった。これを POST メソッドに変える <form action="/greet" method="post">
。これに伴い、コントローラーも POST メソッドを取り扱えるように次のように変える必要がある。
@app.route("/greet", methods=["POST"])
def greet():
return render_template("greet.html", name=request.form.get("name", "world"))
Flask の最も最悪なネーミングの一つで分かりづらい部分が、GET リクエストは request.args
、POST リクエストの場合は request.form
と指定する。この変更を加えた後に、再度リンクを辿ると今度は URL に何も表示されていないことが確認できるだろう。
Layout(レイアウト)
index.html
と greet.html
では同じ HTML 構文を繰り返さないといけなかったので、Flask テンプレートを使って繰り返している部分を省略することができる(他の Web フレームワークでも可)。そのために、まず基本となるページ(テンプレート) layout.html
を作る。
<!DOCTYPE html>
<html lang="en">
<head>
<title>hello</title>
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
Flask は、Jiinja というテンプレート言語を使用し、その構文である {% %}
を用いることで、各ページの共通する部分はプレースホルダーブロックを置き、テンプレートで指定した構文を挿入する。例えば、index.html
は次のように書き換えることができる。
{% extends "layout.html" %}
{% block body %}
<form action="/greet" method="post">
<input autocomplete="off" autofocus name="name" placeholder="Name" type="text">
<input type="submit">
</form>
{% endblock %}
同様に、greet.html
は次のようになる。
{% extends "layout.html" %}
{% block body %}
hello, {{ name }}
{% endblock %}
GET メソッド、POST メソッドを2つ同時に指定することも可能。index()
関数の中で、条件分岐させてあげれば良い。
@app.route("/", methods=["GET", "POST"])
def index():
if request.method == "GET":
return render_template("index.html")
if request.method == "POST":
return render_template("greet.html", name=request.form.get("name", "world"))
ウェブアプリ導入
講師の David が学生の頃に初めて作成した Web アプリケーションを例に講義は進む。
<meta>
タグは複数の属性の指定が可能で、viewport
メタデータに content
属性を使うことで、スマートフォンなどで開いたときにも見やすいように対応してくれる。
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="initial-scale=1, width=device-width">
<title>froshims</title>
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
application.py
のプログラムの中で、デフォルトの /
ルートに対しては index.html
を返すよう指示。
from flask import Flask, render_template, request
app = Flask(__name__)
SPORTS = [
"Dodgeball",
"Flag Football",
"Soccer",
"Volleyball",
"Ultimate Frisbee"
]
@app.route("/")
def index():
return render_template("index.html")
index.html
テンプレートを作る。index.html
の中身は、生徒の名前と好きなスポーツを選択し、登録してもらうというシンプルなもの。
{% extends "layout.html" %}
{% block body %}
<h1>Register</h1>
<form action="/register" method="post">
<input autocomplete="off" autofocus name="name" placeholder="Name" type="text">
<select name="sport">
<option disabled selected value="">Sport</option>
<option value="Dodgeball">Dodgeball</option>
<option value="Flag Football">Flag Football</option>
<option value="Soccer">Soccer</option>
<option value="Volleyball">Volleyball</option>
<option value="Ultimate Frisbee">Ultimate Frisbee</option>
</select>
<input type="submit" value="Register">
</form>
{% endblock %}
application.py
プログラムの中では、/register
ルートに対して POST メソッドでデータの引き渡しをするようにし、ユーザーが何も入力しないで登録しようとした場合にエラーページ(failure.html
)をレンダリングする。それ以外の場合は、success.html
をレンダリングする。
@app.route("/register", methods=["POST"])
def register():
if not request.form.get("name") or not request.form.get("sport"):
return render_template("failure.html")
return render_template("success.html")
ただし、このプログラムには問題点が存在する。ユーザーが Chrome 等のディベロッパーツールズなどを使えば、スポーツの選択肢を勝手に増やし、その結果を登録できてしまう。サーバーにあるデータを書き換えられないように、HTML にハードコードしていたスポーツの一覧は、代わりに Python のリスト型を宣言し、そこに値をスポーツ一覧を代入する形で対応する。
from flask import Flask, render_template, request
app = Flask(__name__)
SPORTS = [
"Dodgeball",
"Flag Football",
"Soccer",
"Volleyball",
"Ultimate Frisbee"
]
@app.route("/")
def index():
return render_template("index.html", sports=SPORTS)
...
さらには、Flask が非常に便利でかつ秀才と言われる機能がある。それは、次のプログラムのように、Flask のブロックに Python コードを挿入することができる。
...
<select name="sport">
<option disabled selected value="">Sport</option>
{% for sport in sports %}
<option value="{{ sport }}">{{ sport }}</option>
{% endfor %}
</select>
...
また、Python でも制御する。 not in SPORTS
と記述することで、スポーツリストに入っていないスポーツリストが選択された場合は、failure.html
をレンダリングする。
...
@app.route("/register", methods=["POST"])
def register():
if not request.form.get("name") or request.form.get("sport") not in SPORTS:
return render_template("failure.html")
return render_template("success.html")
{% extends "layout.html" %}
{% block body %}
<h1>Register</h1>
<form action="/register" method="post">
<input autocomplete="off" autofocus name="name" placeholder="Name" type="text">
{% for sport in sports %}
<input name="sport" type="checkbox" value="{{ sport }}"> {{ sport }}
{% endfor %}
<input type="submit" value="Register">
</form>
{% endblock %}
データを保存する
生徒が登録ボタンを押した際に、実際にウェブサーバーにデータを登録できるようにプログラムを修正する。そのためには、まず REGISTRANTS = {}
を用意する。
from flask import Flask, redirect, render_template, request
app = Flask(__name__)
REGISTRANTS = {}
...
@app.route("/register", methods=["POST"])
def register():
name = request.form.get("name")
if not name:
return render_template("error.html", message="Missing name")
sport = request.form.get("sport")
if not sport:
return render_template("error.html", message="Missing sport")
if sport not in SPORTS:
return render_template("error.html", message="Invalid sport")
REGISTRANTS[name] = sport
return redirect("/registrants")
登録できない場合に、error.html
をレンダリングするだけでなく、意味のあるエラーメッセージを返すために、message
変数を error.html
に渡し、表示させる。
{% extends "layout.html" %}
{% block body %}
{{ message }}
{% endblock %}
一方で、登録が正しくできた場合には、registrants.html
をレンダリングする。その際に、REGISTRANTS
リストも渡す。
@app.route("/registrants")
def registrants():
return render_template("registrants.html", registrants=REGISTRANTS)
registrants.html
は仮に次のようにする。
{% extends "layout.html" %}
{% block body %}
<h1>Registrants</h1>
<table>
<thead>
<tr>
<th>Name</th>
<th>Sport</th>
</tr>
</thead>
<tbody>
{% for name in registrants %}
<tr>
<td>{{ name }}</td>
<td>{{ registrants[name] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
これで完成のように見えるが、後もう少し。このままではウェブサーバーを再起動させたらこのデータは消えてしまう。よって、データを保存するために、CSV ファイルに書き出したり、データベースに保存したりすることが必要。SQL データベースに保存する場合、まず db = SQL("sqlite:///froshims.db")
データベースを作る。
from cs50 import SQL
from flask import Flask, redirect, render_template, request
app = Flask(__name__)
db = SQL("sqlite:///froshims.db")
...
そして、登録ボタンが押される度にグローバル変数に登録するのではなく、代わりに INSERT
コマンドを使ってデータベースにデータを保存する。
@app.route("/register", methods=["POST"])
def register():
name = request.form.get("name")
if not name:
return render_template("error.html", message="Missing name")
sport = request.form.get("sport")
if not sport:
return render_template("error.html", message="Missing sport")
if sport not in SPORTS:
return render_template("error.html", message="Invalid sport")
db.execute("INSERT INTO registrants (name, sport) VALUES(?, ?)", name, sport)
return redirect("/registrants")
@app.route("/registrants")
def registrants():
registrants = db.execute("SELECT * FROM registrants")
return render_template("registrants.html", registrants=registrants)
ここで、registrants.html
も多少変えてあげる必要がある。registrant in registrants
、registrant.name
、registrant.spots
<tbody>
{% for registrant in registrants %}
<tr>
<td>{{ registrant.name }}</td>
<td>{{ registrant.sport }}</td>
<td>
<form action="/deregister" method="post">
<input name="id" type="hidden" value="{{ registrant.id }}">
<input type="submit" value="Deregister">
</form>
</td>
</tr>
{% endfor %}
</tbody>
Flask には、メール flask_mail
ライブラリーも用意されている。登録がされた際に、ユーザーにメールを送る際には、次のようにすれば良い。
import os
import re
from flask import Flask, render_template, request
from flask_mail import Mail, Message
app = Flask(__name__)
app.config["MAIL_DEFAULT_SENDER"] = os.getenv("MAIL_DEFAULT_SENDER")
app.config["MAIL_PASSWORD"] = os.getenv("MAIL_PASSWORD")
app.config["MAIL_PORT"] = 587
app.config["MAIL_SERVER"] = "smtp.gmail.com"
app.config["MAIL_USE_TLS"] = True
app.config["MAIL_USERNAME"] = os.getenv("MAIL_USERNAME")
mail = Mail(app)
最後に、register
ルートを修正する。
@app.route("/register", methods=["POST"])
def register():
email = request.form.get("email")
if not email:
return render_template("error.html", message="Missing email")
sport = request.form.get("sport")
if not sport:
return render_template("error.html", message="Missing sport")
if sport not in SPORTS:
return render_template("error.html", message="Invalid sport")
message = Message("You are registered!", recipients=[email])
mail.send(message)
return render_template("success.html")
メールを送るためには、メールアドレスを知っている必要があるので、名前の登録ではなく、メールアドレスの登録に変更する。
<input autocomplete="off" name="email" placeholder="Email" type="email">
これで、ユーザーが登録したメールアドレスにメールが届くようになる。
セッション
セッションは、ウェブサーバーがユーザーの情報を記憶する技術で、ログイン状態を継続したい場合などに使う。ブラウザを閉じて再度開けた際に Gmail にログインしなくても良いように、セッションがあればログインしていた状態をキープしておいてくれる。
HTTP ヘッダーの中にこの情報を入れることが可能で、サーバー側から Set-Cookie
を返す。Cookie(クッキー)とは、ウェブサーバーから返される小さいデータの塊のこと(数字の羅列)で、ブラウザがその情報を保存してくれている。そして、Gmail 等のウェブアプリに何度もアクセスする際には、ブラウザがこのユーザーはログインしていたという情報を渡していて、これによってユーザーは何度もログインし直さなくても良い。Facebook や Google などの広告はこれを利用していて、一度訪れたサイトの広告を他のサイトでも表示することができている。
HTTP/1.1 200 OK
Content-Type: text/html
Set-Cookie: session=value
...
では、どう実装するのか?一度ログインしたユーザーをずっとログインしたままにする方法を紹介する。Flask では、flask_session
ライブラリーが用意されている。それを使う。
まずは、サイトに訪れたユーザーがログインされていない状態だったら、/login
に誘導し、そうでなければ、index.html
に誘導する。ここで、ログインされているか状態かどうかは、Flask で用意されているsession
ライブラリーを使い、session["name"]
を使って確認する。セッションを終了するために、ログアウトする機能を /logout
ルートに用意することを忘れずに。
from flask import Flask, redirect, render_template, request, session
from flask_session import Session
app = Flask(__name__)
app.config["SESSION_PERMANENT"] = False
app.config["SESSION_TYPE"] = "filesystem"
Session(app)
@app.route("/")
def index():
if not session.get("name"):
return redirect("/login")
return render_template("index.html")
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
session["name"] = request.form.get("name")
return redirect("/")
return render_template("login.html")
@app.route("/logout")
def logout():
session["name"] = None
return redirect("/")
- デフォルト
/
ルートでは、session
に誰の名前も入っていなければ/login
へリダイレクトし、それ以外の場合(つまり、誰かの名前が既に入っている)は、index.html
テンプレートへリダイレクトする。 /login
ルートでは、POSTメソッドを使って、session
にname
を代入し後、デフォルト/
ルートにリダイレクトする。もし、GETメソッドでアクセスされた場合は、login.html
を返す。/logout
ルートでは,session
に代入されているname
の値をクリアして、/
にリダイレクトする.- 最後に、Flaskのルールの一つである
requirements.txt
に使用するライブラリーの追加が必要。
ログイン画面(login.html
)では、名前を入れるシンプルなものを用意(パスワードは後の例で紹介)。
{% extends "layout.html" %}
{% block body %}
<form action="/login" method="post">
<input autocomplete="off" autofocus name="name" placeholder="Name" type="text">
<input type="submit" value="Log In">
</form>
{% endblock %}
デフォルトページ (index.html
) では、session.name
が存在するかを確認し、存在する場合はログインをしている画面を、存在しない場合は、ログインを促す画面の表示を動的なプログラムを実装することが可能。
{% extends "layout.html" %}
{% block body %}
{% if session.name %}
You are logged in as {{ session.name }}. <a href="/logout">Log out</a>.
{% else %}
You are not logged in. <a href="/login">Log in</a>.
{% endif %}
{% endblock %}
Chrome のディベロッパーツールズでヘッダーファイルを確認すると、Cookie
に値を代入されていることが分かる。

オンラインストア
最後の章では、e-Commerce など、オンラインストアの作り方の紹介。まずは予め用意されたプログラムを見てみる。
application.py
の中では、まず初期化、それからデータベースとセッションを使うための設定を行う。続いて、デフォルト /
ルートでは、index()
で本の一覧を表示するページへレンダリングする。
from cs50 import SQL
from flask import Flask, redirect, render_template, request, session
from flask_session import Session
# Configure app
app = Flask(__name__)
# Connect to database
db = SQL("sqlite:///store.db")
# Configure sessions
app.config["SESSION_PERMANENT"] = False
app.config["SESSION_TYPE"] = "filesystem"
Session(app)
@app.route("/")
def index():
books = db.execute("SELECT * FROM books")
return render_template("books.html", books=books)
@app.route("/cart", methods=["GET", "POST"])
def cart():
# Ensure cart exists
if "cart" not in session:
session["cart"] = []
# POST
if request.method == "POST":
id = request.form.get("id")
if id:
session["cart"].append(id)
return redirect("/cart")
# GET
books = db.execute("SELECT * FROM books WHERE id IN (?)", session["cart"])
return render_template("cart.html", books=books)
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="initial-scale=1, width=device-width"/>
<title>store</title>
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
では、本の一覧を表示させるだけでなく、そのそれぞれに対して “Add to Cart” (買い物カゴに入れる)ボタンが実装されている。
templates/books.html
{% extends "layout.html" %}
{% block body %}
<h1>Books</h1>
{% for book in books %}
<h2>{{ book.title }}</h2>
<form action="/cart" method="post">
<input name="id" type="hidden" value="{{ book.id }}">
<input type="submit" value="Add to Cart">
</form>
{% endfor %}
{% endblock %}
その「買い物カゴへ追加」ボタンを押せば、 /cart
ルートに引き渡され、POSTリクエストを使って本の id
がセッションの変数として入る。一方で、GETメソッドを使った場合は、session
に入っている本の一覧(id
)が表示される。
{% extends "layout.html" %}
{% block body %}
<h1>Cart</h1>
<ol>
{% for book in books %}
<li>{{ book.title }}</li>
{% endfor %}
</ol>
{% endblock %}
ショッピングカートは、こうして実装できる。
次の例では、好きなテレビ番組のキーワード検索をしたら、データベースに保存されている一覧の中から対応する検索結果を返してくれるというプログラムを見てみる。
まず application.py
では、shows.db
を開く。デフォルト /
ルートでは、検索キーワードを入力できるフォームを用意。そこから更に /search
でデータベース上の一致する値を返し、search.html
へとレンダリングする。
ここで、更に Google 検索のようにユーザーインターフェースを良くすることを試みる。
from cs50 import SQL
from flask import Flask, render_template, request
app = Flask(__name__)
db = SQL("sqlite:///shows.db")
@app.route("/")
def index():
return render_template("index.html")
@app.route("/search")
def search():
shows = db.execute("SELECT * FROM shows WHERE title LIKE ?", "%" + request.args.get("q") + "%")
return render_template("search.html", shows=shows)
検索キーワードを一文字ずつ入力する都度、検索結果を表示させるようにできる。これには、JavaScript を使えば実装できる。jsonify
関数を使って、JSON フォーマットで値を返す。JSON とは、JavaScript Object Notation の略で、ウェブブラウザとウェブサーバー間でデータをやり取りするためのフォーマットである。
@app.route("/search")
def search():
shows = db.execute("SELECT * FROM shows WHERE title LIKE ?", "%" + request.args.get("q") + "%")
return jsonify(shows)

これが JSON フォーマット。このフォーマットを index.html
へ渡すことで DOM を書き換えることができる。
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="initial-scale=1, width=device-width">
<title>shows</title>
</head>
<body>
<input autocomplete="off" autofocus placeholder="Query" type="search">
<ul></ul>
<script crossorigin="anonymous" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script>
let input = document.querySelector('input');
input.addEventListener('keyup', function() {
$.get('/search?q=' + input.value, function(shows) {
let html = '';
for (let id in shows)
{
let title = shows[id].title;
html += '<li>' + title + '</li>';
}
document.querySelector('ul').innerHTML = html;
});
});
</script>
</body>
</html>
リクエストを簡単に記述するために、JQuery ライブラリーも使用する。input
要素に何か変化がないかをずっと聞いていて、$.get
を使ってJQuery ライブラリーを呼び出し、input
の値をGET メソッドを使って検索する。結果は shows
に返され、<li>
で繰り返し表示される。
$.get
は、AJAX コールと呼ばれるもので、ページがロードされた後に追加で何かを要求できる機能で、これで一文字ずつ入力した際に違う検索結果を返してくれることが見て取れる(Chrome ディベロッパーツールズ)。

以上、盛りだくさんでした。
未希 諒
Related posts
About media
Ramps Blog は、ノマドによるノマドのためのブログです。日本、香港、シンガポール、タイ、台湾のノマド生活を通じて見えた景色、特に食、カルチャー、テクノロジー、ビジネス、アート、デザインなどについて発信しています。
Recent Posts
Twitter feed is not available at the moment.