
视频元信息
- 时长:约 2 小时(本讲完整版)
- 课程:CS50x Introduction to Computer Science(2026)
- 主讲:David J. Malan
- 主题:Lecture 9 - Flask(Web 编程)
内容摘要(TL;DR)
本讲是 CS50 的 Web 编程核心课,介绍如何使用 Python Flask 框架构建动态网站。从最简 Flask 应用出发,逐步讲解路由、模板(Jinja)、GET/POST 方法、表单处理、模板继承、服务端验证、数据库集成(SQLite)、会话与 Cookies、MVC 模式,最终实现注册系统、购物车、搜索 API 等真实案例。强调不要信任用户输入,并理解 HTTP 的无状态性及会话机制。
1. 课程定位与 Web 编程概述
要点
- 本讲是前 10 周知识的综合应用,重点转向 Web 编程。
- 前几周已学 HTML、CSS、JavaScript(客户端),今天补上服务端组件。
- 目标:用 Python 构建动态 Web 应用,处理用户输入、生成动态页面。
详细说明
[00:45] 本讲最接近实际编程工作,是对过去 10 周的总结。Web 应用背后是 HTML/CSS/JavaScript 负责布局和客户端交互,今天引入服务端逻辑。
理解: Web 编程的实质是:浏览器发送 HTTP 请求(虚拟信封),服务器解析 URL 和参数,动态生成响应内容。
2. Flask 框架入门
要点
- 从零写 Web 服务器不现实,使用现成框架 Flask(微框架)。
- 框架 = 库 + 约定,遵循约定才能高效开发。
- 最小项目文件:
app.py和requirements.txt。 - 运行命令:
flask run(默认端口 5000)。
详细说明
[02:30] URL 传统上指向服务器上的文件(如 file.html),但今天开始把 URL 当作路由(route)——服务器根据 URL 路径决定输出内容。查询参数用 ?key=value&key2=value2 传递。
[04:30] 使用 Flask 的原因:
- C 语言实现 Web 服务器是噩梦,Python 更高级。
- 处理 HTTP 请求、解析参数等是通用逻辑,不必重造轮子。
- Flask 是微框架,只实现我们需要关心的业务逻辑。
易错点: flask run 默认寻找 app.py,若文件名不同需设置环境变量。
基础事实:
requirements.txt每行列出一个库名,用pip install -r requirements.txt安装。- Flask 默认监听端口 5000(而 http.server 是 8080)。
3. 第一个 Flask 应用
要点
- 最小代码结构:
from flask import Flask app = Flask(__name__) @app.route("/") def index(): return "hello, world" @app.route是 Python 装饰器,将函数与 URL 路径关联。- 浏览器直接返回字符串时,没有 HTML 标签,浏览器会自行补全。
详细说明
[07:30] from flask import Flask:从 flask 包导入 Flask 类(注意大小写)。
app = Flask(__name__):__name__ 是当前文件名,这行把当前文件变成 Flask 应用。
理解: 装饰器 @app.route("/") 本质上告诉 Flask:“当用户访问 / 时,调用下面这个函数”。这比 C 里手动解析请求简单得多。
[10:00] 返回纯文本 "hello, world" 时,查看页面源码只有一行文字,浏览器自动生成最小 HTML 文档。若想发送真正的 HTML,可以直接返回 HTML 字符串,但这样代码混乱。
易错点: 返回 HTML 字符串时,如果字符串内有双引号,外层请用单引号,反之亦然。
4. 模板与 Jinja
要点
- 使用
render_template("index.html")渲染 HTML 文件。 - HTML 文件必须放在
templates/文件夹中。 - Jinja 模板语言用
{{ 变量 }}插入动态值。
详细说明
[13:00] 把 HTML 从 Python 代码中分离,放在 templates/index.html。Flask 默认在 templates/ 目录查找模板。
基础事实: render_template 是 Flask 提供的函数,底层使用 Jinja 模板引擎。
理解: 模板(template)是网页的蓝图,通过占位符动态生成最终 HTML。
[15:30] 示例:URL 传入 ?name=David,在 Python 中获取参数:
from flask import Flask, render_template, request
@app.route("/")
def index():
name = request.args.get("name", "world")
return render_template("index.html", placeholder=name)
模板中:<h1>hello, {{ placeholder }}</h1>
易错点: request.args 用于 GET 请求的参数;如果参数不存在,直接 request.args["name"] 会报错,应使用 .get("name", "world") 提供默认值。
考点: {{ }} 是 Jinja 的变量插值语法。
5. GET 与 POST 方法
要点
- GET 把参数放在 URL 中(
?key=value),适合低风险数据,可收藏/分享链接。 - POST 把参数放在请求体(信封内部),适合隐私数据,URL 不显示参数。
- 路由默认只支持 GET,要支持 POST 需指定
methods=["POST"]。
详细说明
[20:00] 表单的 method 属性决定提交方式:
method="get":参数暴露在 URL 中,如/greet?name=David。method="post":参数在请求体中,URL 只显示/greet。
易错点: 使用 POST 时,request.args 拿不到数据,必须改用 request.form。这是 Flask 中容易混淆的地方。
错误示例: 只定义 @app.route("/greet") 时,POST 请求会返回 405 Method Not Allowed。需要改为:
@app.route("/greet", methods=["POST"])
def greet():
name = request.form.get("name", "world")
return render_template("greet.html", name=name)
理解: HTTP 动词(GET/POST)就像信封上的不同投递方式。GET 把信息写在信封表面,POST 塞进信封内部。
6. 模板继承(layout.html)
要点
- 多个页面共享相同 HTML 结构时,用
layout.html存放公共部分。 - 子模板通过
{% extends "layout.html" %}和{% block body %}定义自己的内容。 - Jinja 的
{% %}用于逻辑控制(如循环、条件、块)。
详细说明
[25:00] 初始的 index.html 和 greet.html 几乎相同,只有 body 部分不同。抽取公共框架到 layout.html:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Hello</title>
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
子模板:
{% extends "layout.html" %}
{% block body %}
<form action="/greet" method="get">
<input name="name" type="text" placeholder="Name" autocomplete="off" autofocus>
<button type="submit">Greet</button>
</form>
{% endblock %}
考点: {% block body %} 与 {% endblock %} 是成对出现的,名称可自定义(如 content),但通常用 body。
理解: 模板继承是“DRY(Don't Repeat Yourself)”原则的体现,避免重复书写相同的 HTML 头部和尾部。
7. 合并 GET 和 POST 到同一路由
要点
- 不需要为“显示表单”和“处理表单”分别创建路由,可以在同一路由内判断请求方法。
- 通过
request.method判断是 GET 还是 POST。
详细说明
[30:00] 将 /greet 合并到 / 中:
@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")
表单的 action 改为 /(或省略,默认提交到当前页面)。
易错点: 合并后,表单的 action 必须指向 /,否则会 404。
理解: 这是一种常见的 Web 设计模式:同一 URL 既负责展示表单(GET),又负责处理提交(POST),减少路由数量。
8. 真实案例:Frosh IMs 注册系统
要点
- 案例背景:哈佛新生运动注册,原来用纸质表格,现在用 Web 应用。
- 实现注册表单、服务端验证、错误提示、数据库存储。
- 关键安全原则:永远不要信任用户输入。
详细说明
[35:00] David 回忆自己在 1997 年用 Perl 写的第一个 Web 应用。今天用 Flask 重新实现。
基础事实:
- 表单包含姓名输入框和运动下拉菜单(
<select>)。 - 下拉菜单使用
<option>标签,value属性决定提交值。 - 默认选中项用
selected属性。
易错点: 如果表单提交到未定义的路由(如 /register),会返回 404 Not Found。
[40:00] 服务端验证:
@app.route("/register", methods=["POST"])
def register():
if not request.form.get("name") or not request.form.get("sport"):
return render_template("failure.html")
return render_template("success.html")
考点: 客户端校验(HTML)可被绕过。例如用开发者工具修改 <select> 添加一个不存在的运动,就能提交任意值。因此服务端必须重新验证所有输入。
理解: HTML 只是 UI,不是安全边界。攻击者可以轻松修改页面元素,所以服务端验证是唯一可信的防线。
9. 服务端验证与动态选项
要点
- 将允许的运动列表放在 Python 全局变量(如
SPORTS)中,统一管理。 - 使用 Jinja 循环动态生成
<option>,避免在 HTML 中硬编码。 - 验证时用
if sport not in SPORTS判断。
详细说明
[45:00] 改进验证逻辑:
SPORTS = ["Basketball", "Soccer", "Ultimate Frisbee"]
@app.route("/register", methods=["POST"])
def register():
name = request.form.get("name")
sport = request.form.get("sport")
if not name or sport not in SPORTS:
return render_template("failure.html")
return render_template("success.html")
模板中动态生成选项:
<select name="sport">
<option value="" selected>Sport</option>
{% for sport in sports %}
<option value="{{ sport }}">{{ sport }}</option>
{% endfor %}
</select>
在渲染 index.html 时传入 sports=SPORTS。
易错点: 全局变量名用全大写(SPORTS)是 Python 约定,表示常量,但 Python 没有真正的常量,靠自觉。
理解: 动态生成选项不仅减少了重复代码,还保证了前后端数据一致——修改 Python 列表即可同步更新下拉菜单。
10. 错误处理与用户体验(UX)
要点
- 用
error.html模板显示具体错误信息(如 “Missing name”),而不是笼统的 “You are not registered”。 - 通过
message变量传递错误详情。
详细说明
[50:00] 创建通用错误模板:
{% extends "layout.html" %}
{% block body %}
<h1>Error</h1>
<p>{{ message }}</p>
{% endblock %}
在 register 路由中分别检查:
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")
易错点: 写模板时注意标签顺序,如 {% block body %} 不能写成 {% body block %},否则 Flask 会报错(如 “unknown tag body”)。
理解: 良好的错误提示能显著提升用户体验,让用户知道如何修正,而不是猜测。
11. 数据库集成(SQLite)
要点
- 将数据从内存变量迁移到数据库(SQLite),实现持久化。
- 使用 CS50 的 SQL 库简化操作。
- 插入数据时使用占位符
?防止 SQL 注入。
详细说明
[55:00] 之前用全局字典 REGISTRANTS 存储注册信息,但服务器重启后数据丢失。改用数据库:
from cs50 import SQL
db = SQL("sqlite:///froshims.db")
@app.route("/register", methods=["POST"])
def register():
...
db.execute("INSERT INTO registrants (name, sport) VALUES(?, ?)", name, sport)
考点: SQL 注入防御——绝不能使用 f-string 拼接 SQL,必须用 ? 占位符让库自动转义用户输入。
基础事实: 数据库表 registrants 包含 id(主键)、name、sport 列。
[60:00] 查询注册列表:
@app.route("/registrants")
def registrants():
rows = db.execute("SELECT * FROM registrants")
return render_template("registrants.html", registrants=rows)
模板中遍历列表:
{% for registrant in registrants %}
<tr><td>{{ registrant["name"] }}</td><td>{{ registrant["sport"] }}</td></tr>
{% endfor %}
易错点: db.execute 返回的是字典列表,不是字典,模板中要用 registrant["name"] 而不是 registrants["name"]。
12. 删除注册(Deregister)
要点
- 在列表每行添加一个“Deregister”按钮,通过隐藏字段传递用户 ID。
- 删除操作必须用 POST,不能用 GET(防止 CSRF/钓鱼攻击)。
详细说明
[65:00] 在 registrants.html 中为每行添加表单:
<form action="/deregister" method="post">
<input type="hidden" name="id" value="{{ registrant["id"] }}">
<button type="submit">Deregister</button>
</form>
路由处理:
@app.route("/deregister", methods=["POST"])
def deregister():
id = request.form.get("id")
if id:
db.execute("DELETE FROM registrants WHERE id = ?", id)
return redirect("/registrants")
考点:为什么删除要用 POST? 如果用 GET,攻击者可以构造一个链接(如 /deregister?id=4),诱导管理员点击,从而删除任意用户。POST 需要主动提交表单,至少多一层用户交互。
理解: GET 请求应是无副作用的(只读),任何改变数据的操作都应使用 POST。这是 Web 安全的基本原则。
13. 静态文件与图片
要点
- 图片、CSS、JavaScript 等静态资源放在
static/文件夹。 - 模板中引用静态文件需使用
/static/文件名路径。
详细说明
[70:00] 在错误页面加入“暴躁猫”图片。图片不能放在项目根目录,必须放在 static/ 下:
<img src="/static/cat.jpg" alt="Grumpy Cat">
易错点: 如果图片放在根目录,浏览器会请求 /cat.jpg,但 Flask 不会提供根目录文件,因此图片显示失败。render_template 会自动查找 templates/,但静态文件必须显式使用 /static/ 路径。
理解: 这种目录划分是 Flask 的约定,有助于组织项目结构:代码、模板、静态资源各归其位。
14. 复选框与多选
要点
- 将单选按钮(radio)改为复选框(checkbox),允许用户一次注册多个运动。
- 使用
request.form.getlist("sport")获取所有选中的值。
详细说明
[75:00] HTML 中:
{% for sport in sports %}
<input type="checkbox" name="sport" value="{{ sport }}"> {{ sport }}
{% endfor %}
Python 中:
sports = request.form.getlist("sport")
for sport in sports:
if sport not in SPORTS:
return render_template("error.html", message="Invalid sport")
for sport in sports:
db.execute("INSERT INTO registrants (name, sport) VALUES(?, ?)", name, sport)
易错点: 复选框使用相同的 name 属性,服务器端要用 getlist 而不是 get,否则只能拿到第一个值。
理解: 数据模型也需要调整:一个人对应多条注册记录,后续优化可引入学生表(student ID)来消除冗余,就像 IMDB 数据库的规范化设计。
15. MVC 模式
要点
- 本讲代码自然形成了 Model-View-Controller(MVC) 架构。
- Model:数据库(持久数据)
- View:模板(用户界面)
- Controller:
app.py(业务逻辑)
详细说明
[80:00] 项目结构:
app.py # Controller
templates/ # Views
static/ # 静态资源
froshims.db # Model
理解: MVC 是一种思维框架,帮助划分职责。虽然模板里也有循环和条件,但大致上各层各司其职。这是业界常见的 Web 应用架构。
16. 会话(Sessions)与 Cookies
要点
- HTTP 是无状态协议,但网站需要记住用户身份。
- Cookie 是服务器发给浏览器的小型键值对,浏览器后续请求会自动携带。
- Session 是基于 Cookie 实现的服务器端“记忆”,可存放任意键值对。
详细说明
[85:00] 以 Gmail 登录为例:
- 用户提交用户名/密码(POST)。
- 服务器验证成功后,返回
Set-Cookie: session=<random>响应头。 - 浏览器保存 Cookie,后续每个请求自动带上
Cookie: session=<random>。 - 服务器根据 Cookie 找到对应的 Session 数据(如用户名)。
基础事实: Cookie 就像游乐场的手环——第一次入场时发放,之后出示手环即可自由进出。
易错点: 不要把用户名/密码直接存在 Cookie 中,应只存随机标识符,敏感数据留在服务器端。
[90:00] Flask 中使用 Session:
from flask import session
from flask_session import Session
app.config["SESSION_PERMANENT"] = False
app.config["SESSION_TYPE"] = "filesystem"
Session(app)
@app.route("/login", methods=["POST"])
def login():
session["name"] = request.form.get("name")
return redirect("/")
@app.route("/logout")
def logout():
session.clear()
return redirect("/")
理解: Session 本质上是服务器端的一个字典,每个用户拥有独立副本。Flask 通过 Cookie 中的会话 ID 来区分不同用户。
17. 购物车示例
要点
- 用 Session 实现购物车,每个用户看到自己的购物车内容。
- 购物车数据存储在 Session 的
cart键中,是一个列表。
详细说明
[95:00] 书店应用(store 目录):
@app.route("/cart", methods=["GET", "POST"])
def cart():
if "cart" not in session:
session["cart"] = []
if request.method == "POST":
id = request.form.get("id")
if id:
session["cart"].append(id)
return redirect("/cart")
books = db.execute("SELECT * FROM books WHERE id IN (?)", session["cart"])
return render_template("cart.html", books=books)
页面中每本书有“Add to Cart”按钮,通过隐藏字段提交书籍 ID。
理解: Session 让每个用户拥有独立的购物车,这正是 Cookie 机制的实际应用。
18. 搜索与 API(JSON)
要点
- 用 SQL
LIKE实现模糊搜索。 - 可以创建返回数据(而非完整 HTML)的路由,即 API。
- 使用
jsonify返回 JSON 格式数据。
详细说明
[100:00] 基础搜索:
@app.route("/search")
def search():
q = request.args.get("q")
shows = db.execute("SELECT * FROM shows WHERE title LIKE ?", f"%{q}%")
return render_template("search.html", shows=shows)
% 是 SQL 通配符,匹配任意字符。
[105:00] 进阶:返回 JSON 的 API:
from flask import jsonify
@app.route("/search")
def search():
q = request.args.get("q")
shows = db.execute("SELECT * FROM shows WHERE title LIKE ?", f"%{q}%")
return jsonify(shows)
前端 JavaScript 可以动态调用该 API 实现自动补全。
考点: API(Application Programming Interface) 允许程序之间交换数据。JSON 是 Web API 常用的数据格式,与 Python 字典/列表结构相似。
理解: 返回 JSON 而不是 HTML 的路由更通用——任何人都可以消费你的数据,构建自己的应用。这是现代 Web 服务的基础。
19. 总结与展望
要点
- 本讲综合了前 10 周全部知识:C、Python、SQL、HTML/CSS/JS。
- Web 编程的核心:路由、模板、请求处理、持久化、会话。
- 最终项目将让这些知识融会贯通。
详细说明
[110:00] David 总结:Web 应用 = 控制器(逻辑)+ 视图(模板)+ 模型(数据库)。所有功能都是对 HTTP 请求的响应。
理解: 学习编程不是记住语法,而是掌握解决问题的模式。Flask 只是工具,背后的 MVC、会话、安全原则才是通用知识。
易错点: 在开发中遇到错误时,应查看运行 Flask 的终端日志(如 SQL 语句、异常栈),而不是只盯着浏览器。
关键标签 / 关键词
Flask, Web编程, Python, 路由, 模板, Jinja, GET, POST, 表单, 模板继承, 服务端验证, SQLite, SQL注入, Session, Cookie, MVC, API, JSON, 静态文件, CS50x, Lecture 9
生成时间:2026-09-02 17:08:10 生成平台:VideoNote