11.1. 第 9 周:Flask¶
11.1.1. 欢迎!¶
在前几周中,你已经学习了许多编程语言、技术和策略。
确实,这门课与其说是一门 C 语言课 或 Python 课 ,不如说是一门 编程课 ,以便你能跟上未来的趋势。
在过去的这几周里,你已经学会了 如何学习 编程。
今天,我们将从 HTML 和 CSS 过渡到结合 HTML、CSS、SQL、Python 和 JavaScript,从而让你能够创建自己的 Web 应用程序。
你可以考虑利用本周所学的技能来完成你的期末项目。
11.1.2. http-服务器¶
到目前为止,你所见的所有 HTML 都是预先编写且静态的。
过去,当你访问一个页面时,浏览器会下载一个 HTML 页面,你便能查看它。这些被称为 静态 页面,因为 HTML 中编程的内容正是用户在互联网浏览器 客户端 看到并下载的内容。
动态页面指的是 Python 及类似语言能够即时创建 HTML 的能力。因此,你可以拥有由代码根据用户的输入或行为在 服务器端 生成的网页。
你过去曾使用
http-server来提供你的网页。今天,我们将利用一种新的服务器,它可以解析网址并根据提供的 URL 执行操作。此外,上周你看到的 URL 如下:
https://www.example.com/folder/file.html
注意
file.html是位于example.com处名为folder的文件夹中的一个 HTML 文件。
11.1.3. Flask¶
本周,我们将介绍与 路由 (如
https://www.example.com/route?key=value)交互的能力,其中可以通过 URL 中提供的关键码和值在服务器上生成特定功能。Flask 是一个第三方库,允许你在 Python 中使用 Flask 框架或微框架来托管 Web 应用程序。
你可以通过在 cs50.dev 的终端窗口中执行
flask run来运行 Flask。为此,你需要一个名为
app.py的文件和另一个名为requirements.txt的文件。app.py包含告诉 Flask 如何运行你的 Web 应用程序的代码。requirements.txt包含你的 Flask 应用程序运行所需的库列表。以下是
requirements.txt的示例:Flask注意只有
Flask出现在此文件中。这是因为运行 Flask 应用程序需要 Flask。src9/hello0/app.py
以下是
app.py中一个非常简单的 Flask 应用程序:# Says hello to world by returning a string of text from flask import Flask app = Flask(__name__) @app.route("/") def index(): return "hello, world"
注意,
/路由仅返回文本hello, world。src9/hello1/app.py
我们还可以创建实现 HTML 的代码:
# Says hello to world by returning a string of HTML from flask import Flask app = Flask(__name__) @app.route("/") def index(): return '<!DOCTYPE html><html lang="en"><head><title>hello</title></head><body>hello, world</body></html>'
请注意,这里返回的不是纯文本,而是 HTML。
src9/hello2/templates/index.html
改进我们的应用时,还可以通过创建一个名为
templates的文件夹,并在该文件夹中创建一个名为index.html的文件(包含以下代码),来基于模板提供 HTML:<!DOCTYPE html> <html lang="en"> <head> <title>hello</title> </head> <body> hello, world </body> </html>
src9/hello5/app.py
然后,在
templates文件夹所在的同一文件夹中,创建一个名为app.py的文件,并添加以下代码:# Uses request.args.get from flask import Flask, render_template, request app = Flask(__name__) @app.route("/") def index(): name = request.args.get("name", "world") return render_template("index.html", name=name)
请注意,此代码将
app定义为 Flask 应用程序。接着,它将app的/路由定义为返回index.html的内容,并传入name作为参数。默认情况下,request.args.get函数会查找用户提供的name。若未提供名称,则默认为world。@app.route也被称为装饰器。你可以在终端窗口中输入
flask run来运行此 Web 应用程序。如果 Flask 无法运行,请确保上述每个文件中的语法正确。此外,如果 Flask 仍无法运行,请确保你的文件组织如下:/templates index.html app.py requirements.txt一旦运行成功,系统会提示你点击一个链接。当你导航到该网页后,尝试在浏览器的地址栏中向基础 URL 添加
?name=[Your Name]。
11.1.4. 表单¶
src9/hello6/templates/index.html
改进我们的程序,我们知道大多数用户不会在地址栏中输入参数。相反,程序员依赖用户在网页上填写表单。因此,我们可以按如下方式修改
index.html:<!DOCTYPE html> <html lang="en"> <head> <title>hello</title> </head> <body> <form action="/greet" method="get"> <input autocomplete="off" autofocus name="name" placeholder="Name" type="text"> <button type="submit">Greet</button> </form> </body> </html>
请注意,现在创建了一个表单,用于获取用户的姓名,然后将其传递给名为
/greet的路由。autocomplete已关闭。此外,还包含一个文本为name的placeholder。src9/hello6/app.py
此外,我们可以按如下方式更改
app.py:# Adds a form, second route from flask import Flask, render_template, request app = Flask(__name__) @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"))
注意,默认路径将显示一个表单供用户输入其姓名。
/greet路由会将name传递给该网页。src9/hello6/templates/greet.html
要完成此实现,你需要在
templates文件夹中为greet.html创建另一个模板,如下所示:<!DOCTYPE html> <html lang="en"> <head> <meta name="viewport" content="initial-scale=1, width=device-width"> <title>hello</title> </head> <body> hello, {{ name }} </body> </html>
请注意,此路由现在会向用户呈现问候语,后跟其姓名。
11.1.5. 模板¶
我们的两个网页
index.html和greet.html拥有大量相同的数据。如果能让主体部分独一无二,同时在页面之间复用相同的布局,那该多好?src9/hello7/templates/layout.html
首先,创建一个名为
layout.html的新模板,并编写如下代码:<!DOCTYPE html> <html lang="en"> <head> <title>hello</title> </head> <body> {% block body %}{% endblock %} </body> </html>
请注意,
{% block body %}{% endblock %}允许从其他 HTML 文件中插入其他代码。src9/hello7/templates/index.html
然后,按如下方式修改你的
index.html:{% extends "layout.html" %} {% block body %} <form action="/greet" method="get"> <input autocomplete="off" autofocus name="name" placeholder="Name" type="text"> <button type="submit">Greet</button> </form> {% endblock %}请注意,行
{% extends "layout.html" %}告诉服务器从哪里获取此页面的布局。然后,{% block body %}{% endblock %}告诉要将什么代码插入到layout.html中。src9/hello7/templates/greet.html
最后,按如下方式修改
greet.html:{% extends "layout.html" %} {% block body %} hello, {{ name }} {% endblock %}请注意,此代码更短且更紧凑。
11.1.6. 请求方法¶
你可以想象某些场景下使用
get并不安全,因为用户名和密码会出现在 URL 中。src9/hello8/app.py
我们可以利用
post方法,通过如下修改app.py来帮助解决此问题:# Switches to POST from flask import Flask, render_template, request app = Flask(__name__) @app.route("/") def index(): return render_template("index.html") @app.route("/greet", methods=["POST"]) def greet(): return render_template("greet.html", name=request.form.get("name", "world"))
注意
POST被添加到了/greet路由,并且我们使用request.form.get而非request.args.get。这指示服务器 更深 地查看虚拟信封,而不在
post的 URL 中暴露项目。src9/hello9/app.py
尽管如此,通过为
get和post使用单一路由,此代码仍可进一步优化。为此,请按以下方式修改app.py:# Uses a single route from flask import Flask, render_template, request app = Flask(__name__) @app.route("/", methods=["GET", "POST"]) def index(): if request.method == "POST": return render_template("greet.html", name=request.form.get("name", "world")) return render_template("index.html")
请注意,
get和post均在单次路由中完成。然而,request.method用于根据用户请求的路由类型进行正确路由。src9/hello9/templates/index.html
因此,你可以按如下方式修改你的
index.html:{% extends "layout.html" %} {% block body %} <form action="/" method="post"> <input autocomplete="off" autofocus name="name" placeholder="Name" type="text"> <button type="submit">Greet</button> </form> {% endblock %}注意表单
action已被修改。src9/hello10/app.py
尽管如此,这段代码仍存在一个缺陷。使用我们的新实现后,当有人在表单中不输入姓名时,
Hello,会在没有姓名的情况下显示。我们可以通过按如下方式编辑app.py来改进代码:# Moves default value to template from flask import Flask, render_template, request app = Flask(__name__) @app.route("/", methods=["GET", "POST"]) def index(): if request.method == "POST": return render_template("greet.html", name=request.form.get("name")) return render_template("index.html")
注意
name=request.form.get("name"))已被修改。src9/hello10/templates/greet.html
最后,按如下方式修改
greet.html:{% extends "layout.html" %} {% block body %} hello, {% if name %} {{ name }} {% else %} world {% endif %} {% endblock %}注意
hello, {{ name }}是如何被修改的,以便在未识别到名称时提供默认输出。由于我们修改了许多文件,你可能希望将最终代码与 our final code 进行比较。
11.1.7. 新生校内体育¶
Frosh IMs 或 froshims 是一个允许学生注册校内体育活动的 Web 应用程序。
关闭所有与
hello相关的窗口,并在终端窗口中输入mkdir froshims以创建一个文件夹。然后,输入cd froshims以浏览到此文件夹。在其中,通过输入mkdir templates创建一个名为 templates 的目录。接下来,在
froshims文件夹中,输入code requirements.txt并按如下方式编写代码:Flask
与之前一样,运行 Flask 应用程序需要 Flask。
src9/froshims1/app.py
最后,输入
code app.py并按如下方式编写代码:# Implements a registration form using a select menu, validating sport server-side from flask import Flask, render_template, request app = Flask(__name__) SPORTS = [ "Basketball", "Soccer", "Ultimate Frisbee" ] @app.route("/") def index(): return render_template("index.html", sports=SPORTS) @app.route("/register", methods=["POST"]) def register(): # Validate submission if not request.form.get("name") or request.form.get("sport") not in SPORTS: return render_template("failure.html") # Confirm registration return render_template("success.html")
请注意,系统提供了
failure选项,若name或sport字段未正确填写,则会向用户显示失败消息。src9/froshims1/templates/index.html
接下来,在
templates文件夹中创建一个名为index.html的文件,方法是输入code templates/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 selected value="">Sport</option> {% for sport in sports %} <option value="{{ sport }}">{{ sport }}</option> {% endfor %} </select> <button type="submit">Register</button> </form> {% endblock %}src9/froshims1/templates/layout.html
接下来,创建一个名为
layout.html的文件,方法是输入code templates/layout.html并编写如下代码:<!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>
src9/froshims1/templates/success.html
第四,在 templates 中创建一个名为
success.html的文件,内容如下:{% extends "layout.html" %} {% block body %} You are registered! {% endblock %}src9/froshims1/templates/failure.html
最后,在 templates 中创建一个名为
failure.html的文件,内容如下:{% extends "layout.html" %} {% block body %} You are not registered! {% endblock %}执行
flask run并查看此阶段的应用程序。src9/froshims2/templates/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"> {% for sport in sports %} <input name="sport" type="radio" value="{{ sport }}"> {{ sport }} {% endfor %} <button type="submit">Register</button> </form> {% endblock %}注意
type如何被更改为radio。再次执行
flask run,你可以看到界面现在已发生变化。src9/froshims4/app.py
你可以想象,我们可能希望接受许多不同注册者的注册。我们可以按如下方式改进
app.py:# Implements a registration form, storing registrants in a dictionary, with error messages from flask import Flask, redirect, render_template, request app = Flask(__name__) REGISTRANTS = {} SPORTS = [ "Basketball", "Soccer", "Ultimate Frisbee" ] @app.route("/") def index(): return render_template("index.html", sports=SPORTS) @app.route("/register", methods=["POST"]) def register(): # Validate name name = request.form.get("name") if not name: return render_template("error.html", message="Missing name") # Validate sport 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") # Remember registrant REGISTRANTS[name] = sport # Confirm registration return redirect("/registrants") @app.route("/registrants") def registrants(): return render_template("registrants.html", registrants=REGISTRANTS)
请注意,一个名为
REGISTRANTS的字典用于记录由REGISTRANTS[name]选择的sport。此外,请注意registrants=REGISTRANTS将该字典传递给了此模板。src9/froshims4/templates/error.html
此外,我们可以实现
error.html:{% extends "layout.html" %} {% block body %} <h1>Error</h1> <p>{{ message }}</p> <img alt="Grumpy Cat" src="/static/cat.jpg"> {% endblock %}src9/froshims4/templates/registrants.html
此外,创建一个名为
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 %}请注意,
{% for name in registrants %}...{% endfor %}将遍历每位注册者。能够在动态网页上进行迭代非常强大!最后,在与
app.py相同的文件夹中创建一个名为static的文件夹。在那里,上传一个 cat 的以下文件。执行
flask run并试用该应用程序。你现在拥有了一个 Web 应用程序!然而,存在一些安全漏洞!由于所有内容都保存在内存中,攻击者可以修改 HTML 并 入侵 网站。此外,如果服务器关闭,这些数据将不会持久化。是否有一种方法可以让我们的数据即使在服务器重启后也能持久化?
11.1.8. Flask 和 SQL¶
正如我们已经看到 Python 如何与 SQL 数据库交互,我们可以结合 Flask、Python 和 SQL 的力量来创建一个数据能够持久化的 Web 应用程序!
要实现这一点,你需要采取若干步骤。
首先,将以下 SQL database 下载到你的
froshims文件夹中。在终端中执行
sqlite3 froshims.db并输入.schema以查看数据库文件的内容。接着输入SELECT * FROM registrants;以了解具体内容。你会注意到文件中目前没有任何注册信息。接下来,按如下方式修改
requirements.txt:cs50 Flask
src9/froshims7/templates/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"> {% for sport in sports %} <input name="sport" type="checkbox" value="{{ sport }}"> {{ sport }} {% endfor %} <button type="submit">Register</button> </form> {% endblock %}src9/froshims7/templates/layout.html
按如下方式修改
layout.html:<!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>
src9/froshims7/templates/error.html
确保
error.html显示如下:{% extends "layout.html" %} {% block body %} <h1>Error</h1> <p>{{ message }}</p> <img alt="Grumpy Cat" src="/static/cat.jpg"> {% endblock %}src9/froshims7/templates/registrants.html
修改
registrants.html使其显示如下:{% extends "layout.html" %} {% block body %} <h1>Registrants</h1> <table> <thead> <tr> <th>Name</th> <th>Sport</th> <th></th> </tr> </thead> <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 }}"> <button type="submit">Deregister</button> </form> </td> </tr> {% endfor %} </tbody> </table> {% endblock %}请注意,其中包含一个隐藏值
registrant.id,以便稍后在app.py中使用此id。src9/froshims7/app.py
最后,按如下方式修改
app.py:# Implements a registration form, storing registrants in a SQLite database, with support for multiple sports and deregistration from cs50 import SQL from flask import Flask, redirect, render_template, request app = Flask(__name__) db = SQL("sqlite:///froshims.db") SPORTS = [ "Basketball", "Soccer", "Ultimate Frisbee" ] @app.route("/") def index(): return render_template("index.html", sports=SPORTS) @app.route("/deregister", methods=["POST"]) def deregister(): # Forget registrant id = request.form.get("id") if id: db.execute("DELETE FROM registrants WHERE id = ?", id) return redirect("/registrants") @app.route("/register", methods=["POST"]) def register(): # Validate name name = request.form.get("name") if not name: return render_template("error.html", message="Missing name") # Validate sports sports = request.form.getlist("sport") if not sports: return render_template("error.html", message="Missing sport") for sport in sports: if sport not in SPORTS: return render_template("error.html", message="Invalid sport") # Remember registrant for sport in sports: db.execute("INSERT INTO registrants (name, sport) VALUES(?, ?)", name, sport) # Confirm registration return redirect("/registrants") @app.route("/registrants") def registrants(): registrants = db.execute("SELECT * FROM registrants") return render_template("registrants.html", registrants=registrants)
请注意,
cs50库已被使用。为post方法包含了一个用于register的路由。该路由将从注册表单中获取姓名和运动项目,并执行 SQL 查询以将name和sport添加到registrants表中。deregister路由指向一个 SQL 查询,该查询将获取用户的id并利用该信息注销此用户。你可以执行
flask run并检查结果。如果你想下载我们对
froshims的实现,可以在此处 here 进行。你可以在 Flask documentation 中阅读更多关于 Flask 的内容。
11.1.10. 购物车¶
接下来是最后一个利用 Flask 启用会话能力的示例。
src9/store/app.py
我们检查了
app.py中store的以下代码。展示了以下代码: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 session 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": book_id = request.form.get("id") if book_id: session["cart"].append(book_id) return redirect("/cart") # GET books = db.execute("SELECT * FROM books WHERE id IN (?)", session["cart"]) return render_template("cart.html", books=books)
注意
cart是使用列表实现的。可以使用books.html中的Add to Cart按钮将商品添加到此列表中。点击此类按钮时,会调用post方法,其中商品的id会被追加到cart。查看购物车时,调用get方法会执行 SQL 以显示购物车中的书籍列表。src9/store/templates/books.html
我们也看到了
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'] }}"> <button type="submit">Add to Cart</button> </form> {% endfor %} {% endblock %}注意这如何使用
for book in books为每本书创建Add to Cart的能力。你可以在 source code 中查看驱动此
flask实现的其他文件。
11.1.11. 显示¶
src9/shows1/app.py
我们查看了一个名为
shows的预设计程序,位于app.py:# Searches for shows using LIKE 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)
注意
search路由提供了一种查找show的方法。此查找会检索标题LIKE用户提供的内容。src9/shows1/templates/index.html
我们还检查了
index.html:{% extends "layout.html" %} {% block body %} <form action="/search" method="get"> <input autocomplete="off" autofocus name="q" placeholder="Query" type="search"> <button type="submit">Search</button> </form> {% endblock %}你可以在 source code 中查看此实现的其他文件。
11.1.12. API¶
应用程序接口或 API 是一系列规范,允许你与另一项服务进行交互。例如,我们可以利用 IMDB 的 API 与其数据库进行交互。我们甚至可以集成用于处理可从服务器下载的特定类型数据的 API。
src9/shows2/app.py
在改进
shows并考察app.py的改进时,我们看到了以下内容:# Searches for shows using Ajax 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(): q = request.args.get("q") if q: shows = db.execute("SELECT * FROM shows WHERE title LIKE ? LIMIT 50", "%" + q + "%") else: shows = [] return render_template("search.html", shows=shows)
注意,
search路由会执行一条 SQL 查询。src9/shows2/templates/查找.html
查看
search.html,你会注意到它非常简单:{% for show in shows %} <li>{{ show["title"] }}</li> {% endfor %}请注意,它提供了一个项目符号列表。
src9/shows2/templates/index.html
最后,查看
index.html,请注意 AJAX 代码被用于驱动查找功能:<!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> let input = document.querySelector('input'); input.addEventListener('input', async function() { let response = await fetch('/search?q=' + input.value); let shows = await response.text(); document.querySelector('ul').innerHTML = shows; }); </script> </body> </html>
请注意,这里使用了一个事件监听器来动态查询服务器,以提供与所给标题匹配的列表。这将定位 HTML 中的
ul标签,并相应地修改网页以包含匹配项列表。你可以在 AJAX documentation 中阅读更多内容。
11.1.13. JSON¶
JavaScript 对象表示法 或 JSON 是包含关键码和值的字典文本文件。这是一种原始且对计算机友好的获取大量数据的方式。
JSON 是从服务器返回数据的一种非常实用的方式。
src9/shows3/templates/index.html
你可以在我们共同研究的
index.html中看到这一点:<!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="text"> <ul></ul> <script> let input = document.querySelector('input'); input.addEventListener('input', async function() { let response = await fetch('/search?q=' + input.value); let shows = await response.json(); let html = ''; for (let i in shows) { let title = shows[i].title.replace('<', '<').replace('&', '&'); html += '<li>' + title + '</li>'; } document.querySelector('ul').innerHTML = html; }); </script> </body> </html>
尽管上述内容可能有些晦涩,但它为你自行研究 JSON 提供了一个起点,以便了解如何在你自己的 Web 应用程序中实现它。
src9/shows3/app.py
此外,我们检查了
app.py以了解如何获取 JSON 响应:# Searches for shows using Ajax with JSON from cs50 import SQL from flask import Flask, jsonify, 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(): q = request.args.get("q") if q: shows = db.execute("SELECT * FROM shows WHERE title LIKE ? LIMIT 50", "%" + q + "%") else: shows = [] return jsonify(shows)
注意
jsonify如何将结果转换为当代 Web 应用程序可接受的易读格式。你可以在 JSON documentation 中阅读更多内容。
总之,你现在已能够使用 Python、Flask、HTML 和 SQL 完成自己的 Web 应用程序。
11.1.14. 总结¶
在本课中,你学习了如何利用 Python、SQL 和 Flask 创建 Web 应用程序。具体而言,我们讨论了…
Flask
表单
模板
请求方法
Flask 与 SQL
Cookie 与会话
API
JSON
