🐍 第9章 使用 Flask 建置网站服务
📄 一句话概括

Flask 的全部内容是「一个网址对应一个函数」:有人访问这个地址,就调那个函数, 把返回值当网页正文发回去。 找得到叫 200,找不到叫 404,服务器自己坏了叫 500。 把内部错误报成 404,排查的人会以为是地址写错,白找半天。 绑 127.0.0.1 只有本机能访问,绑 0.0.0.0 才轮得到别人。 「我起了服务,同事打不开」八成就是这个原因。

📝 本章和前面几章的关系

前八章的程序都是「跑一下、出结果、结束」。从这一章开始,程序要一直挂着等别人访问。 教材第 9 章开头三节是【认识 HTTP 通信协议】和【Flask 网站应用程序设计】, 还有一节【设置 Flask 服务器的 IP 地址和端口】。 本篇的演示是真跑的:用 make_server 把服务挂在后台线程里,端口让系统分配, 然后用 requests 真发请求、真拿响应。不是「示意代码」。 Flask 是这一章唯一需要装的第三方包,前面几篇 pip install flask 那一步已经做了。

一、三行起一个网站

from flask import Flask,app = Flask(__name__),然后@app.route('/') 下面挂一个函数。 那个 @app.route('/') 叫装饰器(附录 A 会讲它的原理),意思是「有人访问根路径, 就调下面这个函数」。 函数返回什么,浏览器就看到什么。返回一个字符串,那就是网页正文。

这三行就是 Flask 的全部核心。 剩下的一切——参数、模板、表单、 数据库——都是在这套「地址对函数」的骨架上长出来的。 启动要写 app.run()。 但这一句会阻塞,一直占着不放,所以演示里用的是 make_server,它把服务挂在后台线程里, 主线程还能继续发请求。

二、一次请求到底交换了什么

演示第二段打出来六项:请求地址、状态码 200、内容类型 text/html; charset=utf-8、正文 15 字节、 正文内容、服务器标识。 这一来一回就是一个完整的 HTTP 交换。 客户端说「我要 GET /」,服务器回一个状态码、几个头、 一段正文。 状态码是三个数字,第一位决定大类:2 开头是成功、3 开头是重定向、4 开头是客户端错了、 5 开头是服务器错了。

Content-Type 里那个 charset=utf-8 很关键。 没有它,浏览器可能按别的编码解释, 中文就成乱码。这是 Flask 默认帮你加上的——它知道你返回的是字符串。 正文 15 字节这个数也对得上:「你好,世界」是 5 个字符,其中逗号是全角的, UTF-8 里每个汉字和全角标点都占 3 字节,5 × 3 = 15。 不过别在正文里硬算字节数。 同一个汉字在 UTF-8 里 3 字节、在 GBK 里 2 字节,半角标点只占 1 字节,混在一起心算必错。 程序里用 len(s.encode('utf-8')) 算,正文里说「十几个字节」就够了。

三、网址里带参数

@app.route('/hello/<name>') 里的 <name> 是占位符,函数多收一个参数 name。 默认是字符串。想要整数,写 <int:no>。演示里 /item/7 返回 200,/item/abc 返回 404。 注意 404 不是「报错」,是「这个地址根本不存在」。 因为 abc 不符合 int, Flask 认为没有哪条规则能匹配这个网址。

这种设计叫 REST 风格的路径参数:把变化的那个东西放进路径,而不是放进问号后面。 /item/7 比 /item?id=7 好在两处:一是好读,二是被搜索引擎当成不同的页面。 商品页、用户页、文章页都这么写。

四、404 和 500 是两个不同的道歉

演示第四段三个地址各试了一次:/ok 是 200、/missing 是 404、/boom 是 500。 404 的意思是「你要的东西我这没有」。可能是网址打错了,可能是内容被删了。 500 的意思是「我这边坏了」。代码抛了没接住的异常,服务端自己出问题。

这两个千万别混。 把内部错误报成 404,排查的人会去查网址、查路由,查半天才发现是数据库连不上。 反过来也一样:本该 404 的地方返回 200 加一句「找不到」,监控系统就永远发现不了坏链。 Flask 默认的 404 页和 500 页都很朴素,正文一两百字节。 真实项目会自己写错误页,但状态码必须对——页面好不好看是次要的。

五、host 和 port 到底在说什么

写法 谁能访问 什么时候用
127.0.0.1:5000 只有本机 自己调试
0.0.0.0:5000 同一局域网的机器都能访问 给同事看、手机测试
0.0.0.0:80 同上,但端口是默认端口 网址不用带端口号

