系列文章:Python 基础语法入门 本文目标:从零理解 Web 开发基本原理,用 Flask 搭建第一个能跑的网站,掌握路由、模板、表单、Session、静态文件、蓝图、错误处理与 gunicorn 部署,最后结合 SQLite 做一个完整待办应用。
一、从命令行到 Web
前面 29 篇文章里,我们写的程序大多是"命令行跑一下就结束"。但现实世界里,绝大多数软件都以网站 或App形态存在:用户在浏览器点一点,服务器处理完返回结果。
为什么要学 Web 开发?
- 用户零安装:打开浏览器就能用,不用下载安装包。
- 跨平台:一份后端,Windows / Mac / 手机浏览器都能访问。
- 数据集中:数据存在服务器,多用户共享、随时备份。
- Python 生态强:Django、Flask、FastAPI 让 Python 写后端非常高效。
学完本篇,你就能用 Python 写一个真正能在浏览器访问的小网站。
二、Web 基础概念
2.1 HTTP 请求与响应
Web 的核心协议是 HTTP(超文本传输协议)。一次交互就是:
浏览器(客户端) ------HTTP 请求------> 服务器
浏览器(客户端) <------HTTP 响应------ 服务器
一个 HTTP 请求包含:方法 、URL 、请求头 、请求体 。 一个 HTTP 响应包含:状态码 、响应头 、响应体(HTML/JSON 等)。
2.2 GET vs POST
| 方法 | 用途 | 参数位置 | 典型场景 |
|---|---|---|---|
| GET | 获取资源 | URL 查询串 ?key=val |
打开页面、搜索 |
| POST | 提交数据 | 请求体 | 登录、注册、上传 |
| PUT | 更新整体 | 请求体 | 修改资料 |
| DELETE | 删除 | URL | 删除记录 |
原则:GET 不应修改服务器数据(只读),修改用 POST。
2.3 URL 结构
https://www.example.com:443/users/123?role=admin#section
\___/ \_______________/\_/\____/\_/ \________/ \______/
协议 域名 端口 路径 锚点 查询参数 锚点
2.4 状态码
| 码段 | 含义 | 常见 |
|---|---|---|
| 2xx | 成功 | 200 OK、201 Created |
| 3xx | 重定向 | 301 永久、302 临时、304 缓存 |
| 4xx | 客户端错误 | 404 找不到、403 禁止、400 参数错 |
| 5xx | 服务器错误 | 500 内部错误、502 网关错误 |
2.5 Cookie 与 Session 简介
HTTP 是无状态协议,服务器记不住"这个浏览器上次来过"。为了保持登录状态:
- Cookie:服务器让浏览器在本地存一小段数据,每次请求自动带上。
- Session:数据存在服务器端,浏览器只存一个 Session ID(通过 Cookie 传递),更安全。
三、Flask 简介
3.1 为什么选 Flask
Flask 是 Python 轻轻量级 Web 框架,核心特点:
- 微框架:核心只做路由 + 请求响应,其他功能用扩展按需加。
- 上手快:5 行代码就能跑起一个网站。
- 灵活:不强制项目结构、不强制数据库、不强制 ORM。
- 文档优秀、社区活跃、扩展丰富。
3.2 vs Django vs FastAPI
| 框架 | 风格 | 适合 | 学习曲线 |
|---|---|---|---|
| Flask | 微框架,灵活 | 小中型、原型、API | 平缓 |
| Django | 全家桶,约定优于配置 | 中大型、内容管理 | 较陡 |
| FastAPI | 异步、类型注解、自动文档 | 高性能 API | 中等 |
本文选 Flask:最适合入门,概念少、代码短、能快速建立 Web 直觉。
四、安装与第一个程序
4.1 安装
pip install flask
4.2 Hello World
新建 app.py:
from flask import Flask
app = Flask(__name__) # 创建 Flask 应用实例
@app.route('/') # 路由:访问根路径时执行下面函数
def hello():
return 'Hello, World!' # 返回字符串作为响应体
if __name__ == '__main__':
app.run(debug=True) # 启动开发服务器,开启调试模式
运行:
python app.py
运行结果:
* Running on http://127.0.0.1:5000
* Debug mode: on
浏览器打开 http://127.0.0.1:5000,看到 Hello, World!。
4.3 路由 route
@app.route(path, methods=[...]) 把一个 URL 绑定到一个函数(视图函数)。
@app.route('/about')
def about():
return '关于我们页面'
@app.route('/user/<username>') # 动态路由
def show_user(username):
return f'用户:{username}'
@app.route('/post/<int:post_id>') # 指定类型转换器
def show_post(post_id):
return f'文章编号 {post_id}'
访问:
/about→关于我们页面/user/张三→用户:张三/post/42→文章编号 42
常用转换器:string(默认)、int、float、path(含斜杠)、uuid。
4.4 运行与 debug 模式
app.run(debug=True) 的作用:
- 代码修改自动重启:改完保存,服务器自动重载,不用手动停再启。
- 出错显示详细堆栈:浏览器直接显示错误 traceback,方便调试。
注意:
debug=True仅用于开发,生产环境务必关闭。
五、返回 HTML 模板
直接 return 字符串写 HTML 太痛苦。Flask 用 Jinja2 模板引擎,把 HTML 和数据分离。
5.1 项目结构
myapp/
├── app.py
├── templates/ # HTML 模板放这里
│ └── index.html
└── static/ # 静态文件(CSS/JS/图片)放这里
└── style.css
5.2 render_template
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html', name='陈序员', age=18)
templates/index.html:
<!DOCTYPE html>
<html>
<head><title>首页</title></head>
<body>
<h1>你好,{{ name }}!</h1>
<p>年龄:{{ age }}</p>
</body>
</html>
浏览器访问 /,看到渲染后的页面。{``{ name }} 会被替换成传入的 name 值。
5.3 Jinja2 模板语法详解
变量 :{``{ var }}
条件判断:
{% if age >= 18 %}
<p>已成年</p>
{% else %}
<p>未成年</p>
{% endif %}
循环:
<ul>
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
</ul>
过滤器 :{``{ name | upper }} 转大写、{``{ price | round(2) }} 保留两位小数、{``{ text | truncate(10) }} 截断。
模板继承(复用布局):
base.html:
<html>
<head><title>{% block title %}默认标题{% endblock %}</title></head>
<body>
<nav>导航栏</nav>
{% block content %}{% endblock %}
<footer>页脚</footer>
</body>
</html>
index.html:
{% extends 'base.html' %}
{% block title %}首页{% endblock %}
{% block content %}
<h1>首页内容</h1>
{% endblock %}
include(引入片段):
{% include 'header.html' %}
5.4 完整待办列表页面示例
app.py:
from flask import Flask, render_template
app = Flask(__name__)
todos = ['学Python', '学Flask', '写项目']
@app.route('/todos')
def todo_list():
return render_template('todos.html', todos=todos, title='待办列表')
if __name__ == '__main__':
app.run(debug=True)
templates/todos.html:
<!DOCTYPE html>
<html>
<head><title>{{ title }}</title></head>
<body>
<h1>{{ title }}</h1>
<ul>
{% for t in todos %}
<li>{{ loop.index }}. {{ t }}</li>
{% else %}
<li>暂无待办</li>
{% endfor %}
</ul>
<p>共 {{ todos | length }} 项</p>
</body>
</html>
访问 /todos,页面显示编号列表和总数。loop.index 是循环内置变量,表示当前是第几次循环。
六、处理表单
6.1 GET 查询参数 request.args
from flask import Flask, request
app = Flask(__name__)
@app.route('/search')
def search():
keyword = request.args.get('q', '') # 从查询串取 q,默认空串
page = request.args.get('p', 1, type=int)
return f'搜索关键词:{keyword},第 {page} 页'
访问 /search?q=flask&p=2 → 搜索关键词:flask,第 2 页。
6.2 POST 表单数据 request.form
from flask import Flask, request, render_template
app = Flask(__name__)
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
if username == 'admin' and password == '123456':
return f'登录成功,欢迎 {username}'
return '用户名或密码错误'
return render_template('login.html')
templates/login.html:
<!DOCTYPE html>
<html>
<head><title>登录</title></head>
<body>
<form method="post">
<p>用户名:<input type="text" name="username"></p>
<p>密码:<input type="password" name="password"></p>
<button type="submit">登录</button>
</form>
</body>
</html>
流程:GET 访问显示表单 → 填写提交 → POST 处理校验。
6.3 文件上传 request.files
import os
from flask import Flask, request, render_template
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'uploads'
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
@app.route('/upload', methods=['GET', 'POST'])
def upload():
if request.method == 'POST':
f = request.files['file'] # 获取上传文件
if f:
path = os.path.join(app.config['UPLOAD_FOLDER'], f.filename)
f.save(path) # 保存到磁盘
return f'已上传:{f.filename}'
return render_template('upload.html')
templates/upload.html:
<form method="post" enctype="multipart/form-data">
<input type="file" name="file">
<button type="submit">上传</button>
</form>
注意:表单上传文件必须加
enctype="multipart/form-data"。
6.4 重定向 redirect 和 url_for
from flask import Flask, redirect, url_for
app = Flask(__name__)
@app.route('/')
def index():
return '首页'
@app.route('/old')
def old():
return redirect(url_for('index')) # 重定向到首页视图函数对应的 URL
访问 /old 会自动跳转到 /。url_for('函数名') 根据视图函数名反查 URL,比硬编码路径更安全(改路由不用改代码)。
七、静态文件
CSS、JS、图片放 static/ 目录,Flask 自动提供访问。模板里用 url_for('static', filename='...') 引用:
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<script src="{{ url_for('static', filename='app.js') }}"></script>
<img src="{{ url_for('static', filename='logo.png') }}">
目录结构:
myapp/
├── app.py
├── templates/
└── static/
├── style.css
├── app.js
└── logo.png
访问 http://127.0.0.1:5000/static/style.css 即可拿到 CSS 文件。
八、Cookie 和 Session
HTTP 协议本身是无状态 的------服务器处理完一次请求就"忘记"了这次交互,下一次请求来了,服务器并不知道是不是同一个用户。这在单纯浏览网页时没问题,但涉及登录、购物车时就不行了:用户登录后翻几个页面,总不能每页都重新输密码。为此 Web 引入了两种状态保持机制:Cookie 把少量数据存在浏览器端,每次请求自动带上;Session 把数据存在服务器端,浏览器只持有一个 ID,更安全、容量更大。下面分别演示 Flask 中的用法。
8.1 Cookie
from flask import Flask, request, make_response
app = Flask(__name__)
@app.route('/setcookie')
def set_cookie():
resp = make_response('Cookie 已设置')
resp.set_cookie('username', 'chen', max_age=3600) # 1小时过期
return resp
@app.route('/getcookie')
def get_cookie():
val = request.cookies.get('username', '未设置')
return f'Cookie username = {val}'
8.2 Session
Session 数据存服务器,更安全。Flask 需要设置 secret_key:
from flask import Flask, session, redirect, url_for, request
app = Flask(__name__)
app.secret_key = 'your-secret-key-change-in-prod' # 必须设置,用于加密签名
@app.route('/login', methods=['POST'])
def login():
session['user'] = request.form.get('username') # 写入 session
return redirect(url_for('profile'))
@app.route('/profile')
def profile():
user = session.get('user')
if not user:
return '未登录', 401
return f'当前用户:{user} <a href="/logout">退出</a>'
@app.route('/logout')
def logout():
session.pop('user', None) # 清除 session
return '已退出'
生产环境若多进程/多服务器,需把 Session 存到 Redis(用
flask-session扩展),默认实现只存内存。
九、结合 SQLite 做一个完整待办应用
前面分别学了路由、模板、表单和数据库,现在把它们串起来,做一个真正"能用"的小网站:一个待办事项清单,支持添加、标记完成、删除,数据持久化在 SQLite 里,重启服务器也不丢。这个例子虽然简单,却涵盖了 Web 开发最核心的"增删改查 + 模板渲染 + 数据持久化"闭环,理解了它,做博客、做后台管理都是同样的套路。
9.1 项目结构
todo_app/
├── app.py
└── templates/
└── todo.html
9.2 app.py
import sqlite3
from flask import Flask, request, render_template, redirect, url_for, g
app = Flask(__name__)
app.config['DATABASE'] = 'todo.db'
def get_db():
if 'db' not in g:
g.db = sqlite3.connect(app.config['DATABASE'])
g.db.row_factory = sqlite3.Row
return g.db
@app.teardown_appcontext
def close_db(exc):
db = g.pop('db', None)
if db:
db.close()
def init_db():
db = get_db()
db.execute('''
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
done INTEGER DEFAULT 0
)
''')
db.commit()
@app.route('/')
def index():
db = get_db()
todos = db.execute("SELECT * FROM todos ORDER BY id DESC").fetchall()
return render_template('todo.html', todos=todos)
@app.route('/add', methods=['POST'])
def add():
title = request.form.get('title', '').strip()
if title:
db = get_db()
db.execute("INSERT INTO todos (title) VALUES (?)", (title,))
db.commit()
return redirect(url_for('index'))
@app.route('/toggle/<int:tid>')
def toggle(tid):
db = get_db()
db.execute("UPDATE todos SET done = 1 - done WHERE id = ?", (tid,))
db.commit()
return redirect(url_for('index'))
@app.route('/delete/<int:tid>')
def delete(tid):
db = get_db()
db.execute("DELETE FROM todos WHERE id = ?", (tid,))
db.commit()
return redirect(url_for('index'))
with app.app_context():
init_db()
if __name__ == '__main__':
app.run(debug=True)
9.3 templates/todo.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>待办清单</title>
<style>
body { font-family: sans-serif; max-width: 500px; margin: 40px auto; }
.done { text-decoration: line-through; color: gray; }
li { margin: 6px 0; }
</style>
</head>
<body>
<h1>📝 待办清单</h1>
<form method="post" action="/add">
<input type="text" name="title" placeholder="输入待办事项" required>
<button type="submit">添加</button>
</form>
<ul>
{% for t in todos %}
<li class="{{ 'done' if t.done else '' }}">
{{ t.title }}
<a href="/toggle/{{ t.id }}">{{ '取消' if t.done else '完成' }}</a>
<a href="/delete/{{ t.id }}">删除</a>
</li>
{% else %}
<li>暂无待办,添加一个吧~</li>
{% endfor %}
</ul>
</body>
</html>
运行 python app.py,浏览器打开 http://127.0.0.1:5000,即可添加、完成、删除待办事项,数据持久化在 todo.db。
运行效果说明:
- 输入"学Flask"点添加 → 列表出现该条。
- 点"完成" → 文字加删除线变灰。
- 点"删除" → 该条消失。
- 重启服务器 → 数据仍在(已存 SQLite)。
十、错误处理
10.1 自定义 404 页面
from flask import Flask, render_template
app = Flask(__name__)
@app.errorhandler(404)
def not_found(e):
return render_template('404.html'), 404
@app.errorhandler(500)
def server_error(e):
return '服务器内部错误,请稍后再试', 500
templates/404.html:
<!DOCTYPE html>
<html>
<head><title>页面不存在</title></head>
<body>
<h1>404 😕</h1>
<p>你访问的页面不存在。</p>
<a href="/">回首页</a>
</body>
</html>
10.2 abort 主动抛错
from flask import Flask, abort
app = Flask(__name__)
@app.route('/user/<int:uid>')
def user(uid):
if uid <= 0:
abort(400, description='用户 id 必须为正整数')
return f'用户 {uid}'
访问 /user/-1 → 返回 400 错误。
十一、蓝图 Blueprint(项目模块化)
项目变大后,所有路由写在一个 app.py 里会臃肿。**蓝图(Blueprint)**把路由按模块拆分,是 Flask 项目模块化的核心手段,相当于 Django 里的"app"概念。
11.1 为什么要用蓝图
小型项目一个文件够用,但当功能增多(用户认证、博客文章、后台管理、API 接口),全堆在 app.py 里会有几个问题:
- 文件几千行,难以维护。
- 多人协作容易冲突。
- 路由命名难管理,URL 前缀容易写错。
蓝图把一组相关路由封装成独立模块,注册时统一加 URL 前缀,结构清晰、便于团队分工。
11.2 目录结构
myapp/
├── app.py
├── auth.py
└── blog.py
auth.py:
from flask import Blueprint
bp = Blueprint('auth', __name__, url_prefix='/auth') # 该蓝图下所有路由加 /auth 前缀
@bp.route('/login')
def login():
return '登录页'
@bp.route('/logout')
def logout():
return '退出'
app.py:
from flask import Flask
from auth import bp as auth_bp
app = Flask(__name__)
app.register_blueprint(auth_bp) # 注册蓝图
if __name__ == '__main__':
app.run(debug=True)
访问 /auth/login、/auth/logout 即可。蓝图让大型项目结构清晰、便于团队协作。
11.3 蓝图里访问应用配置
蓝图内部可以通过 current_app 访问主应用,避免循环导入:
from flask import Blueprint, current_app
bp = Blueprint('api', __name__, url_prefix='/api')
@bp.route('/config')
def show_config():
# 读取主应用的配置,而不是直接 import app
return str(current_app.config.get('API_KEY', '未设置'))
蓝图是中大型 Flask 项目的标配,建议从第二个项目起就养成用蓝图的习惯。
十三、部署:用 gunicorn 部署到生产
Flask 自带的开发服务器不能用于生产 (单线程、性能差)。生产用 gunicorn (Linux/macOS)或 uWSGI。
13.1 安装
pip install gunicorn
13.2 启动
假设 app.py 里应用实例叫 app:
gunicorn -w 4 -b 0.0.0.0:8000 app:app
参数说明:
-w 4:4 个 worker 进程(建议 CPU 核数 × 2 + 1)。-b 0.0.0.0:8000:监听所有网卡的 8000 端口。app:app:app.py文件里的app对象。
运行结果:
[2023-08-15 10:00:00] [INFO] Starting gunicorn 21.0
[2023-08-15 10:00:00] [INFO] Listening at: http://0.0.0.0:8000
[2023-08-15 10:00:00] [INFO] Booting worker with pid: 12345
浏览器访问 http://服务器IP:8000 即可。
13.3 常用搭配
- Nginx:放前面做反向代理、静态文件、HTTPS。
- systemd / supervisor:守护进程,崩溃自动重启。
- Docker:容器化部署,环境一致。
生产典型架构:
用户 → Nginx → gunicorn (Flask) → SQLite/MySQL
十四、配置管理与扩展生态
14.1 配置分离
实际项目里,开发用 SQLite、生产用 MySQL,密钥也不同。把配置写死在代码里是大忌,一旦上线就会暴露密钥或连错数据库。Flask 支持用配置类分离环境:
class Config:
SECRET_KEY = 'dev-secret'
class DevelopmentConfig(Config):
DEBUG = True
DATABASE = 'dev.db'
class ProductionConfig(Config):
DEBUG = False
DATABASE = 'prod.db'
app = Flask(__name__)
app.config.from_object(DevelopmentConfig) # 切换环境只改这一行
print(app.config['DEBUG']) # True
更安全的做法是从环境变量读取敏感信息,避免写进代码仓库:
import os
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'default')
14.2 常用 Flask 扩展
Flask 核心精简,复杂功能靠扩展按需引入,这是它"微框架"哲学的体现:
| 扩展 | 作用 |
|---|---|
| Flask-SQLAlchemy | ORM 集成 |
| Flask-WTF | 表单处理 + CSRF 防护 |
| Flask-Login | 用户登录状态管理 |
| Flask-Migrate | 数据库迁移(类似 Django migrate) |
| Flask-RESTful | 快速构建 REST API |
| Flask-Caching | 缓存 |
| Flask-Mail | 发邮件 |
安装示例:
pip install flask-sqlalchemy flask-wtf flask-login
扩展生态是 Flask 的核心竞争力:核心保持轻量,功能随用随装,不会一开始就背一堆用不上的东西。
14.3 应用工厂模式
中大型项目推荐用工厂函数创建 app,便于测试和多实例部署:
from flask import Flask
def create_app(config_name='development'):
app = Flask(__name__)
app.config.from_object(f'app.config.{config_name}Config')
# 注册蓝图
from app.auth import bp as auth_bp
app.register_blueprint(auth_bp)
return app
# run.py
app = create_app('production')
这样不同环境创建不同配置的 app,测试时也能轻松造一个独立实例,互不干扰。
十五、最佳实践
- 生产关闭 debug :
app.run(debug=False),或直接用 gunicorn。 - 密钥保密 :
secret_key从环境变量读取,不要硬编码进代码。 - 配置分离 :开发/生产配置用
app.config.from_object()切换。 - 用蓝图拆模块:项目一大就分 Blueprint。
- 数据库连接用
g+teardown_appcontext:请求级连接,自动关闭。 - 表单校验 :复杂表单用
flask-wtf扩展,自带 CSRF 防护。 - 永远参数化 SQL:防 SQL 注入(见上一篇)。
- 静态文件生产交 Nginx:Flask 不擅长处理静态文件。
- 日志记录 :用
app.logger记录请求和错误,便于排查。 - HTTPS:生产必须上 HTTPS,用 Nginx + Let's Encrypt 免费证书。
十六、小结
至此,Flask Web 开发入门的核心内容已全部讲完。本文从"为什么要学 Web"讲起,介绍了 HTTP、GET/POST、状态码、Cookie/Session 等 Web 基础;然后带大家用 Flask 一步步搭起一个真实网站:
- 第一个程序:5 行 Hello World 跑起来。
- 路由与动态参数 :
@app.route、<int:id>转换器。 - 模板 Jinja2:变量、条件、循环、过滤器、继承、include。
- 表单处理 :
request.args/request.form/request.files、redirect+url_for。 - 静态文件 :
static/目录。 - Cookie 与 Session:保持登录状态。
- 完整待办应用:Flask + SQLite + 模板,能增删改查并持久化。
- 错误处理 :
errorhandler、abort。 - 蓝图 Blueprint:项目模块化。
- gunicorn 部署:生产环境正确姿势。
核心心法:Flask 把"URL → 函数 → 响应"这条链路做到极简,理解了它就理解了 Web 后端的本质。 后续可继续学 Django(全家桶)、FastAPI(异步高性能)、Flask 扩展(数据库、登录、表单)等进阶方向。
至此,Python 基础语法入门系列的核心主题已覆盖:从变量、流程控制、函数、类,到文件、异常、模块、迭代器、装饰器,再到本系列的数据库、正则、Web 开发。希望这个系列能帮你打下扎实的 Python 地基,接下来就可以向数据分析、自动化、爬虫、机器学习等方向自由进发了!
作者:全栈陈序员 系列:Python 基础语法入门 专栏:Python 从零到一 如果对你有帮助,欢迎点赞、收藏、关注 ❤️