
视频元信息
- 时长: 约3小时(包含休息时间)
内容摘要 (TL;DR)
本课是CS50本学期最后一讲,聚焦Web编程。课程从静态HTTP服务器转向Flask框架,讲解如何用Python动态生成HTML,处理用户输入(GET/POST),使用模板继承(Jinja2),引入数据库(SQLite),实现会话与Cookie,构建购物车和搜索API,最终整合HTML/CSS/JavaScript/Python/SQL构建完整Web应用。
1. 课程引入与回顾 [01:16]
- 本课是CS50今年(2025)在演播室的最后一周。
- 主题:Web编程,将Python与Flask框架结合,整合HTML、CSS、JavaScript以及前几周学过的所有概念。
- 理解:本课目标是将所有积木组合起来,让学生能构建自己的项目,无论是本周的作业还是最终项目。
上一周回顾
- 上次课介绍了HTTP(超文本传输协议),浏览器和web服务器通过HTTP通信,底层还有IP和TCP。
- 上周用
http-server命令启动静态文件服务器,它只能简单地从代码空间发送文件(.html、图片、JS、CSS)到浏览器。 - 关键点:上周所有内容都是静态的——写文件、保存、刷新浏览器查看;只有JavaScript带来了一点客户端动态性(如自动补全)。
本周新方向
- 转向服务端编程(server-side programming):用Python写代码,用函数、循环、条件语句来动态生成HTML。
- 理解:对于有几十、几百、几千个网页的网站,不可能手动生成每个页面,必须通过编程动态生成。
2. URL结构与路径概念 [04:16]
- 回顾URL结构:
https://www.example.com/,斜杠/通常指默认网页(如index.html,或index.htm、default.aspx等)。 - 可以指定具体文件:
/file.html、/folder/、/folder/file.html,文件夹可嵌套。 - 新术语:从今天起,
/path泛指浏览器请求的所有文件和文件夹。 - 重要概念:在Web编程中,这些路径对应的文件并不一定真实存在!路径可以代表一种“资源”(resource)。
- 路由(route):指Web服务器上你选择的路径。
- 理解:例如Google的
/search?q=cat,我们上周只做了前端,今天可以实现后端(backend)——即程序员实现的部分;前端(frontend)是用户交互的部分。
3. HTTP请求与Flask框架介绍 [07:35]
- 当浏览器通过表单提交请求时,HTTP“虚拟信封”里有这样的消息:
GET /search?q=cats HTTP/2Host: ...- 其他头部信息
- 手动写代码解析这些信封很麻烦,所以使用框架/库来简化。
- Flask:Python中非常流行的微框架(micro framework),是一个相对小的库,解决了常见问题——读取信封、解析路径、提取键值对。
从http-server到flask run
- 不再使用
http-server(只能提供静态内容)。 - 运行命令:
flask run。 - Flask既是库(可导入函数),又是命令(启动Web服务器)。
- 理解:Flask更智能,它执行你的Python代码来动态生成网页,而不是直接输出文件内容。
4. 第一个Flask应用:Hello World [09:33]
- 约定需要两个文件:
app.py— Flask会查找并执行这个文件requirements.txt— 列出依赖库
代码解释
from flask import Flask
app = Flask(__name__)
@app.route("/")
def index():
return "hello, world"
- 第一行:从
flask库导入Flask类(大写F表示类/对象)。 - 第二行:
__name__是当前文件名,告诉Flask将这个文件变成Web应用,变量名为app。 - 装饰器(decorator):
@app.route("/")是Python的装饰器语法,将函数包装为路由。 index函数返回字符串“hello, world”,当用户访问/时,Flask调用此函数并将返回值发送给浏览器。- 思考:这不是唯一实现Web应用的方式,但Flask标准化了路由和返回HTML的约定。学的是Web框架的代表,而非只学Flask本身。
requirements.txt
- 内容:
Flask(大写F)。 - 安装命令:
pip install -r requirements.txt。 - 在CS50环境中已预装,但真实世界中需要此文件。
错误示范:用http-server运行Flask应用
- 运行
http-server后,浏览器点击app.py只会下载源码,而不是执行代码。 - 易错点:不要用http-server运行Flask应用,否则会暴露源代码!
正确的运行方式
- 按
Ctrl+C停止http-server,运行flask run。 - 默认端口:5000(不是http-server的8080,也不是80/443)。
- 浏览器访问后显示“hello, world”。
- 查看页面源码,只有纯文本,没有HTML标签。
5. 动态生成HTML与模板 [20:13]
- 继续用Flask返回更复杂的HTML,但直接在Python字符串中写HTML很丑陋且易出错(引号冲突)。
- 改进:将HTML放入独立的
.html文件——模板(template)。
创建templates文件夹
- 在项目文件夹中创建
templates目录,放入index.html。 - 在
app.py中导入render_template函数。
from flask import Flask, render_template
@app.route("/")
def index():
return render_template("index.html")
- 注意:Flask默认在
templates文件夹中查找模板。
6. 传递变量给模板:使用Jinja2 [29:00]
- 目标:根据用户输入动态显示“hello, david”而不是固定文本。
读取URL参数
- 在
app.py中导入request对象。 - 用
request.args存储GET参数(类似Python字典)。
from flask import Flask, render_template, request
@app.route("/")
def index():
name = request.args.get("name", "world")
return render_template("index.html", name=name)
request.args.get("name", "world"):如果URL中有?name=david则取david,否则默认world。- 易错点:如果不设默认值,用户直接访问
/会出现400 Bad Request错误。
模板中的占位符
- 在
index.html中使用双花括号语法:
hello, {{ name }}
- Flask通过Jinja2模板引擎将
name变量替换为实际值。 - 理解:
name=name看似重复,左边是模板中的占位符名称,右边是Python变量值。
7. 表单与POST方法 [35:41]
- 用户不应手动编辑URL来输入数据,应使用Web表单。
创建表单
<form action="/greet" method="get">
<input name="name" type="text" placeholder="Name" autocomplete="off" autofocus>
<button type="submit">Greet</button>
</form>
autocomplete="off"关闭自动填充,autofocus让光标自动聚焦。action="/greet"指定提交到/greet路由,method="get"使参数显示在URL中。
添加新路由
@app.route("/greet")
def greet():
name = request.args.get("name", "world")
return render_template("greet.html", name=name)
- 创建
greet.html模板。 - 遇到404/500错误时,查看终端中Flask的日志定位问题。
- 易错点:500 Internal Server Error = 你的代码有错误,终端日志会提示。 例如模板不存在、语法错误等。
GET vs POST
- GET:参数在URL中可见,适合搜索等非敏感操作。
- POST:数据放在虚拟信封更深处,不出现在URL,适合密码、信用卡等隐私数据。
- 理解:POST避免数据出现在历史记录、自动补全中,更安全。
切换为POST
- 表单method改为
post。 - 路由需指定支持POST:
@app.route("/greet", methods=["POST"])。 - 读取POST参数:
request.form.get("name"),不是request.args! - 易错点:GET参数在
request.args,POST参数在request.form。
8. 单一路由处理GET和POST [54:00]
- 将代码合并到一条路由:
@app.route("/", methods=["GET", "POST"])
def index():
if request.method == "POST":
name = request.form.get("name")
return render_template("greet.html", name=name)
else:
return render_template("index.html")
- 用
request.method判断请求类型。 - 表单不指定
action时默认提交到当前URL(/)。 - 思考:这样减少函数数量,使代码更紧凑,但并非唯一方式。
9. Jinja2模板继承(Layout) [01:00:00]
- 问题:多个模板有大量重复HTML(
<!DOCTYPE html>,<head>,<title>,<body>等)。 - 解决方案:创建
layout.html作为基模板。
layout.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>hello</title>
<meta name="viewport" content="initial-scale=1, width=device-width">
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
{% block body %}定义了一个可被子模板填充的区块。- meta viewport标签:让网页在移动设备上响应式显示。
子模板
{% extends "layout.html" %}
{% block body %}
<!-- 每页不同的内容 -->
{% endblock %}
- 要点:
extends和block语法是Jinja2的核心,避免重复编写公共HTML。
10. 客户端验证 vs 服务端验证 [01:08:00]
- 在表单中添加
required属性可提供浏览器端验证。 - 重要提醒:客户端验证可以被轻易绕过(用开发者工具删除required属性、修改选项值)。
- 理解:恶意用户可以修改HTML选项,提交任何数据。所以必须进行服务端验证。
服务端验证示例(项目:Frosh IMs)
- 用全局列表定义有效运动:
SPORTS = ["basketball", "soccer", "ultimate frisbee"]
- 在模板中用Jinja2 for循环动态生成选项:
<select name="sport">
<option disabled selected>Sport</option>
{% for sport in sports %}
<option value="{{ sport }}">{{ sport }}</option>
{% endfor %}
</select>
- 在路由中检查
if not request.form.get("name") or request.form.get("sport") not in SPORTS:,若失败返回错误页面。 - 易错点/考点:永远不要信任用户输入,永远要有服务端验证。
11. 数据库集成:SQLite [1:15:00]
- 用全局字典存注册数据的问题:服务器重启后数据丢失。
- 改用SQLite数据库(
froshims.db)。 - 用CS50库的SQL功能:
from cs50 import SQL
db = SQL("sqlite:///froshims.db")
- 插入数据:
db.execute("INSERT INTO registrants (name, sport) VALUES(?, ?)", name, sport)
- 查询数据:
registrants = db.execute("SELECT name, sport FROM registrants")
- 模板中迭代:
{% for registrant in registrants %}
<tr><td>{{ registrant.name }}</td><td>{{ registrant.sport }}</td></tr>
{% endfor %}
- 理解:SQLite中每行作为一个字典返回,可以通过点号或方括号访问列。
12. MVC架构模式 [1:43:00]
- 本课应用代表了Web编程中的模型-视图-控制器(MVC)架构:
- Controller(控制器):
app.py,控制应用逻辑(路由、业务逻辑)。 - View(视图):
templates/中的模板,用户界面。 - Model(模型):数据库或存储层,负责读写数据。
- Controller(控制器):
- 理解:MVC是分离关注点的设计模式,帮助组织代码,不是必须遵守的规则。
13. 会话与Cookie [1:45:30]
- HTTP是无状态的,每次请求都是独立的。
- 需要记住用户是否已登录,类似游乐园在手上盖章(hand stamp)。
- Cookie:服务器在HTTP响应头中发送
Set-Cookie: session=<random_value>,浏览器存储并随后在每次请求中发送Cookie: session=<random_value>。 - Session(会话):服务器通过Cookie维护的与特定浏览器的状态。
- 理解:Cookie通常是大随机值,不是用户名/密码,避免信息泄露。广告商可用Cookie跟踪用户行为,私人浏览模式可清空Cookie。
Flask中的Session实现
from flask import Flask, render_template, request, session, redirect
from flask_session import Session
app.config["SESSION_PERMANENT"] = False
app.config["SESSION_TYPE"] = "filesystem"
Session(app)
- 登录逻辑:
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
session["name"] = request.form.get("name")
return redirect("/")
return render_template("login.html")
- 显示当前用户:
name = session.get("name") - 登出:
session.clear(),然后重定向到/。 - 要点:session是一个像全局字典一样的“每个用户的变量”,底层Flask处理所有Cookie细节。
14. 购物车示例 [2:02:00]
- 使用Session存储购物车(列表)。
- 数据库:
store.db,有books表(id, title)。 - 模板中为每本书创建表单,提交隐藏的
id字段,而不是标题(避免重名问题,使用主键)。
{% 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 %}
cart路由:
@app.route("/cart", methods=["GET", "POST"])
def cart():
if request.method == "POST":
book_id = request.form.get("id")
if "cart" not in session:
session["cart"] = []
session["cart"].append(book_id)
return redirect("/cart")
else:
books = db.execute("SELECT * FROM books WHERE id IN (?)", session.get("cart", []))
return render_template("cart.html", books=books)
- 注意:
session["cart"]是列表,首次使用时需要初始化。 - 文件系统session会生成
flask_session文件夹中的文件,保存会话数据。
15. API与JSON [2:20:00]
- 应用场景:搜索电视节目数据库(IMDB)。
- 传统方法:表单提交,服务器返回完整HTML页面。
- 改进:用JavaScript
fetchAPI异步请求数据,无需刷新页面。
服务器端返回JSON
- 导入
jsonify:
from flask import jsonify
- 修改搜索路由,返回JSON而不是HTML模板:
@app.route("/search")
def search():
query = request.args.get("q")
if query:
shows = db.execute("SELECT * FROM shows WHERE title LIKE ? LIMIT 50", f"%{query}%")
else:
shows = []
return jsonify(shows)
- 理解:JSON是JavaScript对象表示法,与Python字典格式相似,是API交换数据的标准格式。
客户端JavaScript
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 show of shows) {
html += `<li>${show.title}</li>`;
}
document.querySelector('ul').innerHTML = html;
});
- 使用
async/await处理异步HTTP请求。 - 易错点:从
fetch返回的数据应调用.json()解析,否则无法作为对象使用。 - 思考:这是API的本质——返回原始数据(JSON),而不是HTML片段。客户端负责渲染。
16. 总结与展望 [2:40:00]
- 已综合运用:Python、Flask、HTML、CSS、JavaScript、SQL、Jinja模板、会话、API。
- 能够构建完整的前后端Web应用,可用于最终项目。
- CS50课程基本结束,还有最后一次课堂总结。
关键标签/关键词
Flask, Web编程, Python, HTML, CSS, JavaScript, Jinja2, 模板继承, HTTP, GET, POST, 路由, 服务端验证, 客户端验证, SQLite, SQL, MVC, 会话, Cookie, Session, 购物车, API, JSON, jsonify, 异步请求, fetch, CS50
核心考点/易错点总结:
- GET参数用
request.args,POST参数用request.form。- 用
@app.route(..., methods=["GET", "POST"])明确支持方法。- 永远进行服务端验证,不要只依赖客户端
required。- 模板继承用
{% extends %}和{% block %}。- Session可实现“每用户”状态,存储数据在服务器端,Cookie只在浏览器端存ID。
- 数据库操作使用参数化查询(
?占位符)防止SQL注入。- API应返回JSON(用
jsonify),而非HTML片段。
生成时间:2026-09-11 15:44:16 生成平台:VideoNote www.xygy.top