一个网址六个零件,其中片段(# 后面)根本不发给服务器。 parse_qs 的每个值都是列表,因为同一个键可以出现多次。 PUT 两次结果一样是幂等,POST 两次多一条不是——重试的时候差别就出来了。
这一篇对应教材 12.2 到 12.3:表示资源位置的网址功用(URL、URI、URN)、 HTTP 方法的 CRUD 资源操作。 上一篇讲了 HTTP 长什么样,这一篇讲它上面那层:地址怎么表达、方法怎么分工。
一、网址的六个零件
| 零件 | 实测值 | 说明 |
|---|---|---|
| scheme | https |
用什么协议 |
| netloc | api.example.com:8443 |
主机加端口 |
| path | /v1/images/42 |
要哪个资源 |
| query | w=80&h=60 |
附带的条件 |
| fragment | thumb |
页面内的锚点 |
例子是 https://api.example.com:8443/v1/images/42?w=80&h=60#thumb。
hostname 只有主机名,port 只有端口,netloc 是两个合起来。
要拆开用哪个看场合:连 TCP 要 host 加 port,拼日志要 netloc。
片段那段不会发给服务器。实测 urlsplit 能拆出来,但它只是字符串处理,
真发请求时浏览器会把它砍掉。
所以前端路由里那个 #/users/1,服务器根本不知道有这么个地址——它是浏览器自己在玩。
这也是为什么 SPA(单页应用)刷新会 404,因为服务端只收到 /,而 /users/1 这条路由在前端。
二、查询串不是字典
| 输入 | parse_qs 的结果 |
|---|---|
tag=dog&tag=cat&w=80 |
{'tag': ['dog', 'cat'], 'w': ['80']} |
tag=dog&tag=cat |
[('tag', 'dog'), ('tag', 'cat')](qsl) |
每个值都是列表,因为同一个键可以出现多次。这是很多人第一次踩的坑。
request.args.get("tag") 只拿第一个。要全拿得用 getlist("tag")。
第 9 章那个搜索只取一个关键字,get 就够。
换成「多选标签筛选」就必须 getlist,否则用户勾了三个只生效一个,而且不报错。
三、中文和空格都要编码
| 原始 | quote 之后 |
|---|---|
猫 狗 |
%E7%8C%AB%20%E7%8B%97 |
100% |
100%25 |
a&b=c |
a%26b%3Dc |
a/b |
a/b(默认保留斜杠) |
a/b 加 safe="" |
a%2Fb |
URL 里只允许一小撮 ASCII 字符,其余一律要编码,否则不同客户端会理解成不同意思。
注意 & 和 = 也被编了——它们本身是查询串的分隔符,当数据传时必须编掉,不然它就变成了新的参数。
quote 默认保留斜杠,因为路径里的斜杠有含义。
把斜杠当数据传的时候(比如文件名)要 safe=""——第 7 章那个路径穿越就是这一类问题。
四、URL、URI、URN 三个词
| 词 | 是什么 | 例子 |
|---|---|---|
| URI | 最宽:能标识一个资源的字符串 | 下面两种都算 |
| URL | URI 里「能定位」的那一类 | https://a.com/x |
| URN | URI 里「只命名不定位」的那一类 | urn:isbn:9784000000000 |
三个是包含关系:所有 URL 都是 URI,所有 URN 也都是 URI,但 URL 和 URN 互不包含。
URN 开头一律是 urn:,它只说「这个东西叫什么」,不说「去哪拿」。
ISBN 号就是典型:你知道是哪本书,但不知道去哪买。
日常说「网址」指的就是 URL。写接口文档说「统一资源标识」时才用 URI 这个词。
名词用错不影响代码,但影响别人对你的判断。
五、四种方法对一个资源
| 方法 | 意图 | 实测返回 |
|---|---|---|
| GET | 查 | 200 + 数据 |
| POST | 建 | 201 + 新建的那条 |
| PUT | 改(整体替换) | 200 + 改完的 |
| DELETE | 删 | 204 + 空体 |
这是约定,不是强制。你完全可以让 GET 去删数据——然后所有人都想杀你。
状态码不只是「成功失败」,它还说更多:201 是「建成了,这是新的那条」,204 是「做完了,
没有内容要给你」。
204 那个空体是有意义的:客户端看到 204 就不去解析响应体了。
返回 200 加一个空字符串,客户端会尝试 json.loads("") 然后报错。
六、405 与 404 是两件事
实测对一个只注册了 GET 与 POST 的路径发 DELETE,返回 405,页面标题是 405 Method Not Allowed。
404 说「没这个地址」,405 说「有这个地址,但不接这个方法」。
Flask 只在 methods= 里列出的方法可用,其余自动 405。这个检查是框架做的,不用自己写 if。
顺带一个常见的困惑:表单只能发 GET 和 POST。
要发 PUT/DELETE,要么用 JavaScript 的 fetch,要么在表单里加一个隐藏字段 _method 再在服务端改写。
七、幂等:重试的时候才看得出区别
| 操作 | 第一次 | 第二次 |
|---|---|---|
| PUT 同一个值 | {"v":100} |
{"v":100} |
| POST 同一个请求 | {"v":101} |
{"v":102} |
PUT 两次结果一样,幂等;POST 两次多了一条,不幂等。 为什么要紧?因为网络会断,客户端会重试。 幂等的可以放心重试。不幂等的重试一次就多一条数据——用户点了一次「支付」,账上扣了两次。
这就是为什么支付接口要给「幂等键」:客户端带一个唯一的请求 ID, 服务端见过这个 ID 就直接返回上次的结果,不再执行一遍。 GET 也幂等(只读),DELETE 也幂等(删两次结果都是「没了」)。POST 是这四个里唯一不幂等的。
八、这一章的位置
第 12 章全是概念,一行代码都不属于「应用」。但它是第 3 篇的地基。 接下来第 13 章定规格、第 14 章实现、第 15 章部署。 那三章里每一个决定,往上都能追到这一章的某一条:为什么返回 JSON 不返回 HTML、 为什么删除用 DELETE 不用 GET、为什么部署时要暴露端口。 概念章最容易跳过,也最容易在实现时反复回来查。
九、动手 Demo:URL 拆开看,方法排开看
这个 Demo 先把一个网址拆成六个零件,再把中文编码、查询串的重复键都试一遍。 第五节起真的起一个 HTTP 服务,把四种方法各打一次——那些状态码是实测的。
parts = urllib.parse.urlsplit(
"https://api.example.com:8443/v1/images/42?w=80&h=60#thumb")
for k in ("scheme", "netloc", "hostname", "port",
"path", "query", "fragment"):
print(k, "->", getattr(parts, k))
实跑输出:
--------------------------------------------------------------------
URL 拆开看,方法排开看
--------------------------------------------------------------------
① 一个网址的六个零件
--------------------------------------------------------------------
原串 -> https://api.example.com:8443/v1/images/42?w=80&h=60#thumb
scheme -> 'https'
netloc -> 'api.example.com:8443'
hostname -> 'api.example.com'
port -> 8443
path -> '/v1/images/42'
query -> 'w=80&h=60'
fragment -> 'thumb'
! 六个零件:协议、主机、端口、路径、查询串、片段。netloc 是「主机加端口」,hostname 只有主机。
! 片段(# 后面那段)不会发给服务器。
它是给浏览器自己用的,所以你在前端路由里看到 #/users/1,服务器根本不知道有这么个地址。
② 查询串不是字典
--------------------------------------------------------------------
parse_qs -> {'tag': ['dog', 'cat'], 'w': ['80']}
parse_qsl -> [('tag', 'dog'), ('tag', 'cat')]
! parse_qs 的每个值都是列表——因为同一个键可以出现多次。
这是很多人第一次踩的坑:request.args.get("tag") 只拿到第一个。
! 要全拿得用 request.args.getlist("tag")。第 9 章那个搜索只取一个关键字,所以 get 就够;
换成多选筛选就必须 getlist。
③ 中文要编码,斜杠也要
--------------------------------------------------------------------
quote('猫 狗') -> %E7%8C%AB%20%E7%8B%97
quote('a/b') -> a/b
quote('100%') -> 100%25
quote('a&b=c') -> a%26b%3Dc
quote 保留斜杠 -> a%2Fb
unquote 还原 -> 猫 狗
! 空格变 %20、中文变三个字节的百分号编码。
URL 里只允许一小撮 ASCII 字符,其余一律要编码,否则不同的客户端会理解成不同的意思。
! quote 默认不编码斜杠(因为路径里的斜杠有含义)。
把斜杠当数据传(比如文件名)时,要 safe=""——第 7 章那个路径穿越就是这一类问题。
④ URL、URI、URN 三个词
--------------------------------------------------------------------
| 词 | 是什么 | 例子 |
|---|---|---|
| URI | 最宽:能标识一个资源的字符串 | 下面两种都算 |
| URL | URI 里「能定位」的那一类 | https://a.com/x |
| URN | URI 里「只命名不定位」的那一类 | urn:isbn:9784000000000 |
! 三个是包含关系:所有 URL 都是 URI,所有 URN 也都是 URI,但 URL 和 URN 互不包含。
! 识别办法:URN 开头是 urn:,它只说「这个东西叫什么」,不说「去哪拿」。
ISBN 号就是典型——你知道是哪本书,但不知道去哪买。
! 日常说「网址」指的是 URL。写文档说要「统一资源标识」时才用 URI 这个词。
⑤ 四种方法,一个资源
--------------------------------------------------------------------
GET /items -> 200 [{"id":1,"name":"猫"}]
POST /items -> 201 {"id":2,"name":"狗"}
GET /items/1 -> 200 {"id":1,"name":"猫"}
PUT /items/1 -> 200 {"id":1,"name":"猫改"}
DELETE /items/1 -> 204 (响应体 0 字节)
GET /items -> 200 [{"id":2,"name":"狗"}]
! 四种方法对应四种意图:查、建、改、删。
这是约定,不是强制——你完全可以让 GET 去删数据,然后所有人都想杀你。
! 注意 POST 返回 201、DELETE 返回 204。
状态码不只是「成功失败」,它还说「建成了什么」「没有内容要给你」。
⑥ 方法没注册会怎样
--------------------------------------------------------------------
DELETE /items(这个路径只注册了 GET 与 POST)-> 405
响应里提到 -> ['<title>405 Method Not Allowed</title>']
! 405 Method Not Allowed。
它跟 404 是两件事:404 说「没这个地址」,405 说「有这个地址,但不接这个方法」。
! Flask 只在 methods= 里列出的方法可用,其余自动 405。这个检查是框架做的,不用自己写 if。
⑦ 幂等:PUT 两次与 POST 两次
--------------------------------------------------------------------
PUT 两次 -> {"v":100} 然后 {"v":100}
POST 两次 -> {"v":101} 然后 {"v":102}
! PUT 两次结果一样,幂等;POST 两次多了一条,不幂等。
! 为什么要紧:网络断了客户端会重试。幂等的可以放心重试,不幂等的重试一次就多一条数据。
这就是为什么支付接口要给「幂等键」。
! 顺带:GET 也是幂等的(只读),DELETE 也是(删两次结果都是「没了」)。
POST 是这四个里唯一不幂等的。
第一节 fragment 能拆出来,但真发请求时它不会到服务器。 第三节最后两行:默认保留斜杠,加 safe 才编掉。 第五节六个状态码串起来看,就是 CRUD 的完整一轮。
30 秒实验
- 把
parse_qs换成parse_qsl,看同一个输入的输出差在哪。 - 给那个内存 CRUD 加一个
/items的 DELETE,再发一次,看 405 还在不在。 - 想一想:为什么 DELETE 也算幂等?删第二次明明「什么也没删到」。
常见误区
- 以为
#后面那段会发给服务器:它只在浏览器里用。 - 用
get取多值参数:只拿到第一个,还不报错。 - URL 里直接拼中文:不同客户端编码方式不一样。
- 以为 405 就是 404:一个是方法不对,一个是地址不对。
自测三问(点开对答案)
1. 一个网址的六个零件是什么? —— scheme、netloc(host 加 port)、path、query、fragment,其中 fragment 不发给服务器。 2. PUT 与 POST 的关键差别是什么? —— PUT 幂等,重复执行结果一样;POST 不幂等,重复执行会多出数据。 3. 405 和 404 分别是什么情况? —— 404 是地址不存在,405 是地址存在但这个方法没注册。第 12 章到此结束。下一篇定规格:这个物件侦测 API 长什么样、返回什么。
- ☐ (本篇完,下一篇 13.1 物件侦测 API 的规格与模型准备)