🧪 第12章 网络 API 的概要
📄 一句话概括

一个网址六个零件,其中片段(# 后面)根本不发给服务器。 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 秒实验

  1. 把 parse_qs 换成 parse_qsl,看同一个输入的输出差在哪。
  2. 给那个内存 CRUD 加一个 /items 的 DELETE,再发一次,看 405 还在不在。
  3. 想一想:为什么 DELETE 也算幂等?删第二次明明「什么也没删到」。

常见误区

  1. 以为 # 后面那段会发给服务器:它只在浏览器里用。
  2. 用 get 取多值参数:只拿到第一个,还不报错。
  3. URL 里直接拼中文:不同客户端编码方式不一样。
  4. 以为 405 就是 404:一个是方法不对,一个是地址不对。
自测三问(点开对答案) 1. 一个网址的六个零件是什么? —— scheme、netloc(host 加 port)、path、query、fragment,其中 fragment 不发给服务器。 2. PUT 与 POST 的关键差别是什么? —— PUT 幂等,重复执行结果一样;POST 不幂等,重复执行会多出数据。 3. 405 和 404 分别是什么情况? —— 404 是地址不存在,405 是地址存在但这个方法没注册。

第 12 章到此结束。下一篇定规格:这个物件侦测 API 长什么样、返回什么。

相关笔记