127.0.0.1 是本机回环地址,数据包根本不出网卡。 所以别人访问不到,不是防火墙的问题,是压根没往外发。 0.0.0.0 表示「所有网卡都听」。绑这个,同一个局域网里的机器才能连上。 很多新手说「我起了服务,同事打不开」,八成就是绑在 127.0.0.1 上。

但反过来也要小心:绑 0.0.0.0 等于把服务暴露给整个网络。 没有密码保护的东西别这么干,尤其在公司的办公网里。 端口也有讲究。1024 以下是系统保留端口,绑定要管理员权限; 80 是 HTTP 的默认端口,用了它网址就不用带 :端口号。开发阶段用 5000 或 8000 就行。

六、动手 Demo:三行代码起一个网站

用 make_server 把 Flask 挂在后台线程,端口让系统分配,然后真发请求。 先看最小应用,再看路径参数、404 与 500 的差别。 最后把 host 的三种写法列出来。

import logging, threading
from flask import Flask, abort
from werkzeug.serving import make_server
import requests

logging.getLogger('werkzeug').setLevel(logging.ERROR)

app = Flask(__name__)

@app.route('/')
def home():
    return '你好,世界'

@app.route('/hello/<name>')
def hello(name):
    return '你好,%s' % name

@app.route('/item/<int:no>')
def item(no):
    return '第 %d 号商品,总价 %.2f' % (no, no * 1.5)

@app.route('/ok')
def ok():
    return '一切正常'

@app.route('/boom')
def boom():
    return 1 / 0

srv = make_server('127.0.0.1', 0, app)
port = srv.server_port
threading.Thread(target=srv.serve_forever, daemon=True).start()
B = 'http://127.0.0.1:%d' % port

print('① 最小的应用')
print("    from flask import Flask")
print("    app = Flask(__name__)")
print("    @app.route('/')")
print('    def home():')
    return '你好,世界'
print("  装饰器 @app.route('/') 的意思是:有人访问根路径,就调下面这个函数,")
print('  把它的返回值当网页正文发回去。')

print('② 真起服务,真发请求')
r = requests.get(B + '/', timeout=5)
print('  请求 %s/' % B)
print('  状态码      %d' % r.status_code)
print('  内容类型    %s' % r.headers.get('Content-Type'))
print('  正文长度    %d 字节' % len(r.content))
print('  正文        %s' % r.text)
print('  服务器      %s' % r.headers.get('Server'))
print('  这一来一回就是一个完整的 HTTP 交换。')

print('③ 网址里带参数')
for path in ('/hello/小王', '/item/7', '/item/abc'):
    r = requests.get(B + path, timeout=5)
    body = r.text.strip().splitlines()[0][:24] if r.status_code == 200 else '(Flask 自带的 404 页)'
    print('  %-14s 状态 %d  %s' % (path, r.status_code, body))
print('  <name> 默认是字符串,<int:no> 限定只能是整数。')
print('  写 abc 进去直接 404——不是报错,是「这个地址根本不存在」。')

print('④ 找不到页面、出错页面')
for path in ('/ok', '/missing', '/boom'):
    r = requests.get(B + path, timeout=5)
    print('  %-10s 状态 %d  正文 %d 字节' % (path, r.status_code, len(r.content)))
print('  404 是「没这个东西」,500 是「我这边坏了」。')
print('  这两个状态码别混:把内部错误报成 404,排查的人会以为是地址写错,白找半天。')

print('⑤ host 和 port 到底在说什么')
print('| 写法 | 谁能访问 | 什么时候用 |')
print('|---|---|---|')
print('| 127.0.0.1:5000 | 只有本机 | 自己调试 |')
print('| 0.0.0.0:5000 | 同一局域网的机器都能访问 | 给同事看、手机测试 |')
print('| 0.0.0.0:80 | 同上,但端口是默认端口 | 网址不用带端口号 |')
print('  本次实际用的是 127.0.0.1:%d(端口由系统分配)' % port)
srv.shutdown()

实跑输出:

