视频封面

视频元信息

  • 时长:约 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 登录为例:

  1. 用户提交用户名/密码(POST)。
  2. 服务器验证成功后,返回 Set-Cookie: session=<random> 响应头。
  3. 浏览器保存 Cookie,后续每个请求自动带上 Cookie: session=<random>。
  4. 服务器根据 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