CS50 Week9 Flask ハーバード大学コンピュータサイエンス講座
CS50 Week9 Flask
前回のHTML、CSS、JavaScriptに加えて、Flaskというフレームワーク、これまで学んできたSQL、pythonも駆使して「会員登録できるウェブサイト」も作れそうな勢い。
ワクワクするけど、やっぱ終盤になってから情報過多で頭飽和状態だなあ…ちょっとこの辺で一旦整理した方が良いのかも…
------------------------------------------------------------------------------------
さて、CS50の9週目です。9週目は、過去数週間分の学習内容とHTML、CSS、JavaScript、SQL、pythonなどの言語を総合的に学習する週になります。
今週の目標は、これらの教材を全てまとめて、おおまかに言うとウェブプログラミングというトピックにすることです。
確かに、先週はJavaScriptを少し紹介しましたし、実際にJavaScriptを使ってプログラミングもしましたが、完全にクライアントサイドでした。
今週はサーバサイドのコンポーネントを再び導入し、いわゆるクライアントであるブラウザと、ウェブアプリケーションのバックエンドであるウェブサーバを結び付けて終わろうと思います。
そのために、私たちの出発点を考えてみましょう。
先週は、静的なウェブページを提供するために、http-serverというシンプルなプログラムを使いました。また、ユーザーからの入力を処理するために、多少のJavaScriptコードを追加しました。
このコマンドに相当するものは、MacやWindows、Linuxなど他のプラットフォームにもありますが、今回の目的のためには、http-serverというプログラムは、文字通りウェブサーバを実行するだけです。
ウェブサーバとは、TCPの80番ポートまたは443番ポートで常に接続を待っているソフトウェアのことです。
ブラウザが接続すると、アクセスしたURLと、そのURL経由で提供されたパラメータ、あるいはもっと一般的にはフォーム経由で提供されたパラメータを調べて、ウェブページを提供します。オプションで何らかの出力も可能です。
しかし、先週扱ったものは全て静的でした。
HTMLファイル、CSSファイル、そしていくつかのJavaScriptファイルだけです。
しかし、ブラウザがウェブサーバに送信するリクエストには、このようなHTTPヘッダが含まれていました。
GET / HTTP/1.1
...
HTTPサーバのプログラムが行っていたこと、またウェブサーバが行っていたことは、仮想的な封筒の内容を上から下、左から右へと分析し、ブラウザや友人、家族が何を要求しようとしているのかを理解することでした。
HTTPヘッダの1行目が単に「GET /」だった場合、それは「デフォルトのウェブページをくれ」という意味です。
慣習的には、それはindex.htmlと呼ばれるファイルを提供してくれという意味です。
ブラウザのURLバーにindex.htmlと文字通り入力すれば、より明確にサーバにそのファイルを要求することができるでしょう。
GET / index.html HTTP/1.1
...
私たちはgoogle.com、特にその検索機能でも遊びました。
手動で次のようなURLを作成し、実際のHTMLフォームでそれを再現しました。
GET /search?q=cats HTTP/1.1
これを送信すると、https://www.google.com/search?q=catsという形式のURLが得られました。
先週のhttp-serverはかなり単純なものでした。
静的なコンテンツしか提供しなかったため、これらのURLパラメータをほとんど無視していました。
しかし、実際にバックエンドのウェブサーバ、google.comのようなウェブアプリケーションを書こうと思ったら、この仮想的な封筒の内容を分析して、ユーザーがどのような経路、いわばルートを望んでいるかを把握する必要があります。
/searchは、このルートにあたります。
そして、正確なHTTPパラメータを把握しなければなりません。
この場合のqの値は"cats"です。
どこかの誰かが、この文字列を解析するコードを書かなければなりません。
/searchが要求されていること、?は無視し、qという変数を宣言して、その値を"cats"にすることなどを読み取らなければなりません。
どこかに、それを実行するコードが必要です。
今日は、そのコードを紹介します。
今回紹介するのはFlaskというライブラリです。
技術的にはFlaskはフレームワークとも呼ばれています。
フレームワークとは、物事を行うための方法です。
コードを整理する方法であり、コードを書く方法であり、より具体的には、このライブラリをどのように使うべきかを示しています。
さて、なぜFlaskが存在するのでしょうか?
代替品もあります。
Flaskが存在するのは、私たちがすぐに退屈になってしまうような多くのタスクを単純化するためです。
もし皆さんが新しいウェブサイトやモバイルアプリケーションを実装しようとしたときに、ユーザーの入力内容を把握するために毎回テキストを分析するコードを書かなければならないとしたら、どれだけイライラするか想像してみてください。
文字列や整数を取得する関数を呼び出すだけで、他の誰かが書いた関数がテキストを見て、パラメータとその値を把握し、使いやすい変数にして渡してくれるとしたら、良いと思いませんか?
これが、Flaskがやってくれることの1つです。
ユーザーの入力内容を分析し、/searchや/index.htmlなど、ユーザーが求めるルートを把握し、プログラマである皆さんがより簡単に利用できるようにしてくれます。
Flaskは、今日やるように、Flaskベースのウェブアプリケーションを作成する際には、通常、このようにファイルやフォルダを整理するという意味で、フレームワークです。
application.py
requirements.txt
static/
templates/
これが、私がフレームワークと言っている意味です。
フレームワークは単なるライブラリではなく、呼び出すことのできる関数を持っています。
フレームワークには通常、「これらの関数を使用するために、更にファイルやフォルダをこのように整理する必要がある」、というドキュメントがあります。
最もシンプルなFlaskアプリケーションには、以下のようなファイルが含まれるでしょう。
application.pyは、コードを書く際にほとんどの労力を費やす場所で、この場合はpythonで書かれています。
requirements.txtは、アプリケーションで使用したい他のライブラリを上から下まで1行ごとに列挙したシンプルなテキストファイルです。
static/は、文字通り静的なファイルを格納するフォルダで、GIF、JPEG、PNG、CSSファイル、JavaScriptファイルなど、先週書いたファイルは全てこのstatic/フォルダに格納されます。
そして最後にtemplatesです。templatesにはHTMLの多くが入ります。
先週との違いを見てみましょう。
要するに、単なるウェブサイトではなく、ユーザーの入力を受け、ユーザーへの出力を行い、データベースと通信し、メールを送信し、その他多くのことを行うウェブアプリケーションを作りたい場合、ここではFlaskというフレームワークを使います。
他にも選択肢はあります。
pythonの世界ではDjangoというフレームワークがありますし、PHPの世界ではSymfonyやLaravelなどのフレームワークがあります。
JavaやC++などの言語にも同様のフレームワークがあります。
これは、世の中に存在するフレームワークの代表的なものです。
しかし、実際にコードを書く前に、これらのフレームワークが特定のデザインパターンを実装する傾向があることを知っておくと役に立つでしょう。
デザインパターンとは、人間がコードを書く方法を説明するための手段です。
↓ ← MODEL ← ←
↓ ↑
UPDATES MANIPULATES
↓ ↑
VIEW CONTROLLER
(HTML、CSS…) (python、JavaScript…)
↓ ↑
SEES USES
→ → USER → ↑
過去何十年にもわたって、最初は独立して仕事をしていた多くの人々は、同じ問題を何度も何度も解決してきました。
そして彼らは、別々なプロジェクトの問題解決方法に共通のパターンがあることに気づきました。
そして、プログラマは、これらのパターンに気づくと、本を書いたり、ブログの記事を書いたりして、そのパターンを公式化し、デザインパターンという名前を付けて、他の人に採用するように勧めました。
なぜでしょうか?
それは、全てのコードを1つの巨大なファイルにまとめるよりも、そうした方がコードを整理するのに役立つからです。
例えば、コードの置き場所を種類ごとに決めておけば、他の人とより効果的に共同作業ができます。また、より大きなプロジェクトを維持する際に正気を保つことができます。
そこでFlaskは、一般的にMVCデザインパターン、もしくはパラダイムとして知られているものを実装しています。
MVCとは、Model、View、Controllerの頭文字を取ったものです。
これらは技術用語のように聞こえますが、今日の最後には、比較的簡単なものであることがわかるでしょう。
Controllerは、pythonコードのほとんどを書く場所になります。
これはウェブアプリケーションを制御するファイルです。
これまでは、Cやpythonのプログラムを書くときは、実質的にControllerコードを書いていました。
ただ、そのラベルを貼ることはありませんでした。
また、MVCのVであるViewと呼ばれるものもあります。
Viewとは、人間が見る全てのもの、HTML、CSS、より一般的にはユーザーインターフェースを指します。
ユーザーに関わるものは全て、アプリケーションのViewの一部として説明されます。
しかし、プログラマである皆さんが書くコードはいわばcontrollerの一部なのです。
そして最後に、MVCのM、いわゆるModelです。
これは一般的に、データにどのような技術、サービス、ソフトウェアを使用しているかを示しています。
それはSQLデータベースかもしれないし、CSVファイルかもしれません。
Modelとは、アプリケーションが使用しているデータのことです。
繰り返しになりますが、これらの用語はいずれも、先週や先々週にはなかった新しい概念を導入するものではありません。
MVCを頭文字やデザインパターンとして導入することでFlaskや、ひいては多くのプログラマがpythonや他の言語を使ってウェブベースのアプリケーションを設計する際に使っているアプローチにラベルを貼ることになります。
他にも選択肢はありますが、これは最も人気があり、恐らく最もシンプルなものです。
他の言語でやったのと同じように、Flaskというライブラリ、もしくはフレームワークを使って書く事ができる最もシンプルなウェブアプリケーションを見てみましょう。
application.pyというファイルの必要な場所を以下のコードで埋めれば、最初のウェブアプリケーションが出来上がります。
今から見るように、これはあまり面白いことはできませんが、ウェブアプリケーションを書くために必要な最小限のコードです。
このプログラムを開始すると、それぞれのブラウザから送られてくるポート番号80または443のTCPリクエストを、ひたすら受信し続けることになります。
そして、ユーザーに応答するコードを書く事ができるようになります。
これは何を意味しているのでしょうか?
それでは、実際にプログラムを書いてみましょう。
ここでは、これまで使ってきたCS50 IDEに切り替えます。
もうhttp-serverは使いません。代わりにFlaskを使います。
最初に、とてもシンプルなファイルを作りましょう。
まずディレクトリを作ります。
helloという名前のディレクトリを作り、cdでアクセスします。
いつものようにファイルブラウザを開くと左上に同じものが表示されますが、今回はコマンドラインに注目します。
helloディレクトリに入ったところで、ファイルを作ってみましょう。
仮にapplication.pyと名付けて、helloディレクトリに保存します。
lsを入力してみるとファイルが確認できますが、現在は空の状態です。
それでは、先ほどのコードの一部をコピーしてみましょう。
flaskライブラリからFlaskと呼ばれるものをインポートしてみましょう。
FlaskのFは慣習上大文字です。
from flask import Flask
このファイルを自分のサーバとして本格的なFlaskアプリケーションにするために、慣習的にappという変数を定義することにします。
Flask関数を呼び出して__name__を渡す、という、ちょっと変なことをします。
app = Flask(name)
この特別な変数は過去に一度しか見たことがありません。
pythonファイルの一番下で、if name == "main":main()として、メイン関数を呼び出すべきだ、と言ったときです。
それとは関係ありませんが、この特別な変数__name__は、基本的に現在のファイルの名前を指します。
つまりこれは「Flaskよ、現在のファイルをアプリケーションにしてくれ。つまり、ブラウザのリクエストを受け付けるウェブアプリケーションにしてくれ」というコードの一行なのです。
さて、このファイルの中で、私はFlaskにルートを教えなければなりません。
ルートとは単にURLのことです。
/index.html、/searchなど、今日のウェブサイトのURLに見られるようなパスを指します。
Flaskでこれを行うには、@app.route("/")とします。
さて、これは今までpythonで見たことがないものですが、実はこれはpythonの機能です。
このように関数の先頭に@記号がある場合、それはpythonデコレータと呼ばれるものです。
今日の目的のために必要なのは、これはある関数を別の関数に適用するための特別な方法であるということだけです。
しかし、今のところそれは置いておいて、Flaskを使用する際の慣習として、最初に/のようなルートを定義し、次に関数を定義します。
この関数は好きなように呼ぶこともできますが、慣例的には問題となっている実際のルートに適したものを呼ぶべきでしょう。
def index():
繰り返しになりますが、人間はデフォルトのルートをindexと呼ぶ傾向があります。
そこで、この関数をindexと呼ぶことにします。
そしてその下で、「hello,world」として、文字通りその文字列を返してみましょう。
return "hello,world"
この後、何が起こるのか見てみましょう。
▼hello/application.py
from flask import Flask
app = Flask(name)
@app.route("/")
def index():
return "hello,world"
ターミナルウィンドウに戻ってhelleディレクトリでlsをタイプすると、application.pyというファイルが1つだけあります。
これはFlaskアプリケーションなので、サーバを起動するためにhttp-serverを使わずに「flask run」を実行したいと思います。
flask run
Flaskはライブラリです。
しかし、MacやPC、IDEにインストールすると、Flaskアプリケーションを起動するためのプログラムも付属しています。
(flask runを実行すると、ターミナルウィンドウに何行かのコードが表示される)
ここではいくつかの難解な出力が表示されます。
hello/ $ flask run
Serving Flask app 'application.py'
Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment....
ただ、http-serverのように、現在起動しているアプリケーションのURLも表示されます。
このURLをクリックして、別のタブで開くと、初めてのダイナミックなウェブアプリケーションが出来上がっています。
(URLを開くと「hello,world」と表示されたウェブページに繋がる)
「hello,world」と表示されているだけです。
実際、chromeのページソースを表示する機能を使ってみると、本格的なHTMLですらないことがわかります。
文字通りのテキストですが、それはまだ実際のHTMLを返すようにしていなかったからです。
それでは、単に任意の文字列を返すのではなく、もっときちんとした方法でやってみましょう。
サーバを停止し、こちらに集中しましょう。
デフォルトでは、私が設定した/というルートは、index.htmlというファイルを返すことを意味しています。
今のところ、それが存在すると仮定しましょう。
どうやってそれを返せばいいのでしょうか?
技術的にはindex.htmlというテンプレートをレンダリングするということになります。
return render_template("index.html")
この関数render_templateを使用するためには、Flaskライブラリからインポートする必要があります。
from flask import Flask, render_template
Flaskはライブラリであり、多くの関数が付属しています。
最初のものはFlask自身と呼ばれるものです。
これがFlaskをウェブアプリケーションとして起動させます。
render_templateは、index.htmlというファイルを探して、その内容を取得し、それを返すことを目的として関数です。
数週間前にpythonでopenやreadを使ったのと精神的には似ていますね。
しかし、これは他の素晴らしい機能も提供してくれます。
コマンドラインでlsと入力して、application.pyしかないことを確認します。
同じディレクトリにindex.htmlを作りたくはありません。
index.htmlと今日以降のすべてのHTMLファイルをtemplatesというフォルダに置くべきだという、公式の推奨事項があります。
では、そうしましょう。
mkdirでtemplatesというフォルダを作ります。
次に、index.htmlという実際のファイルを作ってみましょう。
これをtemplatesディレクトリに格納します。
そして、おなじみのことをやってみましょう。
もう何度もやっていることですが、ここで簡単なHTMLページを作成します。
▼index.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>hello</title>
</head>
<body>
hello,world
</body>
</html>
先週からまだ何も変化はありません。
しかし今、application.pyに戻って、render_template("index.html")という関数呼び出しを返したことに注意してください。
このrender_templateという関数は、ファイルを開いて、その中の全てのバイトを取得し、最終的にこの行を介してそれを返します。
では、もう一度Flaskを起動して、IDEのURLをクリックしてみましょうか。結果は同じです。
しかし、chromeのソース表示機能で開いてみると、今度は本格的なウェブページを返していることに気付きます。
(ページ上で右クリック→「ページのソース表示」Ctrl+U)
それほど面白いものではありませんが、好きなHTMLを生成するための小さな一歩を踏み出したことになります。
では、これをもっと面白くしてみましょう。
私はずっと、ウェブプログラミングでは、URLを介してユーザーの入力を受け付ける方法があると主張してきました。
では、どうやってそれを行うのでしょうか?
さて、ここで提案したいことがあります。
次のようなコードを追加で書いてみましょう。
render_templateの後、0個以上の名前付き引数を渡すことができます。
これらの引数の名前は自由ですが、そのような例はこれまでに見たことがありませんでした。
これまでは、他の人の関数を使うときには、ドキュメントや講義ノートをチェックして、「この関数に渡せる引数は何か」を把握しなければなりませんでした。
しかし、render_templateではそんなことはありません。
もう少し強力です。
例えば、ユーザーの名前をapplication.pyファイルから渡したい場合、controllerを呼び出す際に「nameという変数をください」と言えます。
return render_template("index.html", name=
この人の名前の値は何にすればいいでしょうか?
実際には?クエスチョンマークの後にあるものと同等の値にしたいですよね?
GET /search?q=cats HTTP/1.1
基本的に、URLからユーザーの入力を得る唯一の方法は、?クエスチョンマークの後に指定することだということを見てきました。
qやcatsではなく、name=Davidやname=Brianにしたいのです。
では、どうすればいいのでしょうか?
name=request.args.get("name")としてみましょう。
return render_template("index.html", name=request.args.get("name"))
そして、このファイルのトップにrequestを加えます。
from flask import Flask, render_template,request
何をしているかというと、Flaskライブラリからrequest変数をインポートすると、HTTPリクエストにアクセスできるようになり、URLにあるかもしれないパラメータにもアクセスできるようになります。
Flaskがしてくれることは、そのURLを解析することです。
GET /search?q=cats HTTP/1.1
何がqで、何がcatsで、何がnameで、何がDavidなのかを把握します。
?クエスチョンマークの後にあるものは何でも、Flaskが解析して変数として返してくれます。
そして、request.args.getにより、URLから取得したいパラメータの名前を呼ぶことで、これらの変数にアクセスできます。
さて、これを保存して、index.htmlに戻ります。
今日以降、このファイルがtemplateテンプレートと呼ばれる理由はここにあります。
テンプレートとは、人間の世界と同じように、他の値を差し込むことができるフレームワークのようなものです。
自分の仕事のベースとなるテンプレートです。
建物を建てるときにベースとなる青写真のようなものです。
そのため、テンプレートには通常、ある値を差し込むための特別な構文があります。
この構文は今まで見てきたものと比べると目新しいかもしれませんが、2つの{}中括弧を使えば、Flaskのrender_template関数に、関数呼び出しの引数の1つとして渡した変数の値をそこに差し込むように指示することができます。
hello,{{name}}
それでは再びflask runを実行してページを表示してみましょう。
(「hello,None」と表示される)
かなり馬鹿げた感じになっています。
しかし、pythonではNoneは特別な値であることを思い出してください。
それは何かが値を持たないことを意味します。
では、どうすればいいかわかりますか?
ここのURLバーを拡大して見てみましょう。
URLの末尾に/?を追加して、q=catsではなく、name=Davidとしてはどうでしょう?
私が追加したのは/?name=Davidだけです。
Enterキーを押すと、ほら、hello,Davidというウェブページが出来上がりました。
このページのソースを見ると、HTMLが動的に生成されているのがわかります。
<!DOCTYPE html>
<html lang="en">
<head>
<title>hello</title>
</head>
<body>
hello,David ←{{name}}のところにDavidと入っている
</body>
</html>
私のIDEにはhello,Davidと書かれたファイルはありません。これは動的に差し込まれたものなのです。
これは変更可能です。
nameをBrianに帰ると、ページも変わります。
ページソースを見ると、まるでindex.htmlに元からBrianという名前が書かれていたかのように変わっています。
しかし、実際にはそうではなく、テンプレートの中にはプレースホルダがあるだけなのです。
そして、これをクリーンアップすることができます。
nameに値が無いと馬鹿げているように見えますが、request.args.get関数は2番目のオプション引数を取ることができます。
値があるかどうかわからず、デフォルトの引数を与えたい場合、get関数の2番目の引数をデフォルトの値に設定することができます。
return render_template("index.html", name=request.args.get("name","world"))
それではFlaskを再実行してみましょう。
(nameを入力しなくてもhello,worldがデフォルトで表示される)
hello,worldというデフォルト値が表示されていますが、URLバーに名前を入れ直すと、今度はデフォルト値が不要になります。
(URLの末尾に/?name=Davidと入力するとhello,Davidと表示される)
人間が実際に入力したものが表示されます。
ここで何が起こっているかというと、皆さんがcatsと入力してEnterを押した時にgoogle.comが何をしているかを考えてみましょう。
q=catsという単語がURLでgoogle.comに渡され、Googleは何らかのプログラムを実行しています。
Flaskを使ったpythonか、あるいは他の言語かもしれませんが、URLを分析してq=catsを取得し、基本的にcatsというキーワードでデータベースを検索し、catsの写真や検索結果を表示するHTMLを動的に生成しています。
Googleのサーバには常に猫のリストを表示しているウェブページやHTMLファイルがあるわけではありません。
imgタグと猫で埋め尽くされた巨大なHTMLファイルを、人間が一日中管理しているわけでもありません。
言うまでもなく、これらはすべて動的に生成されます。
些細な例ではありますが、私たちはまさにその能力の表面をなぞっているのです。
さて、ここで一旦止まって、質問や不明点がないか確認しましょう。
というのも、どんなフレームワークでも、まずは慣習を学び、それから生産性を高める必要があるからです。
ブライアン「render_template関数を呼び出したとき、なぜtemplates/index.htmlとせずにindex.htmlにしたのでしょうか?という質問がありました。」
↑良い質問ですね。
これには「そう決まっているから」と答えるしかありません。
render_template関数は、ファイルがtemplatesディレクトリにある前提で実装されています。
技術的には、アプリケーションを再設定することでこれを無効にすることができますが、デフォルトではこれらのファイルをtemplatesに置くことが慣習となっています。
サンティアゴ「なぜアプリケーションを宣言するときに、@app.routeの引数として"/"を入れなければならないのですか?」
↑ええ。なぜ/を入れなければならないのでしょうか?
それはFlaskに「このルートには次の関数を使ってください」と伝えるための方法なのです。
/は、恐らく最もシンプルで、最も基本的なルートを定義することができます。
それは他の言葉が無い状態を表します。
そこで、/を置く必要があります。
そうしないと、皆さんがどこかの.com/にアクセスした場合、サーバは何をすべきかわからないからです。
この表示は変更することができます。何にでも変更することができます。
ウェブサイトに秘密のURLを作るようなものですが、サーバを停止して、これを"/secret"に変更してみましょう。
@app.route("/secret")
Flaskを再実行してみましょう。
(Not Foundと表示される)
何も起こりません。
URLにアクセスすると、Not found,404になります。
URLバーだと隠れてしまっていますが、実際には末尾に/が存在しています。
しかし、これを変更して「/secret」にすると、そのページが再び表示されます。
(URLの末尾に/secretを入力すると、元のページが表示される)
このように、@app.route関数では、それに続く関数に関連するルートを定義することができます。
ソフィア「モデルは、今はHTMLファイルの一部ですか?名前のようにデータとして、HTMLファイルでエンコードされているのでしょうか?」
↑ええ。現時点ではまだモデルと呼べるものはありません。
データベースもCSVファイルも無いので、今はただC(controller)とV(view)で遊んでいるだけです。
今日これからやるように、本格的なアプリケーションができて初めて、本当の意味でのM(Model)が出てきます。
しかし、合理的な人たちは反対するかもしれませんが、これらは単なる慣習であり、厳密なルールではありません。
私のウェブサイトにアクセスするときに、URLバーに自分の名前を入力すべきことを知っていなければならない、というのは、ちょっと馬鹿げていますよね?
誰もそんなことはしません。
その代わりに、人間はフォームを入力しています。
そこで、もう一歩踏み込んで、フォームを用意するよう改善してみましょう。
それでは別のファイルを作成してみましょう。
templatesディレクトリに入ると、現在はindex.htmlしかありませんが、このindex.htmlというファイルをgreet.htmlという別のファイルにコピーしてみましょう。
cp index.html greet.html
エディタでgreet.htmlを開いてみます。
greet.htmlは最終的にはindex.htmlの動作を代わりに行うことになります。そして、index.htmlは実際のフォームを持つように変更するつもりです。
index.htmlのhello,{{name}}を削除し、代わりに<form action="/greet"とします。
これはまだ存在しませんが、すぐに作ります。
<form action=""/greet" method="get">
最初はgetを使います。
そして、このフォームの中で人間の名前が必要なので、名前が文字通りnameになるように設定してみましょう。
<input name="name"
そして、このフィールドのtypeをtextとします。
<input name="name" type="text">
そして、もう1つのinputのtypeをsubmitにして、submitボタンを設置します。
値はデフォルトの通りとします。
<input type="submit">
▼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>
それではhelloディレクトリのapplication.pyに戻ってみましょう。
flask runをtemplatesディレクトリで実行しないようにしてください。
必ず、application.pyファイルのあるディレクトリでのみ実行してください。
このファイルを実行してみましょう。
ファイルを開いてみましょう。
(入力フォームと送信ボタンが表示される)
私の/ルート、デフォルトのルートはこのHTMLフォームに変更されています。
ソースを見ると、私が入力した内容がそのまま表示されています。
つまり、このコードに動的な要素は何もありません。
確かにハードコーディングされています。
そこにはプレースホルダもありません。
しかし、このフォームは事前に私が設計したもので、getを使って/greetルートで挨拶するようになっています。
<form action="/greet" method="get">
では、どうやってそれを実装するのでしょうか?
実際に自分のアプリケーションに入ってみましょう。
私の名前Davidを入力して送信ボタンを押してもNot Foundになってしまいます。
なぜなら、この/greetは存在しないからです。
/greet?name=David
これは私のアプリケーションで定義されたルートではありません。
では、私のサーバ(application.py)に戻って、下の方に行って、同じようなことをやってみましょう。
@app.route("/greet")
では、2つ目のルートを作ってみましょう。
ユーザーが/greetにアクセスしたときに呼び出されるべき関数を定義してみましょう。
何と呼んでも構いませんが、ここではシンプルにルートと同じ名前にしておきましょう。
def greet:
greet関数では何をしたいのでしょうか?
"TODO"を返すようにします。
return "TODO"
まだ実装していませんが。
しかし、Cやpythonの時と同じように、変更は少しずつ行い、コードの基本が機能していることを確認してください。
flask runを再実行してみましょう。
そしてフォームに再びDavidと入力してみましょう。
(TODOと表示される)
ほら。これでgreetルートが存在するようになりました。
URLバーを見ると、/greetが機能していることに注目してください。
もうNot Foundではありませんが、もちろんまだ何の役にも立っていないので、TODOとなっています。
また戻って、サーバを停止してみましょう。
そして代わりに、TODOがgreet.htmlというテンプレートをレンダリングするようにしてみましょう。
def greet():
return render_template("greet.html"
しかし、greet.htmlは私が始めた場所であることを思い出してください。
今のファイルには{{name}}というプレースホルダがあります。
そこで、先ほどindexに使っていたコードをそのまま移動させれば良いと思います。
return render_template("index.html")
return render_template("greet.html", name=request.args.get("name","world"))
index.htmlはもはやハードコードされた静的なフォームしか持っていませんから、nameパラメータをgreetルートに移動させましょう。
サーバを再起動します。
再びフォームにDavidと入力してみましょう。
(helle,Davidと表示される)
これで、/と/greetという2つの完全に機能するルートができました。
1つ目のルートは静的なフォームを表示するだけですが、2つ目のルートはもっと面白いことをしてユーザーを迎えます。
先ほど述べたように、私はこのユーザーインターフェースにちょっとした不満を感じていました。
以前に名前を入力した人を記憶しているという事実に、私は不快感を覚えていました。
気になったのはプライバシーが漏れてしまうことです。
そこで、index.htmlにアクセスして、オートコンプリートをオフにしてみましょう。
また、オートフォーカスを実行して、テキストフィールドにデフォルトで点滅するカーソルを与えることもできます。
<input autocomplete="off" autofocus placeholder="Name" name="name" type="text">
変更を行ったあと、サーバを再起動してみましょう。
フォームには予めNameと表示され、オートコンプリートはオフになり、オートフォーカスになっています。
これでシンプルながらも動作するウェブアプリケーションが完成しました。
さて、ここで何を追加したのでしょうか?
先ほどと同じアプリケーションですが、2つ目のルートと2つ目のテンプレートを追加して、1つのフォームがもう1つのルートにデータを送信できるようにしました。
ここで一旦立ち止まって質問や不明点がないかどうか確認してから、この作業を繰り返していきましょう。
オーディエンス「""で囲まれたnameは後で指定したnameと同じなのでしょうか?」
↑そうです。それを今から変更して実証してみたいと思います。
ここでは人間の名前について話しているのでnameとしましたが、これは好きなように呼べます。
例えば、私のフォームではこれを人のフルネームとして集めています。
しかし、ファーストネームだけを知りたい場合には、例えばfirst_nameに変更することもできます。
プレースホルダも変更して、ファーストネームだけが必要であることを明確にしましょう。
<input autocomplete="off" autofocus placeholder="First Name" name="first_name" type="text">
それではapplication.pyにアクセスしてみましょう。
HTTPパラメータのfirst_nameを取得し、
return render_template("greet.html", name=request.args.get("first_name","world"))
テンプレートのgreet.htmlで、これをfirst_nameに変更してみましょう。
hello,{{first_name}}
実はもう1つ変更する必要があります。
(requestの前のnameもfirst_nameに変更)
return render_template("greet.html", first_name=request.args.get("first_name","world"))
このように、あちこちで同じことを繰り返さなければならないのは少し面倒ですが、これらは異なる意味を持っています。
application.pyの文脈では、これはテンプレートに渡すパラメータです。
このget関数のコンテキストでは、URLから取得するHTTPパラメータです。
そして、テンプレートのコンテキストでは、これは、これらのうちの最初のものを参照していて、それはrender_template関数に渡す引数です。
サーバを再起動してみましょう。
(同じように動作する)
出力は変わっていませんが、機能の面でたくさんの変化がありました。
URLバーを見ると、nameではなくfirst_nameを使っているのがわかります。
ルートやURLのパラメータ、テンプレートなどについて、他に質問や不明点はありますか?
ありませんか?
それでは皆さんに質問してみましょう。
これを少し下にスクロールしてスペースを確保します。
ここにindex.htmlファイルがあります。
フォームが入っています。
これはgreet.htmlで、hello,{{first_name}}が入っているだけです。
これのどこがデザイン的に良くないのでしょうか?
first_nameをnameに戻して、少しでも画面に収まるようにします。
内容は間違っていません。
ウェブアプリケーションとしては動作しているようです。
しかし、どこがデザイン的に悪いのでしょうか?
ピーター「URLは入力者の個人情報を公開してしまっているので、それを隠すためにpostを使いたいということでしょうか?」
↑よくわかりましたね。
今までずっと、非常に意図的に、少し心配ではありますが、URLの情報を漏らしていましいた。しかし、これでは恐らくブラウザに保存されてしまいますよね?
URLバーに何かを入力していると、以前に検索したことのある内容や、以前に訪れたことのあるウェブサイトが表示されることがよくあります。
これは、TabキーやEnterキーを押すだけで、考えていることをすぐに終わらせることができるので、キーストロークを省略するのに役立つという意味で、良いユーザーインターフェース機能です。
しかし、自分がどこに行ったのか、何を検索したのか知られたくない場合や、研究室のコンピュータである場合、兄弟や同僚も使っている場合には、少々プライバシーを侵害していると言えます。
自分が入力したものがURLバーに表示されるのを嫌う理由はたくさんあります。
私は先週、これを避けるためにPOSTを使うことを提案しました。
実際にそれをやってみましょう。
index.htmlに戻ってみます。
HTMLファイルに簡単な変更を加え、methodをpostに変更してみましょう。
<form action="/greet" method="get">
↓
<form action="/greet" method="post">
postはgetとほとんど同じですが、q=catsやname=DavidのようにパラメータをURLに置く代わりに、例えば言えば、仮想的な封筒の中のより下の方の、より深いところにパラメータを置きます。
そのため、ブラウザからサーバへの送信は行われますが、ブラウザがURLバーに記憶することはありません。
しかし、これを行う場合、controllerを変更する必要があります。
pythonのコードを変更する必要があるのです。
とても簡単な変更です。
/greetというルートが実際には異なるメソッドのセットをサポートすることをFlaskに伝えなければなりません。
@app.route("/greet")
↓
@app.route("/greet", methods=["POST"])
デフォルトであるgetをサポートするのではなく、methodsという少々不可解な引数を渡さなければなりません。
そして、サポートして欲しいメソッドを、pythonのリストで渡さなければなりません。
この引数を使用しない場合、デフォルトでは1番目と2番目のすべてのルートは、基本的にこのデフォルト値であるmethods=["GET"]、つまり、サイズ1のリストを持ちます。
@app.route("/", methods=["GET"])
@app.route("/greet", methods=["GET"])
これをあちこちで入力するのはちょっと面倒なので、デフォルトでは何も入力しないようにしています。
しかし、postをサポートしたい場合は、これをオーバーライドして、デフォルトであるgetを明示的にpostに変更する必要があります。
また、パラメータは別の変数から取得する必要があります。
URLの引数を参照するrequest.argsを使う代わりに、request.formに変更しなければなりません。
これら変数の名付け方はひどいものです。
Flaskでは、これらのグローバル変数、request.argsとrequest.formは、それぞれgetとpostを意味します。
request.getとrequest.postの方が良かったと思うのですが、とにかくそのようになっています。
つまり、request.argsはURLでのgetリクエスト用です。
request.formはpostリクエスト用で、同じ情報が仮想的な封筒の中により深く埋め込まれています。
これらの変更の後、flask runを実行してみましょう。
いつものようにフォームが表示されるので、Davidと入力しますが、ここで魔法がかかります。
(URLの末尾が/greetだけになっている)
ルートは相変わらず/greetですが、後で私のラップトップを使う人は、私の名前が何だったのか、私のGoogle検索が何だったのか、私のクレジットカード番号が何だったのか、あるいは私がこのフォームに何を入力したかを知ることはできません。
しかし、出力は完全に機能しています。
このように、メールアドレスやクレジットカード、パスワードなどの個人情報を収集する際にはpostを使うことになりますが、getを使うこともできます。
続いて質問させてください。
なぜなら、私たちはまだこれよりも上手くやれると思うからです。
index.htmlとgreet.htmlのデザインは、根本的に何か最適ではないような気がします。
あえて言えば、先週、いくつかのHTMLファイルを作成したときに、何人かの人が同じ問題に気付きました。
自分のホームページのためにいくつかのHTMLファイルを作成してみて、ちょっと面倒だったこと、イライラしたこと、ゴチャゴチャしていたこと、デザインが悪かったことなどはありませんか?
ブライアン「チャットでは、複数のHTMLファイルの間で内容に重複があることが指摘されています。」
↑そうなんです。
正直なところ、2つ目のファイル、3つ目のファイル、4つ目のファイルでは、恐らく以前のファイルをコピー&ペーストして、ちょっとした変更を加えただけだったでしょう。
HTMLとCSSだけでは、そしてJavaScriptを使っても、複数のページでHTMLを共有することはできません。
静的コンテンツを提供する際の、この1週間の皆さんの唯一の選択肢は、そのコンテンツを冗長にコピー&ペーストすることでした。
これを見てください。
index.htmlファイルの1行目から7行目までは、greet.htmlの1行目から7行目までと全く同じ内容です。
これから紹介するのは、Flaskや他のウェブプログラミングフレームワークを使う事によって得られるもう1つの機能です。Flaskに限った事ではありません。
共通のコンテンツを抽出する機能があります。
では、どのようにしてそれを行うのでしょうか?
3つ目のファイルを作成します。
Flaskでは慣習的にlayout.htmlと呼ばれています。
HTMLファイルなので、念のためtemplatesディレクトリに置くことにします。
定型文を全てコピー&ペーストし、greet.html固有の内容を削除します。
▼layout.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>hello</title>
</head>
<body>
</body>
</html>
今見ている1行目から10行目は、他の2つのウェブページのレイアウトのテンプレートのようなものだと思います。
というわけで、こうしてみましょう。
特別な構文を使って、{% block body %}とし、ちょっと変ですが、{% endblock %}とします。
▼layout.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>hello</title>
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
これはFlask特有の構文、あるいは技術的にはJinja特有の構文で、(JinjaというのはFlaskが使っている言語で誰かが書いたものです)基本的には「ここにプレースホルダを置け」というものです。
ここにプレースホルダを置くことで、他のHTMLを少しずつ入れていくことができます。
では、これをどうやって使うのでしょうか?
さて、index.htmlの内容を調べて、全ての冗長な部分を取り除いてみましょう。
index.htmlで唯一特別なのは、formタグです。
先ほど作ったのと同じレイアウトを使いたい場合は、上の方で{% extends "layout.html" %}という特別な構文を使い、下の方で{% block body %}としてみましょう。
そして、formタグの下に{%endblock%}と書いておきます。
▼index.html
{%extends "layout.html" %}
{%block body%}
<form action="/greet" method="post">
<input autocomplete="off" autofocus placeholder="First Name" name="name" type="text">
<input type="submit">
</form>
{% endblock %}
構文は奇妙ですが、これはC言語のヘッダファイルと精神的に似ていて、共通点を抽出して複数の場所で再利用することができます。
ここでは文字通り青写真やレイアウトがあり、そこに様々なコンテンツを差し込むことができるので、少し凝った作りになっています。
最初の行{%extends "layout.html" %}は、「Flaskよ、次のファイルlayout.htmlは基本的に私のデフォルトのレイアウトを継承し、拡張しています。」と言っています。
そのレイアウトとは、layout.htmlのレイアウトです。
任意にbodyと呼ばれるプレースホルダを定義していることに注目してください。
xやy、zと呼ぶこともできますが、ここではbodyと呼ぶことにします。
なぜなら、100%がbodyの中身のようなものだからです。
そしてindex.htmlでは、Flaskに「ここにはプレースホルダに挿入すべきコードがあります」と伝える必要があります。
ここには複数行のHTMLコードを挿入することができます。
{% block body %}→ココ←{% endblock %}
これを保存しましょう。
greet.htmlに行き、同じように冗長な部分をすべて削除し、ここで上に行って{%extends "layout.html" %}ともう一度言って、下に行き、{% block body %}と言いましょう。
さて、これらのHTMLファイルは非常に奇妙に見えます。
ここにはほとんどHTMLが記述されておらず、ただのテキストと奇妙なFlaskの構文があるばかりです。index.htmlにも同じことが言えます。
しかし、これはウェブアプリケーションがもたらす価値の1つです。
pythonのようなプログラミング言語があれば、これらの共通点を抽出して、GoogleやFacebookなどが毎日行っているように、動的にページを生成することができます。
それではflask runを実行し、うまくいくかどうか見てみましょう。
今回は色々と変更を加えました。
URLを開いてみると、ほら、今のところ問題なく動作しているようです。
先に進んで、これを送信する前に、ページソースをお見せしましょう。
▼indexのソースコード
<!DOCTYPE html>
<html lang="en">
<head>
<title>hello</title>
</head>
<body>
<form action="/greet" method="post">
<input autocomplete="off" autofocus placeholder="First Name" name="name" type="text">
<input type="submit">
</form>
</body>
</html>
インデントが少しずれているなど、きれいに出力されているとは言えませんが、問題ありません。
テンプレートはきちんとしたスタイルでなければなりません。
しかし、テンプレートがFlaskでレンダリングされた後に、空白部分がきれいでなくても、ブラウザは気にしないので問題ありません。
しかし、確かに私は今や本格的で完全なウェブページを持っています。
このフォームを送信してみましょう。
(Davidと入力し、送信すると、hello,Davidと表示される)
まだ動作しているようです。
hello,Davidと表示されていますが、ここでページソースを見ると、Confirm Form Resubmissionと表示されました。
POST経由でクレジットカードを使って間違って2回チェックアウトされてしまうと困るので、手動で再読込みをクリックして、このフォームを再送信したいことを確認しますが、これについては何も危険なことはありません。
▼/greetのソースコード
<!DOCTYPE html>
<html lang="en">
<head>
<title>hello</title>
</head>
<body>
hello,David
</body>
</html>
これで私の/greetルートが完成しました。
先週のツールキットにはこのツールがなかったので、恐らくこれで皆さんの頭痛や煩わしさを解消し、コピーペーストの手間を省くことができるでしょう。
これはHTMLやCSS、あるいはJavaScriptだけではできないことです。
ここでテンプレートが威力を発揮するわけです。
テンプレートでは2つの{{}}中括弧を使って変数の値を入力できるだけでなく、この中括弧の%記号を使って、他のファイルの実際の内容を入力することもできます。
ふう。
質問や不明点はありませんか?
ブライアン「Jinjaとは何でしょうか?」
↑はい。それについては説明しませんでした。
というのも、今日のウェブプログラミングの世界では新しい用語がたくさん出てきていて、正直困っていたからです。
これはコードの再利用の良い例です。
世の中にはたくさんの賢い人がいて、様々な問題を解決しています。
もし私たち全員が世界中のコンピュータの問題を一人ですべて解決しようとしたら、ちょっと迷惑だし、ちょっと傲慢ですよね。
そこでFlaskの作者はテンプレート機能のために、車輪を再発明したり、独自の構文を考えたりしないことにしました。
二重中括弧{{}}や中括弧{}と%記号を使った、他の人による素晴らしい構文を流用することにしました。
これは良いことで、もし皆さんが何らかの理由でアンチJinjaであったり、構文が嫌いであったりする場合、技術的には別の言語を使うことができます。
これは異なるソフトウェアを接続しても相互運用が可能なコンポーネントベースの設計の有効性をコンピュータで実証したことになります。
※コンポーネントとは、部品、成分、構成要素などの意味を持つ英単語。 ITの分野では機器やソフトウェア、システムの構成する部品や要素などのことを意味する。
コースのウェブサイトにJinjaのドキュメントへのリンクを追加し、テンプレートでJinjaを使ってできることを少しずつ紹介していきます。
繰り返しになりますが、Jinjaとは、テンプレートファイルに表示される二重中括弧{{}}の構文と中括弧{}と%記号の構文のことです。
さて、helloの最後の機能に取り掛かりましょう。
これまでのapplication.pyでは2つの別々のルート、つまりフォーム用の/ルートと、実際に情報を表示する/greetルートを持つことで、構文的には複雑でも、概念的には少しシンプルにしてきました。
しかし厳密に言えば、少し賢くなってこれらのルートを組み合わせることもできます。
もし皆さんが多くのルートを持つかなり洗練されたウェブサイトを構築しているとすると、1つのフォームが別のフォームに送信しなければならない場合、すべてのルートが2つのルートを必要としていたら、一種のイライラを感じるでしょう。
そこで実装を改善し、1つのルートを再利用することを提案します。
application.pyに戻り、greetルートを削除してみましょう。
from flask import Flask, render_template,request
app = Flask(name)
@app.route("/")
def index():
return render_template("index.html")
少し冗長になっているのが気になるので、デフォルトルートであるindexがgetとpostの両方のメソッドをサポートするようにします。
@app.route("/", methods=["GET","POST"])
つまり、ユーザーにフォームを表示するだけでなく、ユーザーに挨拶するためにも同じルートを使うことにします。
そのため、同じルートでgetとpostの両方をサポートする必要があります。
では、どうすればいいのでしょうか?
まず、request.method == "GET"の場合は、このコードを実行します。
def index():
if request.method == "GET":
return render_template("index.html")
そして、request.method == "POST"の場合は、このコードを実行します。
if request.method == "POST":
return render_template("greet.html",name=request.form.get("name","world"))
新しいことは何もしていません。
これは先ほど削除したコードと全く同じです。
しかし、同じルートを再利用して、controllerのコードで論理的にチェックしています。
メソッドがGETであれば、フォームを表示します。
メソッドがPOSTであれば、ユーザーに挨拶します。
さて、なぜこれがうまくいくのでしょうか?
http://www.harvard.edu、yale.edu、google.comなど、インターネット上のURLにアクセスするときは、常にGETリクエストを行っていました。
実際、先週のHTTPリクエストのサンプルとヘッダを見てみると、先週行ったすべてのリクエストのエンベロープ(仮想的な封筒)には、デフォルトでGETというキーワードが含まれていました。
GET / HTTP/1.1
...
GETはフォームとは基本的に何の関係もありませんが、実際にはどんなURLにアクセスしても使われるデフォルトのHTTP動詞です。
ウェブ上のURLにアクセスするときは、デフォルトでGETを使っています。
フォームを送信する際には、最初に紹介したようにGETを使うこともあれば、2番目に紹介したようにPOSTを使うこともあります。
さて、ここで何が言いたいのでしょうか?
index.htmlに戻って、私のフォームに1つの変更を加えてみましょう。
<form action="/greet" method="post">
投稿はPOSTで行いたいと思います。
/greetではなく、デフォルトの/ルートで送信するつもりです。
<form action="/" method="post">
ここで全てを正しく行ったとすると、flask runを実行し、以前のようにURLを開くと、私は/ルートにいることになります。
ページソースを見ると、action="/"になっているので、同じルートの2つの異なる機能を使っているようなものです。
私の名前を入力して送信をクリックすると、ほら、驚くべきことにうまくいきました。
でも、URLバーを見ても、私のルートは変わっていません。
さて、なぜこれが便利なのでしょうか?
そうかもしれないし、そうでないかもしれません。
pythonでプログラムを表現することができれば、受信したHTTPリクエストの中にある2つの動詞GETとPOSTを区別することができるので、これは皆さんが今持っている能力に過ぎません。
何か良い事があるとすれば、1つのことを別のことに渡すために2倍のルートを必要としない、ということだけです。
これについてはまた後日紹介します。
それでは、helloやFlask、ルートについて何か質問はありますか?
ないですか?
さて、ちょっとしたお楽しみとして、ここに昔私が作った最初のウェブアプリケーションのスクリーンショットがあります。
大学生の頃、学内スポーツに参加していましたが、それはスポーツというよりもコンピュータサイエンス的な意味での参加でした。
ハーバード大学の新入生向け学内スポーツプログラム、通称Frosh IMsの最初のウェブサイトをボランティアで作りました。
当時、私たちは紙に名前とメールアドレスを書き、スポーツを選択して、ハーバードヤード(ハーバード大学の中央にある芝生のエリア)を歩いて横切り、プロクターやレジデントアドバイザーのドアの下に滑り込ませれば、それでスポーツの登録が完了しました。
でも、ウェブサイトはありませんでした。
インターネットはありましたが、ウェブサイトはまだありませんでした。
90年代後半の話ですね。
そこで私はスポーツ登録のための紙切れと同じアイデアをウェブブラウザで実現できたら楽しいだろうと考えました。
残念ながら、当時の私はCとC++、それに他のCSコースで習った言語をいくつか知っているだけでした。
ウェブプログラミングについては何も知りませんでした。
そこで、友人に質問したりしながらウェブプログラミングの方法を独学で学びました。
たまたまPerlという言語を使ったのですが、最終的にはこのようなおどろおどろしいデザインになりました。
上の方にはJavaScriptを使ったたくさんのメニューオプションがあり、ここにはいくつか画像があり、バックエンドにはすべての登録データを保存するCSVファイルがありました。
※バックエンドとは、ソフトウェアやシステムの構成要素のうち、利用者や他のシステム、ソフトウェアなどから見えないところでデータの処理や保存などを行う要素のことをこのように呼ぶ。
当時の私は、データベースについて何も知りませんでした。
しかし、これが私のウェブプログラミングへの初めての挑戦でした。
この最初の休憩の後にやろうと思ったのは、HTML、CSS、python、そして最終的にはSQLを使って登録システムのアイデアを実装することですが、今日は外見についてはあまり気にしていません。
ここで5分間の休憩を取りましょう。
戻ってきたらFrosh IMsを実装します。
さあ、戻ってきました。
目下の目標は、私が何年も前にこのFrosh IMsウェブサイトで初めて実装した、最も原始的な機能を実装することです。つまり、学生がスポーツに登録できるようにすることです。
スポーツに登録するには、全員の名前と、登録したいスポーツを集めます。
では、どのようにすればいいのでしょうか?
先に進み、前のhelloの例からレイアウトだけをコピーしてみましょう。
そして、実は1つだけ変更を加えるつもりです。
数行のコードを追加するだけで、ウェブサイトをかなりモバイルフレンドリーにすることができます。
特にheadに<meta name="viewport" content="initial-scale=1,width=device-width">というタグを追加しようと思います。
viewportとは、ウェブブラウザのユーザーインターフェースのほとんどを定義する長方形の領域のことです。
この最後のキーワードは魔法のようなものです。
この呪文は基本的にユーザーのデバイスの幅が何であれ、ブラウザに「この幅が私のウェブページの最大幅だと思ってください」と伝え、ユーザーがラップトップやデスクトップ、iPhoneやAndroidなどを使用しているかどうかに応じて、フォントサイズを自動的に拡大・縮小することになります。
ウェブサイトをレスポンシブルにするためには他にもやるべきことがあり、そのためにBootstrapのようなライブラリを使うのも有効です。
これは、少なくともフォントサイズの問題を解決するのに最低限役立ちます。
そこで、ここではこれを含めておきます。
さて、application.pyを見てみましょう。
残念ながらFrosh IMsの実装に役立つものはまだありません。空っぽです。
休憩中に作成したapplication.pyは空で、templatesディレクトリがありますが、その中にはlayout.htmlがあるだけです。
ですから、私たちは一緒にここで作業しなければなりません。
それでは、アプリケーションの構築を始めましょう。
まずはflaskからFlask関数自体をインポートし、そして後々必要になるであろうrender_template、requestもインポートします。
from flask import Flask, render_template, request
__name__を使って、このFlask関数でアプリケーションを初期化してみましょう。
app = Flask(name)
これは先ほどやったのと同じ方法です。
そして、先に進んで最初のルートを定義しましょう。
@app.route("/")
indexという関数を定義します。何とでも呼べますが、これは慣例です。
def index():
ここでindex.htmlというテンプレートをレンダリングしてみましょう。これはまだ存在しません。
return render_template("index.html")
では、index.htmlを作ってみましょう。
index.htmlという名前の新しいファイルを作成します。
繰り返しますが、これを何と呼んでも良いのですが、もしこれが私のデフォルトルートになるのであれば、私のデフォルトページを最も一般的なデフォルト名であるindex.htmlと呼んだ方がいいかもしれません。
これらをFrosh IMsフォルダとtemplatesフォルダに格納して、適切な場所に置きます。
ここで{%extends "layout.html" %}と言っておきましょう。
そして、ここでは{% block body %}、{% endblock %}とします。
ここで、これだけのコードを書いたことに少し不安があるので、TODOを入れて、全体が動作することを確認しておきましょう。
{% extends "layout.html" %}
{% block body %}
TODO
{% endblock %}
ディレクトリに戻って、flask runを実行します。
ブラウザを開いてみると、TODOと表示されています。
ページソースを見ると、先ほど追加したmetaタグを含む本格的なウェブページが表示されますが、ここには大きなTODOがあるだけです。
さて、私は何をしたいのでしょうか?
一番やりたいことは、学生がスポーツに登録できるようにすることです。
そこで、それを行うためのHTMLフォームを定義してみましょう。
index.htmlテンプレートのbodyブロックの中で定義します。
まず、h1タグでRegisterとし、フォームを定義しましょう。
actionは何でも構いませんが、同じルートを使って物事を複雑にするのではなく、シンプルに/registerとしてみましょう。※registerは登録の意味
<h1>Register</h1>
<form action="/register"
ルームメイトに私がどんなスポーツに登録しているのか知られないよう、methodはPOSTにして、秘密にしておきましょう。
<form action="/register" method="post"></form>
そして、ここではユーザーに名前を聞いてみましょう。
<input name="name">とします。
この入力の名前は文字通り人間の名前になります。
ボックスのタイプはtextにして、先ほどと同じように外見を整えましょう。
<input name="name" type="text">
オートコンプリートをoffにし、オートフォーカスを有効にし、プレースホルダを置いて、名前を入力することが分かるようにします。
<input autocomplete="off" autofocus placeholder="Name" name="name" type="text">
そして、submitボタンを用意します。
<input type="submit" value="Register">とし、ウェブサイトをもう少し使いやすくしましょう。
サーバを一旦停止して再起動し、変更した内容をすべて読み込んでから、もう一度このURLにアクセスしてみましょう。
(名前を入力するフォームと「登録」というボタンが現れる)
ほら。これは名前を聞いているだけなので、まだ完全ではありません。
そこで、スポーツ名を入力してもらう必要があります。
これにはたくさんの方法があり、CS50ではまだすべてを見ていないかもしれませんが、実際にインターネットでウェブサイトを見るときには必ず使っているはずです。
まずはドロップダウンメニューを作ってみましょう。
選択肢を選ぶので、selectメニューと呼ばれるものを使ってみましょう。
名前はスポーツにします。
<select name="sport"></select>
selectメニューは、ドキュメントやオンラインチュートリアルを読むと、optionと呼ばれる子を持たなければならないことがわかります。
そして、子であるoptionは値を持たなければなりません。
例えば、学内でよく行われるスポーツであるドッジボールという値を使ってみましょう。
<option value="Dodgeball"></option>
optionタグの中にはリンクのように、人間が目にするべきものを書かなければなりません。
そこで、ここでも冗長ですがDodgeballとします。
<option value="Dodgeball">Dodgeball</option>
そして、ここで少し楽をします。
ここでちょっとしたコピーペーストをしていきます。
<option value="Dodgeball">Dodgeball</option>
<option value="TODO">TODO</option>
<option value="TODO">TODO</option>
<option value="TODO">TODO</option>
これをFlag Footballにしてみましょう。
これはSoccerにしてみましょう。
リンクのように二重性があり、同じ値を持っているかもしれませんが、同じである必要はありません。
しかし、今はシンプルに考えましょう。
<>の中にあるスポーツ名はコンピュータが見るものです。
<>の外にあるスポーツ名は人間が見るものです。
Volleyball、Ultimate Frisbee…としましょう。
これでselectメニューができました。
サーバを再起動して同じURLにアクセスしてみましょう。
(名前の入力フォームの隣にドロップダウンメニューができている)
醜いユーザーインターフェースですが、確かに非常にミニマルです。
名前をDavidと入力でき、スポーツ名はデフォルトではDodgeballと表示されていますが、選択することができます。
しかし、誰もがDodgeballを最初に選択するようにするのは良くありません。
そこで、例えば空の値を持つオプションを用意します。
<option value=""></option>
そして、ここで例えばsportと言って、値を持たないようにします。
つまり、これは実際に有効なオプションではありません。
では、私のURLを開いてみましょう。
すると、ドロップダウンにsportと表示されています。
でも、sportを選択できてしまうのも変ですよね?
selectメニューのドキュメントを読むと、実際にオプションを無効にしたり、例えばデフォルトで値を選択することもできることがわかります。
<option disabled selected value="">Sport</option>
optionタグのドキュメントに記載されている追加属性を指定することで、ユーザーがsportに登録しないように、ユーザーインターフェースをもう少し強固にすることができます。
さて、ブラウザをリロードしてみると、デフォルトではSportと表示されていますが、選択しようとするとグレーアウトされ、選択できなくなっています。
これはもう有効な値ではありません。
というわけで、多少ユーザーインターフェースが改善されました。
Dodgeballは良いですね。
私の時代には学内スポーツではありませんでしたが、最近ではあるようですね。
では、早速登録してみましょう。
(Not Foundと表示される)
当然ながら何も起きません。
なぜでしょうか?
先ほどのNot FoundのURLを見てみると、まだ/registerルートを実装していません。
(URLの末尾に/registerが付いている)
では、実装してみましょう。
そのためにはapplication.pyに戻って、登録の仕掛けを作りましょう。
@app.route("/register")ではregisterと呼ぶべき関数を定義しますが、名前は何でも構いません。
@app.route("/register")
def register():
この関数の中身は最初はシンプルにしておきましょう。
success.htmlというテンプレートを返し、登録が成功したと仮定しましょう。
@app.route("/register")
def register():
return render_template("success.html")
実際、success.htmlという新しいファイルをFrosh IMsのtemplatesディレクトリに作成します。
{%extends "layout.html"%}とし、{%block body%}を設定し、念のため{%endblock%}も用意しておきましょう。
そして、bodyの部分は今のところシンプルにしておこうと思います。
▼success.html
{%extends "layout.html"%}
{%block body%}
You are registered!
{%endblock%}
別に誰かを登録するわけではありませんが、登録されたことにしておきます。
これでアプリケーションを再起動して、フォームに戻り、DavidをDodgeballに再登録して、registerをクリックすると…
(Method Not Allowed
The method is not allowed for the requested URL.と表示される)
えっと。これはバグです。
ここで一旦停止して、ピースがきちんと収まっているかどうか確認してみましょう。
/で登録しているのに、methodが許可(allowed)されていません。
何か良い修正方法はないでしょうか?
ターミナルウィンドウに赤でエラーメッセージが表示されていますが、ステータスコード405、つまりメソッドが許可されていません。稀なケースですが、確かに失敗です。
サンティアゴ「@app.routeを定義したときに、/registerのmethodをpostに指定していなかったと思います。」
↑ええ。デフォルトではmethodはすべてGETですが、プライバシー保護のためにPOSTを使いたい場合は、このルートでサポートしてほしいメソッドはこのリストの通りであり、そのリストはPOSTというキーワードのみを含むサイズ1になる、ということをFlaskに伝えれば良かったのです。
@app.route("/register")
↓
@app.route("/register", methods=["POST"])
繰り返しになりますが、このような問題に遭遇した時、初めての場合は絶対にそうなりますが、エラーメッセージの詳細を気にするのではなく、アイデアを大切にしてください。
例えば、何が悪かったのか?→メソッドです。→メソッドが許可されていない→それはどういう意味だろう?
これまで話してきたメソッドはGETとPOSTだけだったので、もしかしたらそれに関連するものかもしれません。
問題のルートは/registerだったので、/registerルートに関連しているのかもしれません。
これらの手がかりが、先ほどサンティアゴが指摘したような解決策につながるかどうか見てみましょう。
それではサーバを再起動します。
フォームをリロードしてDavidと入力して、Dodgeballを選択して、登録して、ほら、登録されたでしょう。
(You are registered!と表示される)
いいでしょう。
実際には何も登録されていません。
今はまだエラーチェックの機会を逃しているような気がします。
実際、フォームに戻ってDavidと入力せず、Dodgeballも選択せず、registerをクリックしてみましょう。
(You are registered!と表示される)
これはちょっと馬鹿げていますね。
何も入力しなくても、登録された!と言っています。
つまり、ここではエラーチェックの機会を逃しているのです。
ですから、これを追加してみましょう。
def register():
if not request.form.get("name") or not request.form.get("sport"):
return render_template("failure.html")
return render_template("success.html")
再度サーバを再起動してみましょう。
フォームには何も変化がありませんが、もし私が何もせずにRegisterをクリックすると、OK、今度は本当に悪いことが起こりました。
(Internal Server Error
The server encountered an internal error and was unable to complete your request. See terminal window.
と表示される)
これは500エラーの1つです。
どこで失敗したのか見てみましょう。
Internal Server Errorとそれに続く段落には、あまり詳しく書かれていません。
しかし、ターミナルウィンドウに戻ると、ここにJinja2の例外が発生しています。これはテンプレートのための言語で、「failure.htmlが見つからない」と言っています。
これは私が馬鹿なミスをしただけですから、サーバを止めてみましょう。
とりあえずsuccess.htmlをコピーしておきましょう。
もっと詳しくすることもできるかもしれませんが、ここではシンプルに、このfailure.htmlをFrosh IMsのtemplatesディレクトリに入れておきます。
▼failure.html
{%extends "layout.html"%}
{%block body%}
You are not registered!
{%endblock%}
では、サーバを再起動してみましょう。
何も入力せずにRegisterをクリックすると…ほら。
(You are not registered!と表示される)
これでやっとチェックされるようになりました。
フォームに戻って、名前は入力し、スポーツを選択しないようにすると、やはり登録されません。
ここにはまだハッキングの余地があります。
先週の話ですが、ウェブページ上で何かを右クリックしたり、コントロールクリックしたりすると、chromeやお好みのブラウザで検査(検証)することができ、HTMLを調べることができました。
これを見ると、例えばテニスが無いことに少し腹が立つかもしれませんね。
テニスは学内スポーツプログラムでは提供されていませんが、なんとしても登録したいのです。
そこでブラウザの検証(inspect)メニューを使って、例えば「UltimateFrisbee」をtennisに変えてみます。文字通りページのHTMLを書き換えて、「テニスはハーバード大学のスポーツなんだ」としたら、どうでしょう?
(開発者ツールの要素タブで書き換えると、ドロップダウンメニューも書き換えられる)
で、何ができるかというと、テニスに登録するには私の名前Davidを入力します。
(You are registered!と表示される)
どうですか?ウェブサイトをハッキングしました。
今回はある意味ハッキングしたと言えるかもしれません。
今、明らかに私はサーバ上のこの情報に対して何もしていません。
ただやみくもに「登録された」「登録されてない」と言っているだけです。
でも、今やったことに気づいてください。
実際、私がやったことをよく見てみましょう。
もう一度、このページのネットワークタブに言って、このフォームを再送信してみましょう。
フォームに戻って、Davidと入力して、sportを右クリックして、inspectでUltimateFrisbeeをTennisに変更します。
さて、ネットワークタブに行ってみましょう。ドロップダウンからTennisを選択し、Registerをクリックします。
ここで何が起きているのか見てみましょう。
先週と同じようにすべてのHTTPリクエストのリストが表示されています。
では、/registerを見てみましょう。
スクロールして、フォームデータを見てみましょう。先週は表示されませんでしたが、今回はPOSTリクエストが表示されています。
(registerをクリック→ペイロードタブを開くとform Dataが表示される)
name:David
sport:Tennis
私は合法的にブラウザからDavidとTennisをサーバに送信しました。
サポートされていないスポーツであるTennisの登録を私にさせないようにする責任は、プログラマである皆さんにあります。
そのため、より多くのエラーチェックが必要になります。
ユーザーが正しく名前とスポーツを入力したことを信じるだけでは十分ではありません。
入力がないことをチェックするだけではダメなのです。
本当はもっと賢く、ブラウザが送ったスポーツが実際にハーバード大学に存在するスポーツなのかどうかまで確認すべきなのです。
エラーチェックなしにはうまくいかないことは、すぐわかるでしょう。
銀行口座にお金を預ける場合はどうでしょうか?
HTMLをハックして、入出金の金額を変更することができますよね?
HTMLを変更するだけで、サーバのデータやデータベースの内容を変更できるなどということは、あってはならないはずです。
ユーザーを絶対に信用してはいけないのです。
では、どうやってこれを防げばいいのでしょうか?
この問題を解決する方法がありますが、それによって他の機能も得ることができます。
IDEに戻って、この欠陥を取り除きましょう。
index.htmlでは、文字通りコピー&ペーストに頼って全てのスポーツをHTMLにハードコーディングしていました。
ここが、もっとうまくできるのではないかという最初のヒントです。
代わりに、application.pyに入り、SPORTSというグローバル定数を定義してみましょう。
定数を定義するときの慣例に従って、これを大文字にします。
そして、SPORTSはpythonのリストになります。
リストなので、[]角括弧を使って複数行に分けて、きれいにしておきます。
※@app.routeの上に追加
SPORTS = [
"Dodgeball",
"Flag Football",
"Soccer",
"Volleyball",
"Ultimate Frisbee"
]
以上がSPORTSです。
では、このSPORTSのリストをどのように使えばいいでしょうか?
テンプレートの中に入れてしまえばいいのです。
次のようにします。
sportsでもx、y、zでも何でも良いのですが、sportsの方が説明しやすいのでsports=SPORTSとします。
@app.route("/")
def index():
return render_template("index.html",sports=SPORTS)
つまり、テンプレートの中にsportsという変数を作り、その値を同じ名前のグローバル変数にする、ということです。
明確にしておきたいのですが、Flaskでは名前を決める必要があります。
SPORTSをそのまま渡すことはできません。
なぜなら、Flaskはそのような単一の引数をどう扱えばいいのかわからないからです。
このようなものには名前をつけなければなりません。
繰り返しになりますが、1つは右の変数で、もう1つは左のテンプレートで使用している名前です。
sports = SPORTS
では、これをindex.htmlでやってみましょう。
ここでテンプレートがより力を発揮します。
Jinjaのようなテンプレートは{}中括弧が左右にあったり、{}中括弧と%記号のブロックがあったりする場合だけでなく、簡単なプログラミングの構成要素も備えています。
実際、このようなことができます。
<select name="sport">
<option disabled selected value="">sport</option>
{% for sport in sports %}
{% endfor %}
</select>
ちょっと馬鹿げた構文ですが、endの後に、さっきの前置詞の名前forをつけています。
先に進んで、<option value="{{sport}}">{{sport}}</option>として、オプションを生成しましょう。
<select name="sport">
<option disabled selected value="">sport</option>
{% for sport in sports %}
<option value="{{sport}}">{{sport}}</option>
{% endfor %}
</select>
これで完了です。
テンプレートの中でforループを使っていますが、この構文はpythonとほとんど同じように見えます。これは意図的なもので、Jinjaの作者は基本的にpythonの構文を借用しているので、新しい情報が増えすぎることはありません。
このJinjaの構文を使って、sports変数を繰り返し処理しています。
これはforループなので、sportsを最初のsport、2番目、3番目、4番目、5番目…と繰り返し設定しています。
{% for sport in sports %}
そして、このブロックの中、つまりforループの中で、{{}}二重中括弧を使って、2ヶ所にsportを入れています。
<option value="{{sport}}">{{sport}}</option>
Flaskやウェブフレームワーク全般のこの機能で驚くべきことは、flask runを実行してURLを開き、このフォームにアクセスすると、ほら、そこにあるということです。
(ドロップダウンメニューに全てのスポーツが反映されている)
ブラウザでページのソースを見てみると、これらのオプションは全て実際に存在していますが、手で入力する必要はありませんでした。
今はそれが配列になっていて、更に多くのHTMLを動的に生成しています。
更に、この方法によって、application.pyでは単にこの値をチェックする以上のことができるようになりました。
if not request.form.get("name")では、名前が無いかどうか確認していましたが、この部分では、スポーツが無い場合、もっと具体的に「ユーザーがフォームに入力したスポーツがSPORTSに含まれていない場合」と言ってみましょう。
or not request.form.get("sport"):
↓
or request.form.get("sport") not in SPORTS:
ここでpythonは非常にエレガントになります。
今、私は2つのブール式をチェックしています。
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")
request.form.get("name")で名前が無い場合、またはユーザーがフォームに入力したスポーツがグローバル変数SPORTSに無い場合はfailureとなります。
ですから、テニスを送ってもいいし、水球を送ってもいいし、どんなスポーツを送っても良いのですが、私は今、サーバ側のデータを基に皆さんの送信を検証しているので、それらを拒否します。
繰り返しになりますが、これはテーマに沿ったものです。
残念ながら、ユーザーの入力は絶対に信用できません。
迷惑なハッカーや友人、兄弟が皆さんのプログラムをハッキングしたり、クラッシュさせようとすることを常に想定しておく必要があります。
しかし、このように防御的なプログラミングをすることで、それを未然に防ぐことができます。
例えば、David、Dodgeball、Registerと入力すると、うまくいきます。
しかし、このちょっとしたハックをもう一度やってみましょう。
HTMLにアクセスして下に行き、例えばUltimate FrisbeeをTennisに変更して閉じ、Tennisを選択して名前にDavidと入力すると…
(You are not registered!と表示されるはずが、You are registered!と表示されてしまう)
ちょっと待ってください…w困ったな。
サーバが古いままで、再起動されていませんでした。
もう一度同じハックをしなければなりません。
同じことをすると…これでOKです。
(You are not registered!と表示される)
一瞬混乱してしまいましたが、もう一度サーバを再起動すれば、実際に行った変更をリロードしてくれます。
飛び込んで助けてくれたブライアン、本当にありがとうございました。
おかげで原因がわかりましたw
さて、ユーザーが選択したメニューオプションのリストを動的に生成し、その同じリストのデータに対してサーバ上で検証を行いましたが、他に質問や不明点はありませんか?
ブライアン「特にありません。」
わかりました。
では、これらの異なるユーザーインターフェースメカニズムを実際に手短に説明してみたいと思います。
一応言っておくと、私たちはユーザーインターフェースデザインにはあまりこだわらないということです。
Bootstrapのようなライブラリを使えば、HTMLタグやCSSクラスを追加するだけで、フォームをより美しく見せることができますが、ここでは1つだけ微調整して、UIをセレクトメニューから様々なものに変更してみましょう。
index.htmlに戻って、forループの中でセレクトメニューの代わりに
<input name="sport" type="radio" value="{{sport}}">{{sport}}
という入力を定義してみましょう。
radioボタンとは、ウェブサイト上のボックスにチェックを入れるための互いに排他的なボタンのことです。そして、この値valueは特定のスポーツになります。
サーバを再起動してみましょう。
ページをリロードしてみると、ほら。ドロップダウンメニューではなく、レイアウトは多少異なりますが、同じ結果を返すradioボタンがあります。
これらのボタンは昔の車のラジオのように、一度にどれか1つしか選択できない、相互に排他的なボタンですが、それ以外のサイトの動作は全く同じです。
では、どちらを選ぶべきでしょうか?
それは場合によります。
100種類のスポーツがあるからといって、100種類のラジオボタンを使えばスペースを取ってしまうでしょう。
その代わりにドロップダウンメニューを使用することをお勧めします。
さて、私たちがまだやっていない重要なことがありますが、それは何でしょうか?
せっかくダイナミックなインターフェースを手に入れたのに、ユーザーがRegisterをクリックした時点で情報を捨ててしまっています。
そこで、実際に適切な登録システムを構築できるよう、昔私がやったように誰が実際に登録したかを覚えておけるようにしましょう。
どのようにすればいいのでしょうか?
これまでpythonを見てきましたが、pythonプログラムの中でデータを保存するには、リスト、辞書、セットを使うことができました。
pythonにはたくさんのデータ構造があります。
では、シンプルに始めてみましょう。
登録者を辞書に格納してみましょう。
今のところ変更されていないapplication.pyの中で、もう1つのグローバル変数REGISTRANTS(登録者)を与え、空の辞書に初期化してみましょう。
※SPORTSの上
REGISTRANTS = {}
何週間か前に、私の名前と電話番号、ブライアンの名前と電話番号を追加した電話帳を実装するために、このようにしたことを思い出してください。
pythonの辞書を使って、名前と電話番号のキーと値のペアを格納しました。
ここでも似たようなことをやってみましょう。
ユーザーの名前と選択されたスポーツを保存して、有効な登録のリストをコンピュータのメモリに保存しましょう。
では、どのようにしてこれを行えばいいのでしょうか?また、何かを変更する必要があるでしょうか?
ラジオボタンではなく、画面にフィットするようにセレクトメニューに戻してみましょう。
ここからは、先ほどのユーザーインターフェースに戻ります。
今の目的は、登録をクリックした時に、単に登録されているかされていないかを見るだけではなく、実際にユーザーを登録する、ということです。
そこで、ユーザーの登録を実際に処理するファイルである、application.pyに少し手を加える必要があると思います。
単にフォームを検証して、名前やスポーツに参加しているかどうかをチェックするのではなく、少し違ったことをやってみましょう。
ここで新しく始めてみましょう。
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")
↓
一旦削除
次のようにします。
name = request.form.get("name")
このように入力された内容を変数に入れておけば少しは使いやすくなるでしょう。
そして、もし名前が無ければ、次のようにします。
if not name:
return render_template("failure.html")
次にスポーツをチェックしてみましょう。
sport = request.form.get("sport")
こちらももう少し扱いやすくするために変数に格納します。
そして、もしスポーツでなければfailure.htmlを返します。
if not sport:
return render_template("failure.html")
そして、もう1つのチェックを行いますが、今回はもう少し厳密に行います。
sportがSPORTSに含まれていなければ、failure.htmlを返しましょう。
if sport not in SPORTS:
return render_template("failure.html")
しかし、これはちょっと馬鹿げていると思います。
もし私がやっていることが、プログラマとしてエラーが何故起きているのか知っていながら、ユーザーに「あなたは登録されていません」としか言わないなら、それは良いデザインであるとは言えません。
ユーザーが何を間違えたのか、何の情報も与えていないのですから。
そこで代わりにこうしてみましょう。
failure.htmlの代わりに、error.htmlという別のファイルを作ってみましょう。
例えばこんなメッセージです。
▼error.html
{%extends "layout.html"%}
{%block body%}
{{ message }}
{%endblock%}
メッセージのプレースホルダ以外には何もありませんが、これが何をしてくれるか見てみましょう。
それではapplication.pyに戻ってみましょう。
そして、error.htmlに、もう少し具体的に「名前が入力されていません」のようなエラーメッセージを渡してみましょう。
if not name:
return render_template("error.html", message="Missing name")
そして、その下では、「スポーツが入力されていません」というメッセージにしてみましょう。
if not sport:
return render_template("error.html", message="Missing sport")
そして、その下では、「有効なスポーツではありません」としましょう。
if sport not in SPORTS:
return render_template("error.html", message="Invalid sport")
つまり、そこにはスポーツがありますが、それは有効なSPORTSのリストに含まれていないということをユーザーに伝える必要があります。
では、実際に試してみましょう。
実際に試してみて、データを正しく入力し、これらを見ることができるようにしてみましょう。
まずは何も入力せずに登録してみましょう。
(Missing nameと表示される)
OKです。
単に「登録されていません」と表示されるよりも親切ですね。
OK、私の名前を教えてあげましょう。Davidです。登録。
(Missing sportと表示される)
OK。
では、スポーツを入力してみましょう。
これで登録しても、まだ成功した結果を処理していないので、何か別のエラーがでるかもしれません。
application.pyの/registerルートに戻って、ユーザーを登録するとはどういうことなのかを考えてみましょう。
ユーザーはコンピュータのメモリにある特定のスポーツのために登録される、ということを覚えておきたいと思います。
コンピュータのメモリではREGISTRANTSというグローバル変数を使っています。
グローバル変数であることを明確にするために全て大文字にしていますが、これは必須ではありません。
そして、ここではREGISTRANTS[name]=sportとしてみましょう。
これは名前と電話番号、キーと値でやったこととよく似ていて、nameを使ってpythonの辞書にインデックスを付け、値をsportに設定しています。
そして、とりあえずその後にこうしましょう。
return render_template("success.html")
これを試してみましょう。
サーバを再起動し、ページをリロードします。
David、Dodgeballとして、登録してみましょう。
登録されたと表示されました。
Brian、Flag Footballとして、登録してみましょう。
登録されました。
しかし、誰が登録されているかはわかりません。
しかし、ここで少し整合性のチェックをしましょう。
登録者をプリントアウトしてみましょうか。
疑問があるときはprintが皆さんの友人です。
print(REGISTRANTS)
もう一度flask runを実行してみましょう。
(ブラウザにはYou are registered!としか表示されないが、
ターミナルウィンドウに{'David':'Dodgeball'}と表示される)
ターミナルウィンドウを見てみると、すでに色んな人が登録しているようですねw
ライブデモを行う際にインターネットがあると、このような問題が生じますが、どうやら実際に機能しているようです。
先に進んで、ここをもう少し綺麗にし、単に登録者を印刷するのではなく、次のようにしてみましょう。
success.htmlではなく、registrants.htmlというテンプレートをレンダリングしてみましょう。
これを実現するには別のルートが必要になると思いますが、このルートは、これまでにこのような成功メッセージを動的に生成したことがないという点で、ちょっと興味深いものになるでしょう。
では早速何かを作ってみましょう。
新しいファイルregistrants.htmlに移動します。
▼registrants.html
{% extends "layout.html" %}
{% block body%}
<h1>Registrants</h1>
{%endblock%}
h1タグの下にはHTMLのテーブルを用意します。
<table></table>
テーブルの内部でtheadを定義します。これは厳密には必要ではありませんが、headとbody、オプションのfooterを区別するのに役立ちます。
<h1>Registrants</h1>
<table>
<thead>
</thead>
</table>
theadタグの中に行を用意し、その中に3つの列を作ります。
テーブルの見出しには、見たことがないかもしれませんが、thを使います。
<table>
<thead>
<tr>
<th></th>
</tr>
</thead>
</table>
通常、デフォルトでは、見出しであることを明確にするために太字にします。
まず人の名前と、もう1つの見出しであるスポーツを入れます。
<tr>
<th>Name</th>
<th>Sport</th>
</tr>
そして、<thead>タグの下にはtbodyというタグを付けます。これも見たことがないかもしれませんが、表の本体を表しています。
<table>
<thead>
<tr>
<th>Name</th>
<th>Sport</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
そして、テーブルの行を動的に生成してみましょう。
登録者の名前に対して、テンプレート内のJinjaのforループを進めて、テーブルの行を動的に出力してみましょう。
<tbody>
{% for name in registrants %}
{% endfor %}
</tbody>
この行の中にはいくつかのテーブルデータがあり、具体的には一列目に人の名前、そしてもう1つの列にはその人が選んだスポーツが入っています。
<tbody>
{% for name in registrants %}
<tr>
<td>{{name}}</td>
<td>{{registrants[name]}}</td>
</tr>
{% endfor %}
</tbody>
スポーツにはregistrants[name]によってアクセスすることができます。
registrantsはテンプレートに渡す辞書になります。
これでOKだと思います。
タイプするのは少し面倒です。
しかし、データが無いことに注意してください。
HTMLとテンプレートがあるだけです。
application.pyに戻って、registrants=REGISTRANTSを渡すと、SPORTS変数と同じように、私はREGISTRANTSという変数を私のregistrantsテンプレートに提供していますが、xやyやzと呼ぶこともできます。しかし、registrantsの方が適切な感じがします。これによってREGISTRANTSへのアクセスが可能になります。
REGISTRANTS[name]=sport
return render_template("registrants.html", registrants=REGISTRANTS)
ファイルを保存し、flask runを実行し、ブラウザに戻ります。
David、Dodgeballとして登録をクリックすると…
(Registrants
Name sport
David Dodgeball
と表示される)
決して綺麗なユーザーインターフェースではありませんが、ページソースを見ると、DavidがDodgeballに登録されていることがわかります。
フォームに戻って登録者をどんどん増やすことができます。
あえて言えば、これらすべての登録者をグローバル変数に保存できるという点で、これはかなりクールです。
しかし、これにはどんな欠点があるでしょうか?
私たちは確実に進歩しています。
登録したユーザーが表示されるようになったので、スポーツを運営しているレジデントアドバイザーやプロクターは誰が登録したかを知ることができます。
しかし、これには問題があります。
何がいけないのでしょうか?
辞書を使って何が悪いのでしょうか?
ソフィア「サーバをリロードするたびにデータはリセットされてしまうのではないでしょうか?データは保存されないのではないですか?」
↑その通りです。
グローバル変数を使っているだけなら、サーバがずっと稼働していて、IDEがオンラインであり続け、停電などの理由がない限り、いつまでも機能します。
しかし、ウェブサーバが停止したり、電源が切れたり、何か問題が発生したりすると、全てのデータは失われてしまいます。
だからこそ、以前のpythonではCSVファイルや最終的にはSQLiteデータベースを使っていたのです。
でも、1つだけ変更したいことがあります。
今のところ、登録者を表示する唯一の方法が、実際に自分で登録することだというのが気に入らない点です。
誰でも現在の登録者リストを見ることができるように、「/registrants」のような別のルートがあると良いと思います。
そこで、微調整を加えてみましょう。
application.pyに戻り、別のルートを定義します。
@app.route("/registrants")
def registrants():
registrants()を関数として定義しますが、実際には好きなように呼ぶことができます。
このテンプレートのレンダリングをここに移動させてみましょう。
def registrants():
return render_template("registrants.html", registrants=REGISTRANTS)
そして、ちょっと気の利いたことをしてみましょう。
ユーザーを/registrantsにリダイレクトしてみましょう。
REGISTRANTS[name]=sport
return redirect("/registrants")
Flaskライブラリからredirectという関数をインポートしましょう。
これは文字通り、HTTP301やその他のステータスコードを実行して、ブラウザに「代わりにここに行ってください」というロケーションを送信する関数です。
なぜそのようなことをするかというと、/registrantsルートの中でユーザーが登録された場合、それを他の場所にコピー&ペーストする必要がなく、冗長なコードを書く必要が無いからです。
ユーザーが自分自身と他の誰が登録したかを確認できるように、すでに存在する/registrantsにリダイレクトするように言えば良いのです。
試しにサーバを再起動してみましょう。
フォームに戻って、David、Dodgeballと登録してみましょう。
登録されました。
しかし、ここで重要なポイントとなるURLバーに注目してください。
(URLの末尾に/registrantsが付いている)
私たちは/registrantsにいます。
そして、リロードし続けると、その後の変化をすべて見ることができます。
ソフィアはデータが全て失われてしまうという、実に良い指摘をしました。
そこで、この問題を解決するために、最終的なバージョンとして、データをSQLiteデータベースなどに保存したり、メールで送信したりして、バックアップコピーを取っておくようにすべきだと思います。
では、これを提案してみましょう。
SQLとpythonの両方を見たときのインスピレーションを借りて、ここにいくつかの変更を加えることを提案しましょう。
今のところ、ほとんどの部分はそのままにしておくつもりです。
この変数は削除してしまいましょう。
REGISTRANTS[name] = sport
↓
削除
REGISTRANTS = {}
↓
削除
その代わりにSQLiteデータベースを使いましょう。
そこで、データベースをSQLとして宣言してみましょう。
※前にREGISTRANTS = {}があった位置
db = SQL("sqlite:///froshims.db")
これをインポートしてみましょう。
これはCS50ライブラリのSQLライブラリです。
from cs50 import SQL
SQLを復習しながらデータベースを作成する必要が無いように、私が持ってきた例題からデータベースをコピーしましょう。
このデータベースを取ってきましょう。ほら。
データベースの出来上がりです。
ディレクトリにfroshims.dbというローカルファイルが出来上がりました。
それではまず、このデータベースを見てみましょう。
sqlite3 froshims.db
.schema、Enter。
ここでは車輪を再発明する必要が無いように、事前に作成したデータベーステーブルを紹介します。
registrantsというテーブルを作り、ID列、name列、sport列を設けます。
CREATE TABLE registrants(id INTEGER,name TEXT NOT NULL,sport TEXT NOT NULL,PRIMARY KEY(id));
主キーはそのIDになります。
TEXTフィールドはNOT NULLです。
しかし、これらの構文は全て過去の週にも見たことがあります。
つまり、全ての登録者のID、名前、そしてスポーツを提供するだけのシンプルなテーブルです。
しかし、私はこれからpythonで作業を行います。
application.pyに戻って、これをやってみましょう。
ユーザーを登録する際には、先ほどと同じように名前とスポーツ名を確認し、ユーザーを認証します。
今回はSQLiteを使ってそれを行います。
グローバル変数に保存するのではなく、下記のようにします。
db.execute("INSERT INTO registrants (name,sport) VALUES(?,?)", name,sport)
IDは自動的にインクリメントされるので必要ありません。
またSQLインジェクション攻撃に注意します。
これで必要なものは全て揃いました。
これからpythonでSQLを使って名前とスポーツをデータベースに挿入します。
その後については問題は無いと思います。
しかし、REGISTRANTSという辞書はもう存在しないので、この変数を変更する必要があります。
しかし、大丈夫です。pythonでSQLを見たときのことを思い出すと、次のようなことができます。
※def registrants():の下に追加
registrants = db.execute("SELECT * FROM registrants")
確かにこの単純なクエリは、これまでの全ての登録者にアクセスできるように見えます。
繰り返しになりますが、テンプレート内の変数名がこの変数名と同じであることは少し奇妙に見えます。
return render_template("registrants.html", registrants=REGISTRANTS)
↓
return render_template("registrants.html", registrants=registrants)
しかし、これも右が変数、左がテンプレートで使用する名前だからですが、好きなように呼ぶことができます。
さて、うまくいくかどうか見てみましょう。
flask runを実行して、URLを開いてみましょう。
David、Dodgeballと入力してみます。
Name Sport
{'id': 1, 'name': 'David', 'sport': 'Dodgeball'}
しかし、これは明らかに何かが間違っています。
奇妙な辞書構文のようなものが見えてしまっています。
でもそれはテンプレートを調整していないからです。
そこで、registrants.htmlでSELECT * FROM registrantsで選択する場合、これは行を取得しているようなものだということを思い出してください。
名前はregistrantsですが、実際にはrow行が戻ってきています。
そして、行は単なる辞書であり、各行は列とその値の辞書です。
ですから、registrants.htmlでは、構文を少し変更する必要があると思います。
{% for name in registrants %}
↓
{% for registrant in registrants %}
<td>{{name}}</td>
↓
<td>{{registrant.name}}</td>
<td>{{registrants[name]}}</td>
↓
<td>{{registrant.sport}}</td>
これは今のHTMLやCSSというよりも、SQLやpythonでやったことに近いものがあります。
今、私はすべての行を繰り返し処理しています。
もしお望みならセマンティクスを変えてみましょう。
何週間か前にやったのと同じにしてみましょう。
(registrantsをrows、registrantをrowに変更する)
各行にはnameフィールドがあり、sportフィールドがあります。
では、コードを再実行してみましょう。
David、Dodgeballと登録してみます。
(結果が反映される)
私たちはみんな同じ結果を見ることができています。
データベースは今、とても活発に動いています。
私たちは今、現実のウェブサイトをまさに代表するような本格的なウェブアプリケーションを手に入れました。
SQLを使った独自のデータベースがあり、独自のテンプレートでテーブルを動的に生成しています。
現時点では見栄えは悪いですが、CSSやBootstrapを使って美しくすることができます。
しかし、これで完全に機能するウェブサイトが完成しました。
これ以上追加するとしたら、それは何でしょう?
そうですね。
ウェブサイトから確認のメールが送られてくるのは今や当然のことですが、それができたらクールですよね。
私が数年前に実際に行ったように、最後の機能を追加して、登録者や皆さんにメールを送り、誰かが実際に登録したことを報告するようにしたらどうでしょう?
では、どうすればいいのでしょうか?
ここでapplication.pyに戻ってみましょう。
そして、最後にもうひと工夫してみましょう。
一番上までスクロールして、別のライブラリをインポートします。
flask_mailからmailとmessageをインポートします。
from flask_mail import Mail,Message
繰り返しになりますが、これらのことを行うには、クラスやドキュメント、チュートリアルなどを通じて内容を知る必要があります。
私は事前に調べておいて、どうすればいいのかを確認しました。
それから、ドキュメントによると、プログラムでメールを生成するためにpythonコードを書くには、たくさんの初期化ステップが必要になります。
ユーザー名やパスワード、その他多くの設定をする必要があります。
それではそれらを設定してみましょう。
※app = Flask(name)の下に追加
app.config["MAIL_DEFAULT_SENDER"] = os.getenv("MAIL_DEFAULT_SENDER")
DEFAULT_SENDERとは、デフォルトのEメールアドレスのことです。
例えば、これらのメールをすべて私から送りたい場合は、そのようにすることができます。
app.config["MAIL_DEFAULT_SENDER"] = "malan@harvard.edu"
しかし、その代わりに他の場所にある変数に保存することにしました。
というのも、私は事前にIDを設定して、ユーザー名とパスワードを誰にも見られないようにしていたからです。
そこで、いわゆる環境から値を取得することにしました。
app.config["MAIL_DEFAULT_SENDER"] = os.getenv("MAIL_DEFAULT_SENDER")
この機能はあまり使わないと思いますが、次のPsetで紹介します。
IDEやLinuxシステム、MacやPCなどの他の場所で定義された変数にアクセスするための方法です。
さて、もう少し定義する必要があります。
これはちょっとしたコピペのように見えますが、メールサーバにはたくさんの設定があるので、必要なことなのです。
app.config["TODO"] = TODO
app.config["TODO"] = TODO
app.config["TODO"] = TODO
...
では、一つずつやってみましょう。
メールのパスワード、これも見られたくないので、私の環境からメールパスワードを取得しておきます。
app.config["MAIL_PASSWORD"] = os.getenv("MAIL_PASSWORD")
メールポートが必要なので、587にします。
app.config["MAIL_PORT"] = 587
Gmailを使う事にしましたが、GmailではTCPポート587でメールを送ることができます。これもドキュメントで調べました。
メールサーバは"smtp.gmail.com"です。これはGoogleのドキュメントに書いてありました。
app.config["MAIL_SERVER"] = "smtp.gmail.com"
メールはTLSを使用します。これは暗号化の一種ですが、これをTrueにしてみましょう。
app.config["MAIL_USE_TLS"] = True
最後にメールのユーザー名ですが、私がどんなユーザー名を使っているか知られたくないので、私の環境からユーザー名を取得することにします。
app.config["MAIL_USERNAME"] = os.getenv("MAIL_USERNAME")
これは実は意図的なものです。
皮肉を込めて言っていますが、コードの中にユーザー名やパスワード、あるいは個人情報やセキュリティが必要なものを入力することは、絶対にやってはいけないことです。
なぜなら、もしみなさんが文字通り今すぐ家で試してみたいと思って、自分のGmailのユーザー名やパスワードをこのプログラムに入力して、50回送信したり、50回チェックしたりしたら、どうなるでしょう?
皆さんのユーザー名とパスワードをソースコードに含めることによって、私たちスタッフはもちろん、GitHubやインターネット全体に送信することになるのです。
ここでやっていることをからかっているようですが、環境変数に個人情報を保存することは、実際にはベストプラクティスです。こうしておけば誤って他にコピーペーストする心配はありません。
さて、この赤い丸印に従って、もう1つインポートする必要があります。
osをインポートする必要があります。
os全体をインポートするというのはちょっと大胆な主張ですが、実際にpythonにはOSというライブラリが付属していて、これらの環境変数などにアクセスすることができます。
import os
ふう。
最後にもう一行コードが必要です。
Mailという関数に私のFlaskアプリケーションを渡すためのmail変数です。
mail = Mail(app)
それでは、実際にメールを送信してみましょう。
下にスクロールして、ユーザーを/registrantsにリダイレクトする前、かつ登録手続きの後にこうしてみましょう。
このライブラリからインポートした機能であるmessageを使って、message変数を定義し、
message = Message("You are registered!")
とします。
db.execute("INSERT INTO registrants(name,sport) VALUES(?,?)", name,sport)
message = Message("You are registered!")
return redirect("/registrants")
これを誰に送れば良いか実際にはわかりませんが、それは問題ありません。
index.htmlの内容を変更してみましょう。
ユーザーに名前ではなくEメールアドレスを尋ねるようにしましょう。
フォームを変更して、代わりにEメールアドレスを聞くようにします。
<input autocomplete="off" autofocus placeholder="Name" name="name" type="text">
↓
<input autocomplete="off" autofocus placeholder="Email" name="email" type="email">
そして、application.pyに戻って、第二引数にrecipients=[email]を設定します。
message = Message("You are registered!",recipients=[email])
ここも変えてみましょう。
名前ではなく、フォームからユーザーのEメールアドレスを取得してみましょう。
name = request.form.get("name")
if not name:
return render_template("error.html", message="Missing name")
↓
email = request.form.get("email")
if not email:
return render_template("error.html", message="Missing email")
データベースを変更するのはやめておいて、シンプルにいきましょう。
SQLのようなものは削除して、確認メールを送るだけにしましょう。
これで更にシンプルになりました。
ここではメールだけに焦点を当てます。
recipients変数とともに、件名と宛先を指定してメッセージを作成しました。
message = Message("You are registered!",recipients=[email])
mail.send(message)
これからこのプログラムを実行してみましょう。
フォームがEmailを入力するように変更されています。
ここにジョン・ハーバードのアドレスを入力してみましょう。
(Internal Server Errorと表示される)
すると、Internal Server Errorと表示されました。
びっくりしましたが、理由を見てみましょう。
誤って、まだデータベースを使用してしまっていました。
これを修正しましょう。
もうデータベースを使っていないので、よりシンプルなreturn render_template("success.html")に戻してみましょう。
return redirect("/registrants")
↓
return render_template("success.html")
サーバを再起動してみましょう。
登録されました。
もう1つのブラウザでGmailを開いてみましょう。
Gmailのアカウントはジョン・ハーバードになっています。
受信箱を更新すると、なんとCS50ボットから登録完了のメッセージが届いています。
(You are registered!という文面のメールが届いている)
これで、プログラムによるメール送信が可能になりました。
ダイナミックにメールを生成することができています。
たくさんのことをやりました。
しかし、アイデアは実際にはテンプレートに基づいています。
Controllerロジックやapplication.py、ユーザーの美的感覚やいわゆるViewを配置する、というアイデアに基づいています。
私たちはデータベース、あるいはEメールバックエンドを持ち、データは保存され、この場合には送信され、Modelの概念を実現しています。
SQLiteやEメール、あるいはFroshIMsの機能の使用について、何か質問や不明点、明確にしたい点はありますか?
(特になし)
いいでしょう。
では、なぜステージ上にクッキーが載ったショッピングカートがあるのでしょうか?
そのために5分間の休憩を取りましょう。
戻ってきたらセッションというものについて話し、更に洗練されたウェブアプリケーションを作ります。
さて、戻ってきました。
ウェブプログラミングに関して、基本的に全く触れていない機能があります。
私たちは毎日、どこかのウェブサイトにログインしたり、オンラインで何かを購入したり、動的なウェブサイトを操作したりしています。
これは一般的にセッションsessionと呼ばれる機能です。
Flaskや他のフレームワークや言語で実装されたウェブアプリケーションには、ウェブサーバが皆さんに関する若干の情報を記憶することができるメカニズムが組み込まれています。
この仕組みが無ければ、Gmailやその他のサイトにログインしても、サイトにログインしたことを覚えておいてもらうことができないからです。
考えてみてください。Gmailにログインすると、ユーザー名、パスワード、そして恐らく2ファクタ認証コードを入力して、受信箱が表示されます。
そこから、次にメールを開くときには再度ログインを促されることはなく、何分、何時間、あるいは何日もあなたのコンピュータはサイトにログインしたままになっています。
ブラウザのタブを閉じても、コンピュータをシャットダウンしても、Gmailに戻ってきたときにはログイン状態になっていることが多く、ログインしたことをある意味で記憶しています。
では、これは実際にどのように機能しているのでしょうか?
覆いの下では、gmail.comにアクセスすると、ブラウザはgmail.comのデフォルトルートである/にこのようなリクエストを送信します。うまくいけば受信箱が表示され、ログインしていない場合はログイン画面が表示されます。
GET / HTTP/1.1
Host: gmail.com
...
その際、次のようなレスポンスが返ってくると思います。
HTTP/1.1 200 OK
Content-Type: text/html
...
仮想的な封筒の内側には、ブラウザとサーバがどのように相互運用すべきかを示すヒントとなる、他のHTTPヘッダがあります。
これこそがHTTPの本質なのです。
HTTPは、ブラウザとサーバがどのように相互運用すべきかを規定する規約のプロトコルセットです。
そして、このサーバがクッキーと呼ばれるものを返す場合があります。
HTTP/1.1 200 OK
Content-Type: text/html
Set-Cookie: session=value
...
クッキーについて聞いたことがある人は多いでしょう。
クッキーは悪いもの、危険なもの、でも必要なもの…という感覚を持っていると思いますが、確かにその通りです。
クッキーとは、基本的にサーバが皆さんのコンピュータや携帯電話に仕掛けるデータやファイルのことで、皆さん以前そこにいたことを記憶するためのものです。
ログインしていないウェブサイトでも、携帯電話やラップトップ、デスクトップにクッキーを置くことがあります。
そのため、個々のユーザーを特定することなく行動を追跡することができるのです。
トラッキング・クッキーという言葉を聞いたことがあれば、それが全てを表しています。
ブラウザにインストールされ、ブラウザが記憶する大きな乱数のデータのことです。
サーバからブラウザに送信されるHTTPヘッダの1つは、文字通り「Set-Cookie: session=value」となっており、valueには大きな乱数が含まれます。
HTTPはブラウザとサーバが一緒に使用する(対話するために使用する)プロトコルであるため、ブラウザはgmail.comやその他のサーバにアクセスするたびに、そのクッキーを送り返すように設計されています。
つまり、皆さんがgmail.comにアクセスすると、今から1時間後、タブを閉じた後でも、あるいは明日寝た後でも、ブラウザはこれらのヘッダを送信するだけではなく、皆さんが以前にgmail.comからクッキーを受け取っていれば、ブラウザは似たようなヘッダを送信します。ただし、setという言葉は無く、「Cookie: session=value」となっています。
GET / HTTP/1.1
Host: gmail.com
Cookie: session=value
...
つまり、皆さんの知らないところで、皆さんのブラウザは、皆さんが以前にそこに行ったことがあることをサーバに常に思い出させているのです。
これは現実の世界で、クラブやバー、遊園地などに入ってゲートを通過すると手にスタンプを押してもらうことで中に戻ることができるのと似ています。
この方法では、チケットや身分証明書などを確認することなく、手にスタンプを押すだけで、以前にそこに行ったことがあることを思い出させてくれるのです。
実際、私はこの手に小さなスタンプを押してみます。
ブラウザはHTTPの設計上、別のページにアクセスしたり、別のリンクをクリックしたりするたびに、私の手のスタンプを示すことになっています。
ちょうど、バーやクラブ、遊園地に再び入る時のように、以前に入ったことがあることをドアマンや用心棒に思い出させ、再ログインすることなく通してもらえるようになっています。
それこそがクッキーです。
コンピュータに貼られた仮想のハンコのようなものですが、コンピュータはそれを何度も提示するように設計されています。
ですから、あるウェブサイトで買い物をした後、不気味なことに突然、Facebookやその他のウェブサイトで、そこで買ってもいないものの広告が表示されることがあるとしたら、それは全てクッキーに由来しています。
簡単に言うと、FacebookやGoogleなどが提携している広告会社が、ウェブサイト上に小さな画像やJavaScriptファイルなどを置いていて、それらの広告が別のウェブサイトに掲載されているため、クッキーがある意味で複数のウェブサイトで共有されているのです。
つまり皆さんは常に自分の手のスタンプを様々な第三者企業に提示しているようなもので、こうして彼らは広告を出したり、最悪の場合はトラッキングしたりしているのです。
余談ですが、ブラウザのシークレット・モードやプライベート・モードを使用すると、全てのクッキーが一時的に破棄されます。
いわばクッキー入れが空になるので、ログアウトしているように見えるのです。
実際、授業中にも何度かシークレット・モードを使ったことがありますが、それは以前のクリックや以前のデモが将来のデモを台無しにしないように新たなスタートを切りたいからです。
つまりそれはクッキーとそれに伴う全ての状態を取り除くためでした。
Cookieは非常に重要で強力な仕組みですが、言うまでもなく、人のプライバシーやセキュリティに関して、悪用される可能性があります。
ということで、今回はクッキーの良い部分に焦点を当て、一般的なウェブサイトがどのようにして皆さんのログイン状態を記憶しているのかを考えてみましょう。
この非常にシンプルなアイデアを、コードを使って簡単に実現できることを明らかにしましょう。
では、次のようにしましょう。
人々をログインさせるプログラムを実演するために、loginという新しいディレクトリを作りました。
先ほどと同じレイアウトで、タグを1つ追加しました。
▼layout.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1,width=device-width">
<title>login</title>
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
基本的にはUnicodeを使用していることをブラウザに伝えるのがベストな方法です。
<meta charset="utf-8">
そうすれば絵文字やその他の文字をHTMLの中に入れることができます。
これはBootstrapのドキュメントや他のサイトで見られるもので、これでこのページの出発点のようなものができました。
しかし、実際にはコンテンツは無く、ここで定義されているのはただのブロック、block bodyだけです。
つまり、先ほどと同じレイアウトになっています。
そしてlsを入力すると、application.pyとtemplatesフォルダが表示されます。
そのtemplatesフォルダには今のところlayout.htmlが入っています。
それでは先に進み、ユーザーにログインできるフォームを提示し、ログインしたことを記憶させることを目的とした簡単なアプリケーションを書きましょう。
flaskからFlaskをインポートしてみましょう。
また、redirect関数、render_template関数、request関数、そしてsession関数をインポートしてみましょう。
from flask import Flask, redirect, render_template, request, session
sessionは新しい関数です。
そしてflask_sessionからSessionをインポートします。
from flask_session import Session
ここでapp = Flask(name)とします。
app = Flask(name)
そして、ドキュメントから更に数行のコードが必要です。
app.config["SESSION_PERMANENT"] = False
繰り返しになりますが、私が使用しているflask_sessionというライブラリにのみ特有のもので、このライブラリは基本的に私のアプリケーションでハンドスタンプを有効にするものです。
そして文字通りファイルシステムを使用するように設定します。
app.config["SESSION_TYPE"] = "filesystem"
ファイルシステムはハードドライブや私自身のIDEアカウントを意味し、データベースなどの代替手段となります。
次は単なる定型文です。
Session(app)
文字通り、このライブラリのドキュメントからこの3行をコピーしました。
app.config["SESSION_PERMANENT"] = False
app.config["SESSION_TYPE"] = "filesystem"
Session(app)
しかし、このライブラリを使うためには、先ほどのメールライブラリにも言えることですが、もう1つ指摘しておくべきファイルがあります。
ここでまだ作成していないもう1つのファイル、requirements.txtを作成しておきます。
それをloginディレクトリに置きます。
このアプリケーションはFlaskを必要とします。
Flask-Sessionというライブラリも必要です。
requirements.txtは必要なものや使いたいライブラリをシンプルにリストアップしたものです。
▼requirements.txt
Flask
Flask-Session
そして、これらの要件をインストールするために、コマンドラインで使用するコマンドがあります。
今回のPsetでは通常このようなことをする必要はありませんが、念のため説明しておくと、pythonアプリケーションの要件をインストールする場合、通常次のようなコマンドを実行します。
※コマンドラインで
pip install -r requirements.txt
事前にやっておいたので今はやりませんが、ライブラリをインストールするためにはC言語よりもpythonの方がはるかに簡単だということを知っておいてください。pipのような技術を使って、より容易にパッケージ化することができます。
それではいくつかのルートを作ってみましょう。
@app.route("/")とし、def index():として先ほどと同様に定型文を作成し、デフォルトでindex.htmlのレンダリングテンプレートを返すようにしましょう。
@app.route("/")
def index():
return render_template("index.html")
そしてもう1つのルートを作ります。
@app.route("/login")
def login():
return render_template("login.html")
今のところはシンプルにしておきましょう。
次にこのフォームを実装してみましょう。
login.htmlというファイルを作成して、ログインフォームを実装します。
ログインフォームにはユーザーの名前を入力するスペースが必要です。
現実世界では、ユーザー名とパスワードを使うことが多いと思いますが、ここではシンプルに、アイデアの本質だけに焦点を当てて、ユーザー名だけを使うようにします。
login.htmlでは、先ほどと同様にlayout.htmlを拡張します。
{% extends "layout.html" %}
block body、endblockタグを用意して、その中にフォームを入れ、アクションを/login、メソッドをpostにします。
{% block body %}
<form action ="/login" method="post>
</form>
{% endblock %}
ここではベストプラクティスを採用しています。現実世界でのユーザー名とパスワードが、人々のオートコンプリート履歴に保存されるのは避けたいからです。
inputは以下のようにします。オートコンプリートは念のためオフ、オートフォーカスは便利なので設定します。nameはユーザーの名前とし、先ほどと同様プレースホルダは名前と同じにして、タイプはテキストとします。
<input autocomplete="off" autofocus name="name" placeholder="Name" type="text">
そして、その下のinputのタイプはsubmitとします。
<input type="submit">
これまでの登録などのフォームとほぼ同じですが、今回はログインのために使いたいと思います。
では、簡単なチェックをしてみましょう。
flask runを実行し、URLを開いてみると…最初のエラーは、「テンプレートが見つかりませんでした」
OK。index.htmlを作成していないことはなんとなくわかっていたので、今のところはこのエラーで大丈夫です。
まず、loginルートに行ってみます。
(URLの末尾に/loginを付けてリロードすると、フォームが表示される)
これはうまくいっているようです。
それではindex.htmlという存在しないファイルを作成して、loginフォルダのtemplatesに入れてみましょう。
キー入力の手間を省くためにコピペしますが、まだ実装していないので、ここでは「あなたはまだログインしていません」と表示します。
▼index.html
{%extends "layout.html" %}
{% block body %}
You are not logged in.
{% endblock %}
Flaskを再起動してみましょう。
You are not logged in.と表示されました。
/loginに行くと、ログインフォームが表示されています。
そこで、もう少しインタラクティブにしてみましょう。
index.htmlに行って、
You are not logged in.<a href="/login">Log in</a>.
と言ってみましょう。
より良いユーザーインターフェースを作りましょう。
Flaskを再起動し、ウェブページに戻ってリロードを押すと、原始的なユーザーインターフェースがあります。
ごくシンプルですが、非常にウェブサイトに似ています。ウェブページが表示されると、ログインしていないものの、ログインするためのリンクやボタンなどが表示されます。
これをクリックすると、リンクのデフォルトである/loginへの単純なgetリクエストが実行されます。
これでほぼ完成です。
ほとんどログインできるようになりました。
この情報を使って何かしてみましょう。
それではapplication.pyに戻って、ユーザーがログインしたときに、正確には何をさせたいのでしょうか?
ユーザーがログインしたら、最初に、私はpostをサポートしたいと思います。
@app.route("/login", methods=["POST"])
プライバシーのためにそうしましょう。
そして、これをやってみましょう。
以前と同様、request.methodがPOSTの場合、ユーザーがログインしたことを記憶しておきましょう。そして、ユーザーを/にリダイレクトします。
def login():
if request.method == "POST":
#Remember that user logged in
#Redirect user to /
return render_template("login.html")
それ以外の場合はログインフォームを表示します。
まだ終わっていません。
しかし、これは重要なアイデアです。
ユーザーがログインしたことをどうにかして覚えておく必要があります。
では、どうすればいいのでしょうか?
まず、以下のようにしてユーザーの名前を取得します。
name = request.form.get("name")
今のところ、それを検証することはありませんが。
そして、ユーザーの名前をどうしたいのでしょうか?
その名前をセッションに保存したいのです。
では、セッションとは一体何でしょうか?
この手に押すスタンプは、コートチェックに似ています。
例え話になりますが、高級レストランでコートを誰かに渡すと、その人はコートをクローゼットに入れてから番号を返してくれますよね。
そうしないと誰もが同じコートを受け取ることになってしまいます。スマイルマークだけではダメですが、何らかの識別子を渡して、コートをハンガーやバケツ、クローゼットのどこかのスペースに入れるのです。
では、なぜこのことがここで重要なのでしょうか?
ウェブブラウザの世界でも同じことが起こっています。
固有のクッキーを取得すると、固有のハンガーが与えられるのです。
この比喩を事前によく考えておくべきでしたが、あなたはコートを含めた全ての荷物が入る固有のハンガーを与えられているのですが、この場合は覚えておきたい変数も含まれています。
つまり、Flaskのおかげで私がアクセスできるのは、(実際にはHTTPのおかげなのですが、)sessionという特別な変数です。
これは特殊な変数で、一方ではグローバル、つまりどこでも使えるということですが、興味深いのは、現在のユーザーに結びついているということです。
つまり、今Zoomにいる全員が私のURLにアクセスしたとしても、それぞれがsessionという1つの変数のコピーを取得しているということです。
これはクッキーの仕組みを使って実装されています。
皆さんが私のアプリケーションを訪れたとき、皆さんはそれぞれ異なるクッキー値を取得しています。
このクッキー値はクローゼットの中の個別のハンガーにマッピングされているようなもので、私のデータはこのハンガーに、あなたのデータはこのハンガーに、というようになっています。
この比喩はうまく問題を言い表しているようです。
ハンガーには私たちの個々のデータが全て格納されることになります。
何を保存したいのでしょうか?
これは簡単で、ユーザーの名前を保存することで、私がログインしたこと、ブライアンがログインしたこと、ソフィアやライアン、その他の誰かがログインしたことを覚えておくことができます。
しかし、この最初の行で強調したsessionグローバル変数は、ユーザーとしての各人が自分のバージョンを取得できるように、Flaskによって実装されることになります。
そこで、これをpythonの辞書として使うことにします。
ここでsession["name"]=nameとして、ユーザーの名前をsessionに追加します。
session["name"]=name
そして、この変数はもう必要ありません。
name = request.form.get("name")
↓
session["name"] = request.form.get("name")
これをもう少し詳しく説明しましょう。
ハンガーのように、ユーザーがログインしたときの名前をキーにして、値をセッションに保存してみましょう。
そして、リダイレクトの方法も知っています。
return redirect("/")
これで完成だと思います。
▼application.py
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():
return render_template("index.html")
@app.route("/login", methods=["POST"])
def login():
if request.method == "POST":
# Remember that user logged in
session["name"] = request.form.get("name")
# Redirect user to /
return redirect("/")
return render_template("login.html")
ユーザーがログインしたことを記憶するには、これで十分です。
ユーザーの名前をセッションに入れていますが、この値をチェックする必要があるのは当然のことです。
では、index.htmlを改善してみましょう。
今のところ、ただやみくもにYou are not logged in.と言っています。
ただ単にハードコードするのではなく、実際に判断してみましょう。
def index():
if not session.get("name"):
return redirect("/login")
つまり、セッションに名前が無いということは、あなたがまだ私に名前を教えていない、ということです。
/loginにリダイレクトすることで、強制的に名前を教えてもらうことにします。
そうでなければ、index.htmlをレンダリングします。
さて、それでは更に何をすればいいのでしょうか?
ここでindex.htmlに行ってみましょう。
ただ単純にハードコーディングするのではなく、今できることを見てみましょう。
Jinjaの構文では、{}中括弧やブロック構文ではなく、forループでもなく、if条件を使うことができます。
もしsession.nameが存在すれば、「あなたは{{session.name}}としてログインしています。」と言います。
その後にログアウトへのリンクのようなものがあるかもしれません。
まだ存在していませんがこれから作ります。
{% if session.name %}
You are logged in as {{session.name}}.<a href="/logout">Log out</a>.
また、session.nameが無い場合は、ユーザーはログインしていないと仮定して、「あなたはログインしていません。ログインしてください。」とします。
{% else %}
You are not logged in.<a href="/login">Log in</a>.
{% endif %}
この方法が正しければ、サーバを再起動してみます。
(Method Not Allowedと表示される)
URLが違っていました。
いや、これは正しかったです。/loginにいますが、メソッドが許可されていません。
サンティアゴが既に指摘してくれていましたが、過ちを繰り返してしまいました。
application.pyに戻ってみましょう。
ここでは複数のメソッドをサポートする必要があります。
@app.route("/login", methods=["POST"]
↓
@app.route("/login", methods=["GET","POST"]
必要に応じてテンプレートを実際にレンダリングできるようにGETもサポートしたいと思います。
では、サーバを再起動してみましょう。
(フォームが表示される)
良いでしょう。/loginにいます。
何が起こるか見てみましょう。
URLの末尾を/に変えてみると、また/loginにリダイレクトされるのがわかります。
先週やったことをやってみましょう。
inspect(検証)を開いてみましょう。
ネットワークタブを開いて、もう一度/にアクセスして、Enterを押してみましょう。ほら。何が起きているかわかります。
このリクエストにアクセスして、chromeのネットワークタブを拡大し、レスポンスヘッダまでスクロールすると、302 Foundが表示されていることに気付きます。
これは301のようなリダイレクトコードで、行き先はここを見ると/loginです。
このように、HTTPのすべての仕組みが機能しています。
ではDavidとしてログインして、submitをクリックしてみましょう。
(You are logged in as Davidと表示される)
素晴らしい。
何が起こっているのか注意してください。
私は今/ルートにいます。
if not session.get("name")
↑この値はTrueなので、これは適用されません。従って、index.htmlを見ています。
ここでlogoutルートを作りましょう。
@app.route("/logout")とし、def logout():とします。
そして、ここではユーザーがしたことを忘れる必要があります。
これにはいくつかの方法があります。
今のところ、最もシンプルな方法でやってみましょう。
session["name"]=Noneとして、nameをNoneに変更します。
そして、リダイレクトを/に戻します。
session["name"] = None
return redirect("/")
これでどうなるか見てみましょう。
サーバを再起動します。
(再起動してもまだYou are logged in as Davidと表示される)
ここではまだログインしているので、ログアウトしてみます。
(フォームに戻る)
OK。例えばBrianとしてログインしてみましょう。登録。
(You are logged in as Brianと表示される)
リロードしたり、タブを閉じたり、再びタブを開いたりしてもログインしたままです。
ブラウザがクッキーを提示しているおかげで、全てが記憶されているのです。
そして、それを実際に見ることができます。
inspectに行って、ネットワークタブに行って、同じページをリロードしてみましょう。
この一番上のレスポンスに注目してみましょう。
200では、ここでリクエストヘッダまでスクロールダウンすると、ブラウザが/をリクエストしていることに気付きます。
しかし、下に下にスクロールすると、知らない間にブラウザはクッキーの値が35456…であると言っています。
一般的に、他人に自分のセッションを教えることは良くありません。
というのも、皆さんが私の振りをして私のセッションを乗っ取ることができてしまうからです。
もちろん、このアプリケーションでは何もできませんが。
しかし、あなたが大切にしているアカウントではこのようなことはしないでください。
誰もあなたのクッキーの値を見るべきではありません。
実際にクッキーの値が表示されていますが、これは私が最初に訪れたときにサーバから送られてきたものです。
何か質問はありますか?
ブライアン「クッキーは永遠に存在するのですか?それとも消えてしまう時があるのですか?」
↑良い質問ですね。
クッキーには時間制限があるのでしょうか?
それは、あなたがどう設定するかによります。
つまり、このクッキーには何分、何時間、何日といったデフォルトの期限が設定されており、それを過ぎるとサーバが更新してくれない限り、期限切れとなります。
永遠に残るクッキーを設定することもできますが、恐らくログインという概念のためにはしたくないでしょう。
というのも、私たちの誰もが研究室のコンピュータや友人のコンピュータから離れた後、ログアウトするのを忘れた経験があるからです。
少なくとも、最終的にはこのサイトから自動的にログアウトされることを知っているのは良いことです。
これはセッションクッキーと呼ばれるもので、最終的には期限切れとなります。
しかし、ウェブサイトを訪れたときに便利なのが、ウェブサイトにある「ログイン情報を記憶する」などのボタンです。
パーマネントクッキーを設定すると、単にランダムな値をコンピュータに保存するだけでなく、その後のフォームに入力される情報を実際に保存することができます。
これはカスタマイズしたウェブサイトの好みの設定を記憶するのに便利で、ナイトモードやデイモードなどの設定を記憶することができます。
また、CS50のウェブサイトでもあなたの選択を記憶しています。
CS50のウェブサイトにログインすると、ハーバード大学やイェール大学のログイン画面が表示されることがあります。
これまで気付かなかった人もいるかもしれませんが、せっかく作ったのに残念なことです。前回ハーバードを使ったか、イェールを使ったかを記憶しているので、常にドロップダウンを操作する必要はなく、あなたの好みを記憶しています。
これにはクッキーを使用しています。
自宅での練習として、もし興味があれば、ブラウザのネットワークタブを開き、ヘッダを見ると、今日の全てのウェブサイトがどのように機能しているかを推測することができます。
実際にはクッキーを使った実装は難しくなりつつあります。というのも、多くのブラウザメーカーがようやくサードパーティークッキーと呼ばれるものを無効にし始めたからです。
サードパーティークッキーとは、gmail.comやFacebook.comではなく、人の追跡によく使われる他のサーバから送られてくる広告スタイルのクッキーのことです。
最近では、特にEUやアメリカでは「I accept cookies」や「I accept the necessary cookies」などのボックスをクリックしなければならなくなりました。
正直、これは良いアイデアですが、少し馬鹿げています。というのも、今や世界は私たちに意識することなくただひたすらボックスをクリックするように仕向けているからです。
しかし、実際にはボックスをクリックすることで、手にスタンプを押されることを許可しているのです。
では、クッキーについて、あるいはこのログインメカニズムの実装方法について、何か質問はありますか?
ブライアン「特にありません。」
それでは、これをやっておきます。
この例では、本格的なeコマースサイト、ある種の店舗を検討します。事前に成功した例を用意しておきます。
これを文字通り「ストア」と呼ぶことにしますが、今回はゼロから書くのではなく、ブラウザでstoreというディレクトリから開いてみましょう。
これはいつものようにコースのウェブサイトで利用できます。
storeに入って、application.py、requirements.txt、そしてlayout、books、cartなどのテンプレートを開いてみましょう。
これは他の人が書いたコードを読む良い練習になります。
複数のファイルから成っています。
このようなプログラムをダウンロードしたとき、あるいはCS50やその他のコースの配布コードをダウンロードしたとき、どのように頭の中を整理すれば良いでしょうか?
繰り返しになりますが、これらの異なるファイルが果たす役割を理解していれば、application.pyがcontrollerで、HTMLファイルやtemplatesはviewですから、パンくずをたどって、これらのファイルで何をするのかを理解することができます。
それではapplication.pyから始めましょう。
CS50のライブラリをインポートしていることに注目してください。
それに続くのはセッションサポートを含むFlaskの機能です。
from cs50 import SQL
from flask import Flask, redirect, render_template, request, session
from flask_session import Session
その下では以前行ったようにアプリケーションを初期化しています。
Configure app
app = Flask(name)
その下では、このケースではbooksという店舗の商品全体を含むデータベースに接続しています。
Connect to database
db = SQL("sqlite:///store.db")
セッションを設定していますが、このコードは以前と同じです。
Configure sessions
app.config["SESSION_PERMANENT"] = False
app.config["SESSION_TYPE"] = "filesystem"
Session(app)
そして、このウェブサイトが何をしようとしているのかを推測することができると思います。
デフォルトの/ルートはindex機能を持っていて、データベースにbooksというテーブルがあるようなので、booksから*を選択して全ての本を取得しています。
そしてbooks.htmlというテンプレートをレンダリングして、booksを渡しているようです。
@app.route("/")
def index():
books = db.execute("SELECT * FROM books")
return render_template("books.html", books=books)
では、このパンくずをたどって、books.htmlに入ってみましょう。
books.htmlはlayout.htmlを拡張しているようです。
{% extends "layout.html" %}
さて、layout.htmlを見てみましょう。
特に面白いことはありません。以前に見たことのある定型的なプレースホルダコードだけです。これは閉じて、もう見ないことにします。
しかし、books.htmlを見ると、面白いことがわかります。
3行目と14行目の間にblock bodyがあります。
その中にはページの一番上にbooksと書かれたh1タグがあります。
<h1>Books</h1>
そして、テンプレートの中にforループがあります。
{% for book in books %}
ここでもJinjaの構文を使っていますが、その下にh2タグがあり、本のタイトルが書かれているようです。
<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>
hiddenの意味はまだわかりませんが、add to cartの値を持つsubmitボタンがあります。
つまり、amazon.comのある種のオンライン書店のためのショッピングカタログを実装しているように思えます。
では、application.pyに戻って、cartを見てみましょう。
cartはgetとpostの両方をサポートしているようです。
@app.route("/cart", methods=["GET", "POST"])
つまり、前に見たように同じルートを使って2つの異なるものを処理しているのかもしれません。
この関数はcartと呼ばれていますが、それは理にかなっています。
def cart():
これはセッションを初期化するためのもので、また別の機会に紹介します。
# Ensure cart exists
if "cart" not in session:
session["cart"] = []
そして、このプログラムにはもうあまり何もないように見えます。
GETがHTTP動詞の場合は、まず下の方に注目しましょう。
# GET
books = db.execute("SELECT * FROM books WHERE id IN (?)", session["cart"])
return render_template("cart.html", books=books)
ということでif条件は無視して、何をしているかというと、IDがリストにあるbooksから*を選択しています。
さて、このbooksのリストはどこから来ているのでしょうか?
session["cart"]です。
先ほどの例ではセッションを使って、あなたの名前や私の名前、そして全員の名前をユーザー毎に保存しました。
今度はショッピングカートと呼ぶものを保存するために使います。
シアターの友人たちのおかげでショッピングカートがここにあるのは、そのためです。
このようにして、ショッピングカートはウェブサイトに実装されています。
覆いの下にあるクッキーを使って、何でも保存できるユーザー固有の変数や辞書のように見せかけているのです。
つまり、私のsession["cart"]キーは、たくさんの本のIDと同じ値を持つことになりそうです。
本がどのように存在しているかはまだわかりませんが、私がそう解釈していると仮定しましょう。
そしてここではcart.htmlというテンプレートをレンダリングして、データベースからbooksを渡しています。
では、どうやって本をセッションに入れるのでしょうか?
さて、ユーザーがカートにフォームを送信した場合、このコードが関係してくると思います。
# POST
if request.method == "POST":
id = request.form.get("id")
if id:
session["cart"].append(id)
return redirect("/cart")
ユーザーが/cartにPOSTした場合、ユーザーが入力したIDを取得することになるようです。
if request.method == "POST":
id = request.form.get("id")
そして実際にIDがあり、それがNoneでない場合、その本のIDをカートに追加し、ユーザーをカートにリダイレクトします。
if id:
session["cart"].append(id)
return redirect("/cart")
これで十分複雑になったので、実際に何が起こっているのかを見るべきだと思います。
それでは、実際にやってみましょう。
flask runを実行してみましょう。
(ハリーポッターシリーズのタイトルが複数表示され、それぞれにAdd to Cartボタンが付いている)
OK。きれいなウェブサイトとは言えませんが、私のデータベースには7冊のハリーポッターが入っているように見えます。
そして、それぞれに「カートに入れる」というボタンがあることに注目してください。
HTMLソースを見て、books.htmlテンプレートが生成したものを見てみましょう。
<h1>Books</h1>
<h2>Harry Potter and the Sorcerer's Stone</h2>
<form action="/cart" method="post">
<input name="id" type="hidden" value="1">
<input type="submit" value="Add to Cart">
</form>
<h2>Harry Potter and the Chamber of Secrets</h2>
<form action="/cart" method="post">
<input name="id" type="hidden" value="2">
<input type="submit" value="Add to Cart">
</form>
...
ちょっと面白いですね。
一番上にはh1のbooksがあります。
その次に本のタイトルが書かれたh2の最初の部分があります。
これはキーボードでは簡単に入力できない記号を出力することができる、HTMLエンティティのひとつです。
Sorcerer's
これは動的に出力した最初のフォームです。
<form action="/cart" method="post">
<input name="id" type="hidden" value="1">
<input type="submit" value="Add to Cart">
</form>
先ほどhiddenとは何かわからないと言いましたが、これは名前通りの機能を持ちます。
これは、ユーザーから視覚的に隠されたフォームでデータを送信することを可能にします。
ユーザーが入力できるテキストボックスではありませんが、そこにはボックスが存在します。
面白いのは、ここに1が代入されていることです。
<input name="id" type="hidden" value="1">
でも、次のフォームでは2、その次は3に値を設定しています。
なぜでしょうか?
ちょっとだけIDEに戻って、SQLiteをstore.dbで実行してみましょう。
.schemaで内容を見ると、このデータベースにはbooksテーブルがあり、それぞれのテーブルにはIDとタイトルがあります。
CREATE TABLE books(id INTEGER, title TEXT NOT NULL, PRIMARY KEY(id));
SELECT * FROM booksを実行すると、本のタイトルだけでなく、発売された順に適切に番号が振られたユニークなIDが表示されます。
(idとtitleの2列が表示される)
さて、ここでターミナルを少し下に移動してみましょう。
Flaskを再起動して、これらのフォームを送信するとどうなるか見てみましょう。
最初の本をカートに入れます。
(/cart、cart.htmlに繋がり、カートに入れた本のタイトルが表示される)
面白いですね。
/cartにリダイレクトされ、注文済リストを表示するolタグがあることがわかります。
カタログを見てみましょう。
「アズカバンの囚人」はなかなか良かったと思います。
それをカートに入れてみましょう。
(カートに「アズカバンの囚人」が追加される。)
カートに入っているものが正確に表示されています。
では、これはどこから来ているのでしょうか?
コードを見てみると、request.method == "POST"の場合の下にあるこのロジックは、ID1、ID3を私のsession["cart"]キーに追加しています。
if request.method == "POST":
id = request.form.get("id")
if id:
session["cart"].append(id)
さて、その["cart"]キーはどこから来たのでしょうか?
これは私が提案したもう1つの部分です。
if "cart" not in session:
session["cart"] = []
コードの先頭でカートを初期化する方法が必要なのです。
簡潔に言うと、誰かがそれを置かない限り、セッション内にcartというキーは存在しません。
そこに置く必要があります。
だから、もしcartがまだセッションに無い場合は、pythonの空のリストに初期化します。セットでも辞書でもOKです。
私はリストというシンプルな方法を選びましたが、これがカートの中身を記憶する方法です。
これまでの例とは異なり、皆さんは自分のカートに物を入れようとしていると思いますが、私たちはそれぞれ異なるショッピングカートを見ています。
みんなそれぞれ違うものをカートに入れています。
他の人が本をカートに追加しようとしても、それぞれのカートに追加されるだけで、私のカートには追加されません。
これが、Amazonにアクセスしてカートに何かを追加する際に、覆いの下で起こっていることの全てです。
Amazonには確かに他の機能もあります。
数量を変更できたり、欲しいものリストに追加できるのも良いことです。
しかし、HTMLや、行き来するネットワークリクエストを見てみると、私たちが当たり前のように使っているこれらの機能は、比較的シンプルなHTMLフォームと入力とボタン、そしてサーバ上のセッションで構築されていることがわかります。
Amazonは別の言語、別のフレームワークを使っているかもしれませんが、これらのアイデアはすべて同じです。
ですから、今週のテーマは、数年後には恐らく時代遅れになっているであろうFlaskを教えることではありません。
HTTPのリクエストとレスポンス、クッキー、セッション、そしてそれらの上に構築することができる機能についてのアイデアと原則を教えることです。
さて、最後の例題ですが、先週学んだJavaScriptを使ってみましょう。
今までは、サーバ上でpythonを使って動的にHTMLを生成してきました。
今日の私のデザインはひどいものでしたが、CSSを追加したり、クラスやBootstrapを追加したりして、フォームを綺麗にすることはできます。しかし、それは先週と比べて新しいことではありません。
先週はプログラミング言語としてJavaScriptを扱いましたが、それは少なくとも私たちの使い方による限り、クライアントサイドのプログラミング言語でした。
それは、コード、条件分岐、関数、ループなどをサーバではなくユーザーのブラウザ上で実行させるための手段でした。
私たちはpythonを再導入することで、サーバで何かをする手段を得ましたが、最近のウェブが本当に面白いのは、ユーザーが見る、いわゆるフロントエンドと、サーバであるバックエンドが結びついていることです。
フロントエンドとは、私たちが操作するユーザーインターフェースのことで、バックエンドとは、人間には見えないがプログラマである皆さんには見えるpythonコードやSQLコードのことです。
では、この2つのコンポーネント、つまりフロントエンドとバックエンドを組み合わせるにはどうしたらいいのでしょうか?
では、次のようにしてみましょう。
テレビ番組の古いデータベースを持ってきて、このフォルダの中に何が入っているのかを簡単に見てみましょう。
sqlite3 shows.db、.schemaとします
CREATE TABLE shows(id INTEGER, title TEXT NOT NULL, PRIMARY KEY(id));
サイズを小さくするためにテレビ番組のID、タイトルだけにしています。
pythonとJavaScriptを使って大量のデータをやり取りすることになるので、タイトルだけに集中するために、視聴率や脚本家、監督などの情報は全て削除しました。
さて、事前に作ったものを開きます。
showsフォルダの中にあります。
ここにはapplication.py、requirements.txt、そしてテンプレートであるindex、layout、searchがあります。
たくさんのファイルがありますが、これまで見てきたものほどではありません。
先に進みましょう。さっきはちょっとズルをしました。
requirements.txtを見ていませんでした。
念のため見ておきましょう。
以下はrequirements.txtに含まれる他のライブラリの例です。
cs50
Flask
CS50ライブラリとFlaskです。
繰り返しになりますが、これらはIDEにプリインストールされていますが、冒険して自分のMacやPCでこれらを実行しようとしたときに、必要な設定がすべてできるように、このようにしています。
さて、先ほどと同じように、application.pyから始めましょう。
最初にこれを実行し、実際にアプリケーションが動いているのを見て、どのように動作するのかを理解しましょう。
ここにアクセスして、フォームにofficeと入力してsearchをクリックしてみましょう。
(officeがタイトルに含まれる番組が全て表示される)
OKです。
すると、officeに言及している全ての番組の箇条書きのリストが表示されます。
ここでズームアウトして、URLを見てみましょう。
(URLの末尾が/search?q=officeになっている)
フォームに戻ってみて、/ルートにいることを確認しておきます。
繰り返しますが、/は存在しますが、最近のchromeはデフォルトで隠しているだけです。
officeなどを検索してEnterを押すと、/search?q=officeとなります。
これはGoogleからインスピレーションを得ました。
先週はGoogleへのフロントエンド、つまりHTMLフォームを実装しただけで、あとはGoogleに猫や犬を探してもらったりして、ちょっとズルをしていました。
しかし、pythonを使えば、アプリケーションの両サイドを実装することができます。
このソースコードを見ると、office関連の全てのテレビ番組のタイトルを表す、非常に大きな順不同のリストを返しているように見えます。
これはこれで良いのですが、これは昔のウェブプログラミングのように、フォームに入力してEnterキーを押すだけのものです。
しかし、最近のウェブサイトでは、いつでも多くのことが同時に起こっています。
オートコンプリートが使われていたり、チャットメッセージが表示されていたり、地図が自動的に動いていたり、Uberの小さな車が同時に動いていたりします。
Web2.0とは、技術的な用語というより流行語のようなもので、今日のウェブのよな、ユーザーとコンピュータの間でのますますダイナミックなウェブ上のやり取りを意味します。
JavaScriptとpythonなどの独自のバックエンドを使えば、このような、よりダイナミックなインターフェースを構築することができます。
もし私がo、f、f、i、c、eと入力すると、すぐに結果が表示されるとしたら、素晴らしいことだと思いませんか?
これはもうすでに見たことがあります。
先週、私がAで始まる全ての単語を検索し始めたことを思い出してください。
Pset5の中の、14万語の大きな辞書を使いました。
このテレビ番組のタイトルのデータベースは数メガバイトの大きさです。
必ずしも番組のデータベース全体をブラウザに送りたいとは思いません。
理由の一つ目はパフォーマンスとサイズの問題、そして多分二つ目は知的財産権の問題です。
全てのユーザーに私のデータベース全体を送りたいとは思わないのです。
ユーザーにログインしてもらったり、サービスに課金したりして、提供するデータをもう少しコントロールしたいと思うこともあるでしょう。
それを実現しましょう。
この番組のアプリケーションを、実際にもっと動的に動作するように強化してみましょう。
まずはその仕組みを理解しましょう。
ここにはcs50とFlaskという2つのライブラリがあります。
from cs50 import SQL
from flask import Flask, render_template, request
ここではFlaskを設定しています。
app = Flask(name)
そして、ここでデータベースを設定しています。
db = SQL("sqlite:///shows.db")
その下に、index.htmlをレンダリングする非常にシンプルなルートがあります。
@app.route("/")
def index():
return render_template("index.html")
見てみると、非常にシンプルで、先週や今日、これまで扱ったものに似たフォームがあります。
<form action="/search" method="get">
<input autocomplete="off" autofocus name="q" placeholder="Query" type="search">
<input type="submit" value="Search">
</form>
ここにはname="q"とありますが、その他は全て定型的なものです。
では、このファイルを閉じましょう。
layout.htmlもお決まりのもので、以前のものに似ています。
このファイルも閉じて、index、application.pyに戻りましょう。
肝心な部分は/searchルートのようです。
先週、犬や猫を探すのにGoogleを使った際に欠けていた部分です。
@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)
ここでは/searchというルートがあり、そのルートで呼ばれるsearchという関数があります。
これはSQLとpythonの良いとこ取りのようなものです。
showsという変数を用意して、これを実行すると戻ってくる全ての行の結果を代入します。
shows = db.execute("SELECT * FROM shows WHERE title LIKE ?", "%" + request.args.get("q") + "%")
"SELECT * FROM shows WHERE title LIKE ?"
さて、?の部分に何をプラグインすればいいのでしょうか?
"%" + request.args.get("q") + "%"
一見するとわかりにくいですが、ユーザーの入力qをrequest.args.getから取得しているようです。formの代わりにargsを使っています。
ユーザーのqの値を取得しているのです。私の場合はofficeでした。
ちょっと確認したいのですが、左右の%記号は何でしょうか?
今日はたくさんの%記号を見てきましたが、これらは違います。
チャットでも構いません。
これらの%記号は、この文脈で何を表しているでしょうか?
ブライアン「みんな、SQLのプレースホルダだと言っています。」
↑そう。SQLワイルドカードのプレースホルダです。
つまり、「左に0文字以上、右に0文字以上の文字が入っている」ということです。
このため、タイトルの最初のoffice、最後のoffice、途中のoffice、全てにマッチするのです。
もっと厳密に分析したければ、%記号をなくして完全に一致するものだけを要求することもできますが、この実装ではそれは目的とはしていません。
これらの行を全て取得したら、return render_template("search.html")で、これらの番組を全て渡しています。
return render_template("search.html", shows=shows)
最後に見るべきものはsearch.htmlです。
{% extends "layout.html" %}
{% block body %}
<ul>
{% for show in shows %}
<li>{{ show.title }}</li>
{% endfor %}
</ul>
{% endblock %}
とてもシンプルですが、1週間前には非常に難解に見えたことでしょう。
率直に言って、今でも難解に見えるかもしれません。
しかし、自分でこの構文を使い始めると、「ああ、これはただのテンプレートで、このテンプレートに渡された番組のタイトルを表す、liタグの順不同のリストを動的に出力できるようにしているだけなんだ」とわかるでしょう。
さて。では、最後の仕上げです。
このアプリケーションを改良して、以前と同じようにフォームを送信するのではなく、全てのロジックがJavaScriptとバックエンドの間で行われるようにしてみましょう。
最初にこれをやってみましょう。
application.pyに戻り、ここではなく、次のようにしてみます。
テンプレートをレンダリングするのではなく、jsonifyという新しい関数を使います。
def search():
shows = db.execute("SELECT * FROM shows WHERE title LIKE ?", "%" + request.args.get("q") + "%")
return render_template("search.html", shows=shows)
↓
def search():
shows = db.execute("SELECT * FROM shows WHERE title LIKE ?", "%" + request.args.get("q") + "%")
return jsonify(shows)
ちょっと面白い名前ですが、インポートする必要があります。
from flask import Flask, render_template, request
↓
from flask import Flask, jsonify, render_template, request
jsonifyあるいはjsonは、JavaScript Object Notation(JavaScriptオブジェクト記法)を意味しています。
それがどのようなものかはすぐにわかるでしょう。
何年も前に、世界は、非常に軽量なテキストフォーマットであるjson、JavaScript Object Notationを使って、サーバとブラウザの間でデータを送信するためのフォーマットを標準化しました。
jsonifyはpythonのリストや辞書をjson形式に変換するFlaskの関数です。
これをすぐに見てみましょう。
今、shows変数のjsonificationを返していることに注目してください。
return jsonify(shows)
これはdb.executeから戻ってきた行のjsonificationです。
では実際にやってみましょう。
いくつかの変更を行ったので、サーバを再起動してみましょう。
サーバの/search?q=officeに行ってみます。
手動でこのURLに行ってみます。
戻ってきたものに注目してください。
[{"id":108878,"title":"Nice Day at the Office"},{"id":112108,"title":"The Office"},{"id":122441,"title":"Avocat d'office"},{"id":264284,"title":"Office Gossip"},{"id":290978,"title":"The Office"},{"id":292829,"title":"Office Office"},{"id":297511,"title":"Box Office America"},{"id":365969,"title":"10-8: Officers on Duty"},{"id":377234,"title":"A Nice Day at the Office"},{"id":386676,"title":"The Office"},{"id":469709,"title":"The Office Temps"},{"id":490705,"title":"The 1970s
....
(pythonのリストの記法でofficeの検索結果が羅列される)
一見すると、何のことかわからないですよね。
しかし、これは確かにJavaScript Object Notationと呼ばれるものです。
左上には[角括弧があります。
これは「ブラウザよ。ここにリストがあるぞ」という意味です。
{中括弧がありますが、これは「ブラウザよ。ここに辞書があるぞ」という意味で、JavaScriptではオブジェクトと呼ばれています。
""引用符で囲まれたidがこの辞書のキーです。
108878はそのキーの値です。
2つ目のキーであるtitleの値は"Nice Day at the office"です。
そして、閉じた}中括弧があります。
これは辞書の終わり、またはオブジェクトの終わりを意味し、カンマの後に更に他の辞書の束が続きます。
要するに、ここで見ているのは、python辞書の非常に生々しいテキストベースのフォーマットを、jsonと呼ばれるものに変換したもので、文字通り、ただそれだけです。
jsonは""二重引用符、:コロン、{}中括弧、[]角括弧を使って情報を表現します。
これは素晴らしいことですが、ユーザーが目にするものがこのような混乱したものであるなら、これは非常に恐ろしいユーザーインターフェースです。
しかし、それは問題ではありません。
今のポイントは、バックエンドが検索結果のjson配列を返し、ブラウザがそれをHTMLに変換していることです。
先週、私はJavaScriptでDOM(ドキュメントオブジェクトモデル)、つまり、メモリ内のツリーを変異させたり変更したりできる、と言いました。
そして、特にJavaScriptでできることは、ツリーにノードを追加することです。
家系図のような構造に、より多くのものを加えることができるのです。
これを最小限にして、もうわざわざ使わないであろうsearch.htmlではなく、index.htmlを開くことにしましょう。
index.htmlですが、ここでいくつかのコードを借りてきます。
これを単純化してみましょう。
(layoutの内容をindexにコピペ)
▼index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="initial-scale=1, width=device-width">
<title>shows</title>
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
不要になったlayout.html、search.htmlを削除します。
このプログラムをシンプルにして、今はindex.htmlに完全に集中するつもりです。
では、ここで何をしたいのでしょうか?
ここではJavaScriptを使う必要があります。
jQueryは少し人気が落ちてきていますが、Bootstrapではまだ使用されていますし、他の例でもすでに使用されているので、引き続き使用します。
そこで、jQuery CDNをGoogleで検索して、このウェブサイトにアクセスしてみましょう。
簡単に言うと、そこにはたくさんのライブラリがあり、そのライブラリのscriptタグを取得して、自分のコードの中で使用することができます。
(jQuery 3.xのminifiedをクリックして出てくるscriptタグをheadタグの中にコピペ)
そこで、これからタグを整理して、見にくいかもしれませんが、1行にまとめます。
<script src="https://code.jquery.com/jquery-3.7.0.min.js" integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g=" crossorigin="anonymous"></script>
いつもは配布コードの問題集の中で皆さんのために行うのですが、これからそのライブラリを自分のファイルにコピーして、使えるライブラリの中に入れます。
そして、先週と同じように、自分のページのheadの中にscriptタグを追加します。
オンラインで見られるものとの一貫性を保つために、少し違う方法で進めてみましょう。
順番を間違える心配が無いように、すべてbodyの中で行うことにします。
このページのbodyの中ですべての作業を行います。
(先ほどのjQueryのコードも新しいscriptタグもbodyの中に移動)
<input autocomplete="off" autofocus placeholder="Query" type="text">とします。
非常にシンプルな検索ボックスと何も入っていない順不同のリストを用意します。
<body>
<input autocomplete="off" autofocus placeholder="Query" type="text">
<ul></ul>
<script src="https://code.jquery.com/jquery-3.7.0.min.js" integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g=" crossorigin="anonymous"></script>
<script>
</script>
</body>
今のところ、これで十分です。
そして、jQueryのライブラリをインポートしてみます。
そして、この下、自分のscriptタグの中で、実際にコードを書くことになります。
let input = document.querySelector('input');
これでうまくいきます。
今のところinputは1つだけなので、IDやクラスは必要なく、querySelector('input')でそのタグを指定します。
では、そのinputに、key upイベントを受信するイベントリスナーを追加してみましょう。
input.addEventListener('keyup',
key upとは、ユーザーがキーに触れたり離したりすることですが、これもイベントの一つです。
ユーザーがそれを行ったときに、次のような関数を呼び出したいと思います。
これも先週扱いましたが、匿名関数です。
input.addEventListener('keyup', function(){
匿名関数の中で次のようなことをします。
新しい関数、今日のJavaScriptの唯一の新しい関数である$.getを呼び出します。
これはjQuery.getの略記法です。
これで、次のように取得していきます。
input.addEventListener('keyup', function(){
$.get('/search?q=' + input.value)
});
この1行のコード、つまりJavaScriptの文字列で、番組をリクエストしたいURL、/search?q=、ユーザーが何かを入力したインプットボックスの値をJavaScriptのコードで動的に構築していることがわかります。
この$.get関数がどのように機能するかというと、コールバックと呼ばれるものをサポートしています。
コールバックとは、最終的に答えが得られたときに呼び出される関数のことです。
これはウェブ上では重要なことです。
インターネットは時に遅く、私が今しようとしているように、他のサーバに連絡するコードを書きたいと思うかもしれません。
サーバが遅かったり、インターネットの接続が遅かったりして、サーバからの応答を待っている間にブラウザ全体がフリーズしてしまうと、とても困りますよね。
電話のように、「データが手に入ったら、この関数を呼び出して、私を呼び戻してください」と言えたら、その方がずっと良いですよね。
では、どんな関数を呼べば良いのでしょうか?
showsという引数を入力とする匿名関数を呼び出したいのですが、これは好きなように呼ぶことができます。
最初は何もないHTML文字列を作成します。
input.addEventListener('keyup', function(){
$.get('/search?q=' + input.value, function(shows){
let html='';
});
});
JavaScriptのforループで、idという変数に、戻ってきたばかりの番組のIDを入れて、このforループの中で、let title = shows[id].title;とします。
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;
});
});
さて、これは間違いなく今日の内容で最もクレイジーなところで、新しい構文はもちろんのこと、非常に多くの異なるアイデアを結びつけています。
今の目的は、明日も同じことができるようにこの構文を紹介することではなく、私たちがやっていることのアイデアを紹介することです。
では、何をしているのでしょうか?
テキストを入力するシンプルなウェブページを用意しました。
<input autocomplete="off" autofocus placeholder="Query" type="text">
そのページには、現在は空の順序なしリストがあります。
<ul></ul>
その下には、Bootstrapがグラフィカルなコンポーネントの一部に使用している、jQueryというサードパーティーのJavaScriptライブラリを入れています。
<script src="https://code.jquery.com/jquery-3.7.0.min.js" integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g=" crossorigin="anonymous"></script>
そして、その下には私自身のJavaScriptコードがあります。
まず、数行上の自分のウェブページのinputタグにアクセスするための変数を宣言します。
let input = document.querySelector('input');
そして、AJAXコールと呼ばれるものを行うために、このファンシーな関数$.getを使用しています。
input.addEventListener('keyup', function(){
$.get('/search?q=' + input.value, function(shows){
AJAXとは、ウェブページがプログラムによってサーバに追加のHTTPリクエストを行う機能のことです。
これにより、今日の全てのウェブサイトは、あるプラットフォームで友人が皆さんにメッセージを送った場合、皆さんのチャットメッセージを取得することができます。
また、住所を入力する際には、全世界の住所情報をもとに、住所を自動入力することができます。
Google検索のオートコンプリートの仕組みもこれと同じです。
$.getはjQueryに組み込まれた関数で、標準的なJavaScriptの機能を使用しています。
これにより、人間が入力したものと同じように、/search?q=で終わるURLにアクセスすることができ、レスポンスの準備ができると、この匿名の関数がコールバックされます。
$.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;
});
この関数はshowsという変数を引数に取ります。showsの内容は次のようなものです。
{"id":108878,"title":"Nice Day at the Office"}
これが、私のJavaScript関数がshows引数として受け取るものです。
これはリスト、または配列で、その中にはIDとタイトルの2つのキーを持つ辞書、またはオブジェクトのリストが入っていることに注意してください。
これは、このようなデータ構造を反復処理するためのJavaScript構文であり、次から次へとli、li、と行を作成し、htmlという変数に連結して追加するためのJavaScript構文でもあります。
for(let id in shows)
{
let title = shows[id].title;
html += '<li>' + title + '</li>'
}
そして、この最後のコードはブラウザに対して「ulを選択して、その内部のHTMLにこの内容、つまり私の変数の値を挿入してください」と言っています。
document.querySelector('ul').innerHTML = html;
構文が間違っていないことを祈りつつ、flask runを実行してみましょう。
(off、と途中まで入力すると、その文字を含む番組のタイトルがリストになって表示される)
これまで入力したもの、あるいはこれまでに見たものはすべてoffという単語が含まれています。
これに続けてofficeと入力し、検索させてインターネットに任せてみると、確かにofficeを含むタイトルがリストアップされます。
更にinspectタブを開き、inspectの下にあるネットワークタブを開いて、officeと入力してみると、入力した全てのキーストロークによって、「office」が実行され、AJAXコール、つまり、サーバへのHTTPコールが引き起こされていることに気付きます。
(入力される一文字ずつに対して/search?q=…が実行され、検索されていることがわかる)
最後の1つをクリックして下にストロークすると、リクエストとレスポンスが表示されるだけでなく、レスポンスをクリックすると、全てのデータの大きなjson配列が表示されることに気付きます。
(search?q=officeのレスポンスを開くと、下記のようなjson配列が表示される)
[
{
"id": 108878,
"title": "Nice Day at the Office"
},
{
"id": 112108,
"title": "The Office"
},
...
遅いのはむしろ好都合で、1つのデモで終わらせるつもりだったからです。
例えば、一日中屋内にいる私が天気を知りたいと思ったら、この電話でブライアンに連絡するかもしれません。
(黒電話でブライアンに電話をかける)
ブライアン「ハロー?」
デイヴィッド「ブライアン、今日の外の天気はどうですか?」
ブライアン「ちょっと外に出ていないのですが、調べてみます。またお電話します。」
デイヴィッド「わかりました。」
ここで彼が外に出るのを電話で待たなければならないとしたら、それは迷惑なことです。
そこで私は一旦電話を切り、彼からの電話を待つことにします。
(受話器を置く)
私のコードではまさにそれが起こっています。
この匿名関数をget関数に渡すと(ここでのgetはHTTP GETを意味します)、これはブラウザに「答えが出たらこの関数(function(shows))を呼べ」と言っています。
$.get('/search?q=' + input.value, function(shows){
これは、うまくいけばブライアンから電話がかかってくるのと同じです。
(ブライアンから電話がかかってくる)
デイヴィッド「ハロー?」
ブライアン「hi。天気を調べてみました。今日は爽やかな秋の一日になりそうです。」
デイヴィッド「素晴らしい。ありがとうございました。」
今日のCS50はこれでおしまいです。
次回にお会いしましょう。
この記事が気に入ったらサポートをしてみませんか?