--------------------------------------------------------------------
三行代码起一个网站
--------------------------------------------------------------------

  ① 最小的应用
  --------------------------------------------------------------------
    from flask import Flask
    app = Flask(__name__)

    @app.route('/')
    def home():
        return '你好,世界'
  装饰器 @app.route('/') 的意思是:有人访问根路径,就调下面这个函数,
  把它的返回值当网页正文发回去。

  ② 真起服务,真发请求
  --------------------------------------------------------------------
    请求 http://127.0.0.1:62795/
    状态码      200
    内容类型    text/html; charset=utf-8
    正文长度    15 字节
    正文        你好,世界
    服务器      Werkzeug/3.1.9 Python/3.13.14
  这一来一回就是一个完整的 HTTP 交换:客户端说「GET /」,服务器回一个状态码、几个头、一段正文。
  Content-Type 里那个 charset=utf-8 很关键。
  没有它,浏览器可能按别的编码解释,中文就成乱码——这是 Flask 默认帮你加上的。

  ③ 网址里带参数
  --------------------------------------------------------------------
    /hello/小王        状态 200  你好,小王
    /item/7          状态 200  第 7 号商品,总价 10.50
    /item/abc        状态 404  (Flask 自带的 404 页)
  <name> 默认是字符串,<int:no> 限定只能是整数。
  写 abc 进去直接 404——不是报错,是「这个地址根本不存在」。
  这种设计叫 REST 风格的路径参数:把变化的那个东西放进路径,而不是放进问号后面。
  商品页、用户页、文章页都这么写。

  ④ 找不到页面、出错页面
  --------------------------------------------------------------------
    /ok          状态 200  正文 6 字节
    /missing     状态 404  正文 207 字节
    /boom        状态 500  正文 265 字节
  404 是「没这个东西」,500 是「我这边坏了」。
  这两个状态码别混:把内部错误报成 404,排查的人会以为是地址写错,白找半天。

  ⑤ host 和 port 到底在说什么
  --------------------------------------------------------------------

| 写法 | 谁能访问 | 什么时候用 |
|---|---|---|
| 127.0.0.1:5000 | 只有本机 | 自己调试 |
| 0.0.0.0:5000 | 同一局域网的机器都能访问 | 给同事看、手机测试 |
| 0.0.0.0:80 | 同上,但端口是默认端口 | 网址不用带端口号 |

  127.0.0.1 是本机回环地址,数据包根本不出网卡。0.0.0.0 表示「所有网卡都听」,这才轮得到别人访问。
  很多新手说「我起了服务,同事打不开」,八成就是绑在 127.0.0.1 上。
  但反过来,绑 0.0.0.0 就等于把服务暴露给整个网络,没有密码保护的东西别这么干。

第一段那三行是 Flask 的全部核心。剩下的一切都是在这套「地址对函数」的骨架上长出来的。 第二段六个数里,状态码和 Content-Type 最该记。charset=utf-8 没有的话,中文可能变乱码。 第三段那个 /item/abc 返回 404 而不是报错,原因在 int 限定:abc 不符合类型, Flask 认为没有规则能匹配这个网址。 第四段 404 和 500 的正文长度差得不多(207 和 265),光看大小分不出来,得看状态码。 这就是为什么状态码必须对。 演示最后那句 srv.shutdown() 别漏。不写的话服务一直挂着,脚本退不出去。

30 秒实验

  1. 把演示里的 @app.route('/item/<int:no>') 改成 <no>(去掉 int),再请求 /item/abc, 看状态码变成几。
  2. 加一个路由 /sum/<int:a>/<int:b>,返回两数之和,访问 /sum/3/4 看结果。
  3. 把 make_server('127.0.0.1', 0, app) 的地址改成 '0.0.0.0', 再在手机浏览器里输你这台电脑的局域网 IP 加端口,看能不能打开。

常见误区

  1. 「404 和 500 都是出错,差不多。」404 是「你要的东西没有」,500 是「我这边坏了」。 混起来会让排查的人找错方向。
  2. 「服务起了,同事打不开,是防火墙问题。」先看绑的是不是 127.0.0.1。 绑回环地址的数据包根本不出网卡。
  3. 「前端已经校验过了,服务端不用再校验。」前端校验挡得住手滑,挡不住直接发请求的人。 服务端校验是不能省的那一道。
自测三问(点开对答案) 1. `/item/7` 返回 200,`/item/abc` 返回 404,为什么是 404 而不是 500? —— 因为路由写的是 ``,只匹配整数。 `abc` 不匹配任何一条规则,Flask 就认为「这个地址不存在」,回 404。 500 是「地址存在但代码出错了」,这里代码根本没被调用。 2. 服务起在本机,自己浏览器能开,同事打不开。最可能是什么原因? —— 绑在了 `127.0.0.1` 上。那是回环地址,数据包根本不出网卡,只有本机能连。 要绑 `0.0.0.0`,表示「所有网卡都听」,同一局域网的机器才连得上。 3. `Content-Type` 里的 `charset=utf-8` 有什么用? —— 告诉浏览器「这段正文是按 UTF-8 编码的」,浏览器才知道怎么解。 没有它,浏览器可能按系统默认编码(比如 GBK)解释,中文就成乱码。 Flask 返回字符串时会自动加上。

下一篇让页面长得像样一点:静态文件怎么发,模板怎么把数据填进骨架。

相关笔记