From 0c3d6402ea695716e14d03d8432e9c82f68558fc Mon Sep 17 00:00:00 2001 From: "zhiye.sun" Date: Thu, 20 Aug 2026 16:14:29 +0800 Subject: [PATCH] =?UTF-8?q?feat(web=E5=9F=BA=E7=A1=80):=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E9=9B=B6=E5=9F=BA=E7=A1=80Web=E5=BC=80=E5=8F=91?= =?UTF-8?q?=E8=AF=BE=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../5_1_Web运行原理与客户端服务器/README.md | 162 +++++++++ .../5_1_Web运行原理与客户端服务器/practice.py | 39 +++ .../web_flow_example.py | 48 +++ 05_web基础/5_2_URL与HTTP请求/README.md | 189 +++++++++++ 05_web基础/5_2_URL与HTTP请求/practice.py | 37 +++ .../5_2_URL与HTTP请求/url_request_example.py | 38 +++ 05_web基础/5_3_HTTP响应与状态码/README.md | 174 ++++++++++ 05_web基础/5_3_HTTP响应与状态码/practice.py | 39 +++ .../response_status_example.py | 44 +++ 05_web基础/5_4_JSON数据交换/README.md | 256 +++++++++++++++ .../5_4_JSON数据交换/json_rest_example.py | 36 ++ 05_web基础/5_4_JSON数据交换/practice.py | 60 ++++ 05_web基础/5_5_RESTful_API设计/README.md | 249 ++++++++++++++ 05_web基础/5_5_RESTful_API设计/practice.py | 49 +++ .../rest_api_design_example.py | 63 ++++ .../5_6_HTTP接口调试与综合实践/README.md | 307 ++++++++++++++++++ .../local_book_api_example.py | 140 ++++++++ .../5_6_HTTP接口调试与综合实践/practice.py | 56 ++++ README.md | 27 +- 19 files changed, 2005 insertions(+), 8 deletions(-) create mode 100644 05_web基础/5_1_Web运行原理与客户端服务器/README.md create mode 100644 05_web基础/5_1_Web运行原理与客户端服务器/practice.py create mode 100644 05_web基础/5_1_Web运行原理与客户端服务器/web_flow_example.py create mode 100644 05_web基础/5_2_URL与HTTP请求/README.md create mode 100644 05_web基础/5_2_URL与HTTP请求/practice.py create mode 100644 05_web基础/5_2_URL与HTTP请求/url_request_example.py create mode 100644 05_web基础/5_3_HTTP响应与状态码/README.md create mode 100644 05_web基础/5_3_HTTP响应与状态码/practice.py create mode 100644 05_web基础/5_3_HTTP响应与状态码/response_status_example.py create mode 100644 05_web基础/5_4_JSON数据交换/README.md create mode 100644 05_web基础/5_4_JSON数据交换/json_rest_example.py create mode 100644 05_web基础/5_4_JSON数据交换/practice.py create mode 100644 05_web基础/5_5_RESTful_API设计/README.md create mode 100644 05_web基础/5_5_RESTful_API设计/practice.py create mode 100644 05_web基础/5_5_RESTful_API设计/rest_api_design_example.py create mode 100644 05_web基础/5_6_HTTP接口调试与综合实践/README.md create mode 100644 05_web基础/5_6_HTTP接口调试与综合实践/local_book_api_example.py create mode 100644 05_web基础/5_6_HTTP接口调试与综合实践/practice.py diff --git a/05_web基础/5_1_Web运行原理与客户端服务器/README.md b/05_web基础/5_1_Web运行原理与客户端服务器/README.md new file mode 100644 index 0000000..6663314 --- /dev/null +++ b/05_web基础/5_1_Web运行原理与客户端服务器/README.md @@ -0,0 +1,162 @@ +# 第5-1课:Web运行原理与客户端服务器 + +## 一、本课目标 + +完成本课后,你能够: + +1. 解释万维网(World Wide Web,Web)是什么; +2. 区分客户端(Client)、服务器(Server)、前端和后端; +3. 描述浏览器访问后端时的请求响应过程; +4. 理解后端程序为什么不能主动把普通响应随意发给浏览器; +5. 运行一个不依赖网络的请求响应模拟程序。 + +## 二、前置知识 + +- Python函数、字典和条件判断; +- 知道程序可以接收输入、处理数据并返回结果; +- 不要求学过任何Python Web框架。 + +## 三、Web是什么 + +Web是建立在互联网之上的信息与应用系统。互联网提供计算机之间的连接,Web则约定如何定位资源、发送请求和返回页面或数据。浏览器、手机应用和接口调试工具都可以充当客户端。 + +客户端主动发起请求,服务器监听请求、执行业务逻辑并返回响应: + +```text +用户 → 客户端 → HTTP请求 → 服务器 +用户 ← 客户端 ← HTTP响应 ← 服务器 +``` + +一次普通请求对应一次响应。服务器若要主动通知客户端,需要轮询、服务器发送事件或WebSocket等额外机制,这些将在后续课程学习。 + +## 四、前端与后端 + +- 前端:直接与用户交互的界面和浏览器端逻辑; +- 后端:运行在服务器侧,负责业务规则、权限、数据库和接口; +- 数据库:负责持久保存数据,通常不直接暴露给浏览器。 + +浏览器是客户端的一种,客户端不只有浏览器。Java中的Controller、Service、Repository分层属于服务器内部结构,不等于客户端与服务器的边界。 + +## 五、用生活场景理解通信双方 + +可以把访问网站类比成到餐厅点餐: + +| Web概念 | 餐厅类比 | 实际职责 | +|---|---|---| +| 客户端 | 顾客 | 提出明确请求并接收结果 | +| HTTP请求 | 点菜单 | 写明想获取什么或提交什么 | +| 服务器 | 餐厅服务体系 | 接收请求并组织处理 | +| 后端业务代码 | 后厨处理流程 | 校验规则、计算并访问数据 | +| HTTP响应 | 送回的餐品和结果单 | 告诉客户端结果与内容 | + +类比只能帮助入门:真实HTTP请求不是口头交流,而是双方按照协议组织的数据。 + +## 六、输入网址后发生了什么 + +第一次学习时先掌握以下主干流程: + +1. 用户在浏览器输入网址并确认; +2. 浏览器从网址中确定要联系的服务器和资源; +3. 浏览器与服务器建立网络连接; +4. 浏览器发送HTTP请求; +5. 服务器读取请求; +6. 后端根据路径执行对应逻辑,必要时查询数据库; +7. 服务器生成HTTP响应; +8. 浏览器读取响应; +9. 如果响应是HTML,浏览器解析并显示页面;如果响应是JSON,通常交给前端代码处理。 + +服务器地址解析、连接和HTTP报文将在第二课继续展开。本课先记住:页面不是浏览器凭空产生的,数据需要经过请求与响应往返。 + +## 七、页面请求与接口请求 + +服务器可能返回不同内容: + +- HTML:描述页面结构,浏览器可以渲染; +- CSS:描述页面样式; +- JavaScript:在浏览器中执行交互逻辑; +- 图片或文件:由浏览器展示或下载; +- JSON:结构化数据,常用于前后端接口。 + +访问一个页面往往不只有一个请求。浏览器取得HTML后,可能继续请求CSS、JavaScript、图片和接口数据。因此开发者工具中看到很多请求是正常现象。 + +## 八、完整示例 + +`web_flow_example.py`用字典表示请求和响应,不建立真实网络连接,目的是先看清四个步骤: + +1. 浏览器创建请求; +2. 服务器接收请求; +3. 服务器根据路径处理; +4. 浏览器根据响应展示内容。 + +## 九、运行方法 + +```powershell +cd D:\Code\Python\05_web基础\5_1_Web运行原理与客户端服务器 +python web_flow_example.py +``` + +命令执行后,Python直接运行本课文件。示例不访问互联网,也不会启动真实服务器,所以任何时候都可以安全重复运行。 + +## 十、预期结果 + +```text +客户端发送:GET /books +响应状态:200 +内容类型:text/html; charset=utf-8 +浏览器展示:

图书列表

+``` + +## 十一、关键代码解析 + +`browser_send_request()`模拟创建请求;`server_handle_request()`相当于最简单的路由和业务处理;`browser_render_response()`读取服务器返回的数据。这里的字典不是HTTP消息本身,只是帮助观察结构的Python表示。 + +按执行顺序阅读: + +1. Python执行文件底部的入口判断并调用`main()`; +2. `main()`把路径`/books`交给`browser_send_request()`; +3. 函数返回一份包含方法、地址和请求头的模拟请求; +4. `server_handle_request()`检查请求路径; +5. 路径是`/books`,因此返回状态码200和HTML内容; +6. `browser_render_response()`依次输出状态码、内容类型和正文。 + +把`"/books"`临时改成`"/missing"`,可以观察404分支。实验完成后恢复标准示例。 + +## 十二、常见错误 + +### 12.1 把Web等同于互联网 + +互联网是连接基础设施,Web是其上的一种应用体系。电子邮件也使用互联网,但不等于Web页面。 + +### 12.2 把服务器理解成一台固定机器 + +服务器既可指提供服务的程序,也可指运行该程序的计算机。学习后端时通常更关心服务器程序。 + +### 12.3 认为后端直接操作页面 + +后端返回HTML或JSON,浏览器中的前端代码决定如何展示。两者通过请求和响应协作。 + +### 12.4 认为一次打开页面只有一个请求 + +现代页面通常还需要样式、脚本、图片和接口数据,浏览器会继续发送多个请求。 + +### 12.5 把“本机”与“服务器”对立起来 + +服务器程序也可以运行在自己的电脑上。是否叫服务器取决于它是否向客户端提供服务,而不是电脑放在哪里。 + +## 十三、课堂练习 + +打开`practice.py`,完成两个练习。练习文件没有答案,请先独立实现。 + +## 十四、本课小结 + +Web应用最基本的闭环是“客户端发请求,服务器做处理,客户端收响应”。前端、后端和数据库是职责划分,客户端与服务器是通信双方。 + +## 十五、验收标准 + +- 能区分Web与互联网; +- 能区分客户端、服务器、前端和后端; +- 能按顺序解释一次请求响应; +- 能说明一个页面为什么可能产生多个请求; +- 能判断HTML与JSON通常由谁处理; +- 示例能够独立运行且输出符合预期; +- 完成`practice.py`中的练习。 diff --git a/05_web基础/5_1_Web运行原理与客户端服务器/practice.py b/05_web基础/5_1_Web运行原理与客户端服务器/practice.py new file mode 100644 index 0000000..cbfa80f --- /dev/null +++ b/05_web基础/5_1_Web运行原理与客户端服务器/practice.py @@ -0,0 +1,39 @@ +# 第5-1课练习:Web运行原理与客户端服务器 +# +# 本文件只提供题目、预期结果、自查清单和验收标准,不包含参考答案或代码骨架。 + + +# 练习一:补全一次请求响应流程 +# 1. 编写client_send_request(path),返回包含method、path和headers的字典。 +# 2. 编写server_handle_request(request),处理/和/about两个路径。 +# 3. 已知路径返回状态码200,未知路径返回状态码404。 +# 4. 编写client_show_response(response),输出状态码和响应体。 + + +# 练习二:区分各部分职责 +# 1. 在注释中说明客户端、服务器、前端和后端分别是什么。 +# 2. 说明“浏览器”和“客户端”为什么不能始终画等号。 +# 3. 写出从用户输入URL到页面展示的至少五个步骤。 + + +# 预期关键输出: +# 客户端发送:GET /about +# 响应状态:200 +# 页面内容:关于我们 +# 客户端发送:GET /missing +# 响应状态:404 +# 页面内容:页面不存在 + + +# 自查清单: +# 1. 客户端是否负责创建请求并处理响应? +# 2. 服务器是否根据路径决定返回内容? +# 3. 是否理解请求和响应是方向相反的两份消息? +# 4. 是否没有把HTML、HTTP和互联网混为同一个概念? + + +# 最终验收标准: +# 1. practice.py通过语法检查并可独立运行; +# 2. 已知路径和未知路径的输出符合预期; +# 3. 能用自己的话解释客户端与服务器的职责; +# 4. 能完整描述一次请求响应过程。 diff --git a/05_web基础/5_1_Web运行原理与客户端服务器/web_flow_example.py b/05_web基础/5_1_Web运行原理与客户端服务器/web_flow_example.py new file mode 100644 index 0000000..6297065 --- /dev/null +++ b/05_web基础/5_1_Web运行原理与客户端服务器/web_flow_example.py @@ -0,0 +1,48 @@ +"""第5-1课标准示例:模拟客户端、服务器和请求响应流程。""" + + +def browser_send_request(url: str) -> dict: + """模拟浏览器根据URL创建HTTP请求。""" + + return { + "method": "GET", + "url": url, + "headers": {"Accept": "text/html"}, + } + + +def server_handle_request(request: dict) -> dict: + """模拟服务器读取请求并返回HTTP响应。""" + + if request["url"] == "/books": + return { + "status": 200, + "headers": {"Content-Type": "text/html; charset=utf-8"}, + "body": "

图书列表

", + } + return { + "status": 404, + "headers": {"Content-Type": "text/plain; charset=utf-8"}, + "body": "页面不存在", + } + + +def browser_render_response(response: dict) -> None: + """模拟浏览器检查响应并展示响应体。""" + + print(f"响应状态:{response['status']}") + print(f"内容类型:{response['headers']['Content-Type']}") + print(f"浏览器展示:{response['body']}") + + +def main() -> None: + """依次模拟发送请求、服务器处理和浏览器展示。""" + + request = browser_send_request("/books") + print(f"客户端发送:{request['method']} {request['url']}") + response = server_handle_request(request) + browser_render_response(response) + + +if __name__ == "__main__": + main() diff --git a/05_web基础/5_2_URL与HTTP请求/README.md b/05_web基础/5_2_URL与HTTP请求/README.md new file mode 100644 index 0000000..4fcd8db --- /dev/null +++ b/05_web基础/5_2_URL与HTTP请求/README.md @@ -0,0 +1,189 @@ +# 第5-2课:URL与HTTP请求 + +## 一、本课目标 + +完成本课后,你能够: + +1. 拆解统一资源定位符(Uniform Resource Locator,URL); +2. 解释HTTP请求行、请求头和请求体; +3. 理解路径参数与查询参数的用途差异; +4. 为常见操作选择GET、POST、PUT、PATCH或DELETE; +5. 使用Python标准库安全编码查询参数。 + +## 二、前置知识 + +- 已理解客户端与服务器; +- Python字典、函数和字符串; +- 不要求记忆HTTP报文的所有字段。 + +## 三、URL的组成 + +示例: + +```text +https://api.example.com:443/books/10?detail=true#summary +``` + +| 部分 | 示例 | 作用 | +|---|---|---| +| 协议 | `https` | 约定通信方式 | +| 主机 | `api.example.com` | 定位服务器 | +| 端口 | `443` | 定位服务器中的服务 | +| 路径 | `/books/10` | 定位资源 | +| 查询字符串 | `detail=true` | 提供筛选或选项 | +| 片段 | `summary` | 通常只供客户端定位页面位置,不发送给服务器 | + +HTTPS是经过传输层安全协议(Transport Layer Security,TLS)保护的HTTP。它保护传输过程,但不能自动修复错误的业务权限。 + +浏览器真正访问时还会涉及以下概念: + +- 域名(Domain Name):便于人记忆的服务器名称,例如`api.example.com`; +- IP地址(Internet Protocol Address):网络用于定位设备或服务入口的地址; +- 域名系统(Domain Name System,DNS):把域名查询为可用于连接的IP地址; +- 端口(Port):同一台计算机上用于区分不同网络服务的编号。 + +可以把IP地址理解成办公楼地址,把端口理解成楼内具体窗口。这个类比不代表端口是物理接口,它只是操作系统管理网络连接时使用的数字。 + +HTTP默认端口通常是80,HTTPS默认端口通常是443。使用默认端口时,URL可以省略端口;使用开发服务器的8000等端口时通常需要明确写出。 + +## 四、浏览器如何根据URL定位请求目标 + +以`https://api.example.com:443/books/10?detail=true`为例: + +1. 浏览器看到`https`,知道要使用受TLS保护的HTTP; +2. 通过DNS查询`api.example.com`对应的IP地址; +3. 向该地址的443端口建立连接; +4. 把`/books/10?detail=true`作为请求目标; +5. 在连接中发送HTTP请求; +6. 等待服务器返回HTTP响应。 + +实际网络还涉及缓存、代理、网关和连接复用。本阶段只建立主干认识,不展开网络工程细节。 + +## 五、HTTP请求结构 + +```http +POST /books HTTP/1.1 +Host: localhost:8000 +Content-Type: application/json + +{"title": "Python入门"} +``` + +- 请求行:请求方法、请求目标和HTTP版本; +- 请求头:描述内容类型、认证信息和客户端能力; +- 空行:分隔头部与请求体; +- 请求体:提交给服务器的数据,GET通常不依赖请求体。 + +逐行阅读这份请求: + +1. `POST /books HTTP/1.1`表示使用POST方法访问`/books`; +2. `Host`说明目标主机和端口; +3. `Content-Type`说明请求体是JSON; +4. 空行表示请求头结束; +5. 最后一行JSON是提交给服务器的数据。 + +HTTP报文在网络中最终按字节传输。这里用文本形式展示,是为了让结构更容易阅读。 + +## 六、常用请求方法 + +| 方法 | 常见含义 | 图书示例 | +|---|---|---| +| GET | 查询 | 查询图书 | +| POST | 创建或触发处理 | 新增图书 | +| PUT | 整体替换 | 替换图书全部可修改信息 | +| PATCH | 局部修改 | 只修改价格 | +| DELETE | 删除 | 删除图书 | + +方法表达意图,最终行为仍由服务器代码决定。GET应当是安全方法,即正常调用不应修改业务数据;重复执行PUT或DELETE通常应具有幂等性,即最终效果与执行一次相同。 + +安全和幂等是HTTP语义,不等同于权限安全: + +- “GET是安全方法”表示它不应改变业务状态; +- “DELETE通常幂等”表示重复删除后最终仍是资源不存在; +- 它们都不表示接口无需登录或权限控制。 + +## 七、路径参数与查询参数 + +- `/books/10`中的`10`通常是路径参数,用于标识某一本书; +- `/books?keyword=Python&page=2`中的值是查询参数,用于筛选、排序或分页。 + +请求体通常承载新增或修改时的结构化数据,例如书名、价格和作者。不要把大量结构化内容全部塞进URL。 + +## 八、URL编码 + +URL中的空格、中文、`&`和`=`可能与URL结构规则冲突,因此需要百分号编码(Percent-encoding)或表单风格编码。 + +```python +urlencode({"keyword": "Python Web", "page": 1}) +``` + +结果中的空格可能显示为`+`: + +```text +keyword=Python+Web&page=1 +``` + +客户端负责编码,服务器负责解码。不要先手工替换一次,再交给工具重复编码。 + +## 九、完整示例 + +`url_request_example.py`使用`urlsplit()`拆分URL,使用`parse_qs()`读取查询参数,使用`urlencode()`处理空格等特殊字符。 + +## 十、运行方法 + +```powershell +cd D:\Code\Python\05_web基础\5_2_URL与HTTP请求 +python url_request_example.py +``` + +关键输出包括协议、主机、端口、路径、查询参数,以及编码后的GET请求行。 + +## 十一、代码执行顺序 + +1. `main()`准备一条完整URL; +2. `parse_url()`使用`urlsplit()`拆分结构; +3. `parse_qs()`把查询字符串转换为字典; +4. 程序分别输出协议、主机、端口、路径和查询参数; +5. `build_request_target()`使用`urlencode()`编码参数; +6. 程序输出一条简化请求行和请求头。 + +`api.example.com`是教学示例域名,程序只解析字符串,不会访问该网站。 + +## 十二、常见错误 + +### 12.1 手工拼接查询字符串 + +空格、中文、`&`和`=`具有特殊含义,应交给`urlencode()`等工具编码。 + +### 12.2 用POST处理所有操作 + +程序可能运行,但接口意图模糊,缓存、重试、权限和文档也更难设计。 + +### 12.3 在URL中传密码 + +URL容易进入浏览历史和日志。密码及令牌不应放入查询字符串。 + +### 12.4 混淆域名、IP和端口 + +域名需要经过DNS解析,IP用于网络定位,端口用于区分服务。它们有关联,但不是同一个值。 + +### 12.5 认为HTTPS代表接口一定可信 + +HTTPS主要保护传输过程。客户端仍要确认访问的域名,服务器仍要进行身份认证、权限和输入校验。 + +## 十三、课堂练习 + +打开`practice.py`,完成URL解析和请求方法选择练习。 + +## 十四、本课小结 + +URL回答“向哪里请求”,HTTP方法回答“想做什么”,请求头描述消息,请求体携带提交的数据。 + +## 十五、验收标准 + +- 能拆解完整URL; +- 能解释域名、DNS、IP和端口的基本关系; +- 能解释HTTP请求的主要部分; +- 能区分路径参数和查询参数; +- 能为常见增删改查选择请求方法; +- 示例运行结果符合预期。 diff --git a/05_web基础/5_2_URL与HTTP请求/practice.py b/05_web基础/5_2_URL与HTTP请求/practice.py new file mode 100644 index 0000000..599f470 --- /dev/null +++ b/05_web基础/5_2_URL与HTTP请求/practice.py @@ -0,0 +1,37 @@ +# 第5-2课练习:URL与HTTP请求 +# +# 本文件只提供题目、预期结果、自查清单和验收标准,不包含参考答案或代码骨架。 + + +# 练习一:URL分析器 +# 1. 使用urllib.parse.urlsplit()解析一个完整URL。 +# 2. 分别输出协议、主机、端口、路径、查询字符串和片段。 +# 3. 使用parse_qs()解析查询参数。 +# 4. 测试包含中文关键字的URL编码结果。 + + +# 练习二:为业务选择请求方法 +# 为图书查询、新增、整体修改、局部修改和删除分别选择HTTP方法。 +# 在注释中解释每种选择,不需要发送真实网络请求。 + + +# 预期关键输出: +# 协议:https +# 主机:localhost +# 端口:8000 +# 路径:/books/10 +# 查询参数:{'detail': ['true']} + + +# 自查清单: +# 1. 是否区分主机、端口、路径和查询参数? +# 2. 是否使用工具编码查询参数,而不是手工拼接特殊字符? +# 3. 是否理解GET通常读取资源,POST通常创建资源? +# 4. 是否知道请求头和请求体不是同一部分? + + +# 最终验收标准: +# 1. practice.py通过语法检查并可独立运行; +# 2. URL每部分解析正确; +# 3. 中文和空格经过正确编码; +# 4. 能为五种图书操作选择合理请求方法。 diff --git a/05_web基础/5_2_URL与HTTP请求/url_request_example.py b/05_web基础/5_2_URL与HTTP请求/url_request_example.py new file mode 100644 index 0000000..d2e1599 --- /dev/null +++ b/05_web基础/5_2_URL与HTTP请求/url_request_example.py @@ -0,0 +1,38 @@ +"""第5-2课标准示例:解析URL并构造HTTP请求信息。""" + +from urllib.parse import parse_qs, urlencode, urlsplit + + +def parse_url(url: str) -> None: + """拆分URL,并把查询字符串转换成便于使用的字典。""" + + parts = urlsplit(url) + query_parameters = parse_qs(parts.query) + print(f"协议:{parts.scheme}") + print(f"主机:{parts.hostname}") + print(f"端口:{parts.port}") + print(f"路径:{parts.path}") + print(f"查询参数:{query_parameters}") + + +def build_request_target(path: str, parameters: dict) -> str: + """把路径和查询参数编码成请求目标。""" + + return f"{path}?{urlencode(parameters)}" + + +def main() -> None: + """展示URL各部分以及一份简化的GET请求。""" + + url = "https://api.example.com:443/books?keyword=Python&page=2" + parse_url(url) + request_target = build_request_target( + "/books", + {"keyword": "Python Web", "page": 1}, + ) + print(f"请求行:GET {request_target} HTTP/1.1") + print("请求头:Accept: application/json") + + +if __name__ == "__main__": + main() diff --git a/05_web基础/5_3_HTTP响应与状态码/README.md b/05_web基础/5_3_HTTP响应与状态码/README.md new file mode 100644 index 0000000..26fa641 --- /dev/null +++ b/05_web基础/5_3_HTTP响应与状态码/README.md @@ -0,0 +1,174 @@ +# 第5-3课:HTTP响应与状态码 + +## 一、本课目标 + +完成本课后,你能够: + +1. 解释HTTP响应状态行、响应头和响应体; +2. 理解2xx、3xx、4xx和5xx状态码分类; +3. 为常见业务结果选择合适状态码; +4. 区分401、403和404; +5. 使用Python的`HTTPStatus`避免魔法数字。 + +## 二、前置知识 + +- 已理解HTTP请求; +- Python字典、函数和异常; +- 知道后端业务可能成功,也可能因输入或系统问题失败。 + +## 三、HTTP响应结构 + +```http +HTTP/1.1 200 OK +Content-Type: application/json; charset=utf-8 + +{"id": 1, "title": "Python入门"} +``` + +- 状态行:HTTP版本、状态码和原因短语; +- 响应头:描述响应内容、缓存和认证等信息; +- 响应体:页面、JSON、文件或错误详情。 + +逐行阅读: + +1. `HTTP/1.1 200 OK`说明HTTP版本和处理结果; +2. `Content-Type`说明响应体是UTF-8编码的JSON; +3. 空行表示响应头结束; +4. 最后一行JSON是响应体; +5. 客户端先根据状态码判断结果类别,再按照`Content-Type`解析响应体。 + +状态码是给程序和人共同使用的标准信号,原因短语`OK`主要帮助阅读。客户端逻辑应依赖状态码,不应依赖英文短语。 + +## 四、状态码分类 + +| 范围 | 含义 | 常见情况 | +|---|---|---| +| 1xx | 信息响应 | 处理中,入门阶段很少手工处理 | +| 2xx | 成功 | 查询、创建、更新或删除成功 | +| 3xx | 重定向 | 资源位置变化或缓存仍有效 | +| 4xx | 客户端请求问题 | 参数、认证、权限或资源错误 | +| 5xx | 服务器处理失败 | 未处理异常或上游服务失败 | + +## 五、常用状态码 + +| 状态码 | 常见使用场景 | +|---|---| +| 200 OK | 成功并返回内容 | +| 201 Created | 成功创建资源,通常附带`Location`头 | +| 204 No Content | 成功但没有响应体 | +| 400 Bad Request | 请求格式或通用参数错误 | +| 401 Unauthorized | 尚未完成身份认证,名称虽是Unauthorized,实际常表示未认证 | +| 403 Forbidden | 身份已知,但没有访问权限 | +| 404 Not Found | 资源不存在 | +| 409 Conflict | 当前状态冲突,例如编号重复 | +| 422 Unprocessable Content | 格式可解析,但字段校验或语义不符合要求 | +| 500 Internal Server Error | 服务器出现未预期错误 | + +业务失败不一定是500。例如图书不存在是404,重复编号可以是409。不要把异常详情和堆栈直接返回给外部客户端。 + +## 六、选择状态码的思考顺序 + +遇到业务结果时,可以依次提问: + +1. 请求是否完成了预期操作?完成则从2xx中选择; +2. 是否需要把客户端引导到其他地址?是则考虑3xx; +3. 客户端修改请求、身份或操作时机后能否解决?能则通常是4xx; +4. 是否因为服务器出现未预期故障而无法完成?是则通常是5xx。 + +进一步判断: + +- 成功返回内容:通常200; +- 成功创建新资源:201; +- 成功且没有响应体:204; +- JSON根本无法解析:400; +- JSON可解析但字段不符合约束:常用422; +- 当前资源状态与操作冲突:409; +- 代码出现未处理异常:500。 + +状态码选择可能受团队规范影响,但同一项目应保持一致并写入接口文档。 + +## 七、401、403、404详细对比 + +假设用户请求删除一本图书: + +- 没有登录凭证:401,客户端需要先认证; +- 已登录但不是管理员:403,身份明确但操作被禁止; +- 有权限但图书编号不存在:404; +- 删除成功且不返回正文:204。 + +有些安全敏感接口会用404隐藏资源是否存在,这是安全策略,需要由项目统一决定,不能随意混用。 + +## 八、完整示例 + +`response_status_example.py`使用标准库`HTTPStatus`表示状态码,分别生成图书存在和不存在的响应。 + +## 九、运行方法 + +```powershell +cd D:\Code\Python\05_web基础\5_3_HTTP响应与状态码 +python response_status_example.py +``` + +关键输出: + +```text +HTTP/1.1 200 OK +Content-Type: application/json; charset=utf-8 +响应体:{'id': 1, 'title': 'Python入门'} +--- +HTTP/1.1 404 Not Found +``` + +## 十、关键代码解析 + +`find_book()`根据业务结果选择状态码;`print_response()`读取枚举的数值和原因短语。实际框架会负责把Python对象转换成网络报文,但选择状态码仍是后端开发者的责任。 + +执行过程: + +1. `BOOKS`保存两条内存图书数据; +2. 第一次调用`find_book(1)`,字典中存在编号1; +3. 函数返回`HTTPStatus.OK`和图书数据; +4. 第二次调用`find_book(99)`,没有找到图书; +5. 函数返回`HTTPStatus.NOT_FOUND`和错误消息; +6. `print_response()`把两种结果按响应结构输出。 + +`HTTPStatus`是Python标准库提供的枚举,`HTTPStatus.NOT_FOUND`比直接写`404`更容易读懂。它最终仍能通过`.value`取得数字404。 + +## 十一、常见错误 + +### 11.1 HTTP永远返回200 + +只在响应体里写“失败”会让客户端、监控和网关难以判断真实结果。 + +### 11.2 把所有异常都返回500 + +输入错误、资源不存在和权限不足属于可预期结果,应明确使用4xx。 + +### 11.3 给204响应添加响应体 + +204的含义就是没有响应内容,需要返回内容时选择200等状态码。 + +### 11.4 只看响应消息,不看状态码 + +错误消息可能变化或被翻译,状态码才是客户端判断结果类别的标准信号。 + +### 11.5 把程序异常详情返回客户端 + +堆栈、SQL和服务器路径可能泄露内部信息。外部响应应提供安全的错误说明,详细异常写入受控日志。 + +## 十二、课堂练习 + +打开`practice.py`,完成状态码选择和创建资源响应练习。 + +## 十三、本课小结 + +状态码是HTTP层的统一结果语言。响应体提供细节,不能代替准确的状态码;准确的状态码也不能代替清晰、安全的错误信息。 + +## 十四、验收标准 + +- 能解释HTTP响应的主要部分; +- 能区分2xx、4xx和5xx; +- 能正确区分401、403和404; +- 能为创建、删除和校验失败选择状态码; +- 能按顺序判断一个结果属于2xx、4xx还是5xx; +- 示例可独立运行。 diff --git a/05_web基础/5_3_HTTP响应与状态码/practice.py b/05_web基础/5_3_HTTP响应与状态码/practice.py new file mode 100644 index 0000000..71034cf --- /dev/null +++ b/05_web基础/5_3_HTTP响应与状态码/practice.py @@ -0,0 +1,39 @@ +# 第5-3课练习:HTTP响应与状态码 +# +# 本文件只提供题目、预期结果、自查清单和验收标准,不包含参考答案或代码骨架。 + + +# 练习一:为业务结果选择状态码 +# 1. 查询成功并返回图书。 +# 2. 成功创建图书。 +# 3. 成功删除且不返回响应体。 +# 4. 请求JSON格式错误。 +# 5. 未登录、已登录但无权限、图书不存在、服务器未知异常。 +# 为每种情况选择状态码,并在注释中解释原因。 + + +# 练习二:构造响应 +# 1. 编写create_book(title),空标题返回客户端错误。 +# 2. 标题有效时模拟创建图书,返回新资源信息和Location响应头。 +# 3. 编写print_response()输出状态行、响应头和响应体。 + + +# 预期关键输出: +# HTTP/1.1 201 Created +# Location: /books/3 +# 响应体:{'id': 3, 'title': 'Web基础'} +# HTTP/1.1 400 Bad Request + + +# 自查清单: +# 1. 是否避免所有结果都返回200? +# 2. 是否区分401未认证与403无权限? +# 3. 是否区分客户端错误与服务器错误? +# 4. 204响应是否没有响应体? + + +# 最终验收标准: +# 1. practice.py通过语法检查并可独立运行; +# 2. 各业务情况的状态码合理; +# 3. 创建成功包含Location响应头; +# 4. 能解释2xx、4xx和5xx的责任边界。 diff --git a/05_web基础/5_3_HTTP响应与状态码/response_status_example.py b/05_web基础/5_3_HTTP响应与状态码/response_status_example.py new file mode 100644 index 0000000..13a90bb --- /dev/null +++ b/05_web基础/5_3_HTTP响应与状态码/response_status_example.py @@ -0,0 +1,44 @@ +"""第5-3课标准示例:根据业务结果生成HTTP响应。""" + +from http import HTTPStatus + + +BOOKS = {1: "Python入门", 2: "数据库实践"} + + +def find_book(book_id: int) -> dict: + """查询图书,并用状态码明确表达查询结果。""" + + title = BOOKS.get(book_id) + if title is None: + return { + "status": HTTPStatus.NOT_FOUND, + "headers": {"Content-Type": "application/json; charset=utf-8"}, + "body": {"message": "图书不存在"}, + } + return { + "status": HTTPStatus.OK, + "headers": {"Content-Type": "application/json; charset=utf-8"}, + "body": {"id": book_id, "title": title}, + } + + +def print_response(response: dict) -> None: + """输出响应状态行、响应头和响应体。""" + + status = response["status"] + print(f"HTTP/1.1 {status.value} {status.phrase}") + print(f"Content-Type: {response['headers']['Content-Type']}") + print(f"响应体:{response['body']}") + + +def main() -> None: + """分别展示查询成功与资源不存在的响应。""" + + print_response(find_book(1)) + print("---") + print_response(find_book(99)) + + +if __name__ == "__main__": + main() diff --git a/05_web基础/5_4_JSON数据交换/README.md b/05_web基础/5_4_JSON数据交换/README.md new file mode 100644 index 0000000..073b5fb --- /dev/null +++ b/05_web基础/5_4_JSON数据交换/README.md @@ -0,0 +1,256 @@ +# 第5-4课:JSON数据交换 + +## 一、本课目标 + +完成本课后,你能够: + +1. 说明JSON是什么以及为什么Web接口经常使用JSON; +2. 区分Python对象、JSON文本和网络中的字节; +3. 掌握JSON的六类基本值; +4. 使用`json.dumps()`完成序列化; +5. 使用`json.loads()`完成反序列化; +6. 识别常见JSON格式错误和不支持的数据类型; +7. 看懂HTTP请求体或响应体中的JSON。 + +## 二、前置知识 + +- Python字典、列表、字符串、数字、布尔值和`None`; +- 已理解HTTP请求体、响应体和`Content-Type`; +- 本课不重复讲解Python容器和异常处理语法。 + +## 三、为什么需要数据交换格式 + +客户端和服务器可能使用不同语言。例如浏览器使用JavaScript,后端使用Python。Python字典只能直接存在于Python程序内,不能原样作为跨语言约定。 + +双方需要一种共同格式: + +```text +Python字典 → JSON文本 → HTTP传输 → JSON文本 → JavaScript对象 +``` + +JavaScript对象表示法(JavaScript Object Notation,JSON)是一种轻量的文本数据格式。它由字符组成,与具体编程语言无关,因此非常适合Web接口交换结构化数据。 + +## 四、不要混淆三个层次 + +下面三项表示相近内容,但类型不同: + +```python +# Python字典:程序可以按键读取和修改 +book = {"id": 1, "available": True} + +# JSON文本:本质是一个Python字符串 +json_text = '{"id": 1, "available": true}' + +# UTF-8字节:真正写入网络连接的数据形式之一 +json_bytes = json_text.encode("utf-8") +``` + +特别注意: + +- Python使用`True`和`None`; +- JSON使用`true`和`null`; +- Python字典显示时可能使用单引号; +- JSON属性名和字符串必须使用双引号。 + +因此,`str(book)`不是可靠的JSON生成方式。 + +## 五、JSON支持哪些值 + +| JSON类型 | JSON示例 | 转成Python后的类型 | +|---|---|---| +| object | `{"title": "Python"}` | `dict` | +| array | `["Python", "Web"]` | `list` | +| string | `"Python"` | `str` | +| number | `59.9` | `int`或`float` | +| boolean | `true`、`false` | `True`、`False` | +| null | `null` | `None` | + +JSON没有元组、集合、日期时间、`Decimal`和自定义类等专用类型。发送这些值之前,需要先决定明确的表示形式。 + +例如日期时间通常转换成字符串: + +```json +{"created_at": "2026-08-20T10:30:00+08:00"} +``` + +这只是常见约定,客户端和服务器仍需在接口文档中统一格式与时区。 + +## 六、阅读一段JSON + +```json +{ + "id": 1, + "title": "Python Web基础", + "price": 59.9, + "available": true, + "tags": ["Python", "Web"], + "description": null +} +``` + +从外向内阅读: + +1. 最外层花括号表示一个object; +2. object中包含六组“属性名和值”; +3. `tags`的值是array; +4. array中有两个string; +5. `description`当前没有值,因此使用`null`。 + +JSON不允许在最后一个属性后留下多余逗号,也不支持Python风格的`# 注释`。 + +## 七、序列化 + +序列化(Serialization)是把程序中的对象转换成便于保存或传输的格式。本课是把Python对象转换为JSON文本: + +```python +json_text = json.dumps(book, ensure_ascii=False, indent=2) +``` + +参数含义: + +- `book`:需要转换的Python对象; +- `ensure_ascii=False`:中文直接保留,不显示成Unicode转义; +- `indent=2`:每层缩进两个空格,便于人阅读。 + +格式化缩进会增加数据大小,生产接口是否缩进由项目性能和可读性要求决定。 + +## 八、反序列化 + +反序列化(Deserialization)是把JSON文本还原为Python对象: + +```python +request_body = '{"title": "HTTP实践", "price": 49.0}' +request_data = json.loads(request_body) +print(request_data["title"]) +``` + +`json.loads()`中的字母`s`可以帮助记忆为“读取字符串”。解析成功只说明JSON语法正确,不表示字段满足业务规则。例如`{"title": ""}`是合法JSON,但空书名可能不符合业务要求。 + +## 九、JSON与HTTP怎样配合 + +客户端提交JSON时,应声明请求体格式: + +```http +POST /books HTTP/1.1 +Content-Type: application/json; charset=utf-8 + +{"title": "Python入门"} +``` + +服务器返回JSON时,也应声明响应体格式: + +```http +HTTP/1.1 200 OK +Content-Type: application/json; charset=utf-8 + +{"id": 1, "title": "Python入门"} +``` + +`Content-Type`告诉接收方应按照什么格式解释正文。如果正文是JSON却声明为`text/plain`,有些工具仍能显示,但自动处理、文档和客户端代码可能出现问题。 + +## 十、完整示例 + +标准示例位于`json_rest_example.py`。文件名保留了创建阶段的名称,但本课示例只负责JSON转换,不讲RESTful API;RESTful将在下一课独立学习。 + +示例包含: + +- 一个包含六种常见值的图书字典; +- Python对象转JSON文本; +- 保留中文并格式化缩进; +- JSON请求体转Python字典; +- 从解析结果中读取书名。 + +## 十一、运行方法 + +```powershell +cd D:\Code\Python\05_web基础\5_4_JSON数据交换 +python json_rest_example.py +``` + +## 十二、预期结果 + +```text +JSON响应: +{ + "id": 1, + "title": "Python Web基础", + "price": 59.9, + "available": true, + "tags": [ + "Python", + "Web" + ], + "description": null +} +请求中的书名:HTTP实践 +``` + +如果看到`true`和`null`,说明输出是JSON写法,不是Python字典的直接显示结果。 + +## 十三、代码执行顺序 + +1. Python导入标准库`json`; +2. 程序从入口调用`main()`; +3. `serialize_book()`创建图书字典; +4. `json.dumps()`返回JSON字符串; +5. `print()`把JSON字符串显示到终端; +6. `deserialize_request()`接收一段JSON请求体; +7. `json.loads()`返回Python字典; +8. 程序读取字典中的`title`并输出。 + +## 十四、动手观察 + +建议复制示例后依次做以下小实验,每次只改一处: + +1. 删除`ensure_ascii=False`,观察中文显示方式; +2. 把`indent=2`改为`indent=4`,观察缩进; +3. 在字典中增加`"stock": 10`,观察number; +4. 把请求体中的双引号改成单引号,观察解析错误; +5. 在JSON最后一个属性后增加逗号,观察解析错误。 + +实验结束后恢复标准示例,不要把故意制造的错误保留在示例文件中。 + +## 十五、常见错误 + +### 15.1 把Python字典文本当成JSON + +`{'available': True}`是Python显示形式,不是合法JSON。应使用`json.dumps()`生成。 + +### 15.2 JSON使用单引号 + +JSON属性名和字符串必须使用双引号。接口工具中输入单引号通常会导致400错误。 + +### 15.3 最后一项后保留逗号 + +JSON标准不允许尾随逗号。编辑器中的JavaScript对象可能允许,但JSON解析器通常拒绝。 + +### 15.4 直接序列化不支持的对象 + +`Decimal`、日期时间和ORM对象不能默认转换。应先转换为接口明确约定的字符串、数字或字典。 + +### 15.5 解析成功就直接写数据库 + +JSON语法正确不等于业务有效。服务器还要校验必填字段、长度、数值范围和权限。 + +### 15.6 把敏感字段放入响应 + +对象能序列化不代表所有字段都应返回。密码、密钥和内部状态必须排除。 + +## 十六、课堂练习 + +打开`practice.py`,依次完成基本转换、错误处理和HTTP消息观察。练习文件不包含答案。 + +## 十七、本课小结 + +JSON是文本格式,不是Python字典。`json.dumps()`负责序列化,`json.loads()`负责反序列化。HTTP通过`Content-Type`声明正文是JSON,语法解析之后仍要进行业务校验。 + +## 十八、验收标准 + +- 能区分Python对象、JSON文本和UTF-8字节; +- 能说出JSON支持的六类值; +- 能独立完成序列化和反序列化; +- 能解释`ensure_ascii`和`indent`的作用; +- 能识别单引号、尾随逗号等格式错误; +- 能说明JSON语法校验与业务字段校验的区别; +- 示例可独立运行; +- 完成`practice.py`中的练习。 diff --git a/05_web基础/5_4_JSON数据交换/json_rest_example.py b/05_web基础/5_4_JSON数据交换/json_rest_example.py new file mode 100644 index 0000000..97d0a6b --- /dev/null +++ b/05_web基础/5_4_JSON数据交换/json_rest_example.py @@ -0,0 +1,36 @@ +"""第5-4课标准示例:完成Python对象与JSON文本的双向转换。""" + +import json + + +def serialize_book() -> str: + """把Python图书对象序列化为JSON文本。""" + + book = { + "id": 1, + "title": "Python Web基础", + "price": 59.9, + "available": True, + "tags": ["Python", "Web"], + "description": None, + } + return json.dumps(book, ensure_ascii=False, indent=2) + + +def deserialize_request(request_body: str) -> dict: + """把客户端发送的JSON文本反序列化为Python字典。""" + + return json.loads(request_body) + + +def main() -> None: + """依次展示序列化和反序列化结果。""" + + print("JSON响应:") + print(serialize_book()) + request_data = deserialize_request('{"title": "HTTP实践", "price": 49.0}') + print(f"请求中的书名:{request_data['title']}") + + +if __name__ == "__main__": + main() diff --git a/05_web基础/5_4_JSON数据交换/practice.py b/05_web基础/5_4_JSON数据交换/practice.py new file mode 100644 index 0000000..dc761a2 --- /dev/null +++ b/05_web基础/5_4_JSON数据交换/practice.py @@ -0,0 +1,60 @@ +# 第5-4课练习:JSON数据交换 +# +# 本文件只提供题目、预期结果、自查清单和验收标准,不包含参考答案或代码骨架。 + + +# 第一部分:创建并序列化Python对象 +# 1. 创建一个图书字典,包含编号、书名、作者、价格、是否上架、标签和备注。 +# 2. 值应覆盖字符串、数字、布尔值、列表和None。 +# 3. 使用json.dumps()转换为JSON文本。 +# 4. 要求中文直接显示,并使用两个空格缩进。 +# 5. 输出转换后的JSON文本。 + + +# 第二部分:反序列化请求体 +# 1. 准备一段新增图书的合法JSON字符串。 +# 2. 使用json.loads()转换为Python字典。 +# 3. 分别输出书名、作者和价格。 +# 4. 输出转换后对象的类型,确认它是dict而不是str。 + + +# 第三部分:处理错误JSON +# 1. 依次测试属性名使用单引号、最后一项带逗号、缺少右花括号三种错误。 +# 2. 捕获json.JSONDecodeError。 +# 3. 为使用者输出简洁中文提示,不直接把程序终止在英文堆栈处。 +# 4. 在注释中记录每段JSON为什么无效。 + + +# 第四部分:观察HTTP中的JSON +# 1. 用多行字符串写出一份POST /books请求报文。 +# 2. 请求头必须包含Content-Type: application/json; charset=utf-8。 +# 3. 请求体包含title与price。 +# 4. 再写出一份状态码为201的JSON响应报文。 +# 5. 在注释中标出请求体、响应体与Content-Type分别位于哪里。 + + +# 预期关键输出: +# 1. 序列化结果中的中文可以直接阅读; +# 2. JSON中的true、false和null使用小写; +# 3. 反序列化后的对象类型为dict; +# 4. 合法请求能够输出书名、作者和价格; +# 5. 三段无效JSON都得到中文错误提示。 + + +# 自查清单: +# 1. 是否导入Python标准库json? +# 2. 是否使用json.dumps()而不是str(dict)? +# 3. JSON文本中的属性名和字符串是否使用双引号? +# 4. 是否理解True与true、None与null的区别? +# 5. 是否捕获了json.JSONDecodeError? +# 6. 是否没有直接序列化Decimal、datetime或自定义对象? +# 7. HTTP请求与响应是否正确声明Content-Type? + + +# 最终验收标准: +# 1. practice.py通过语法检查并可独立运行; +# 2. Python对象能够转换为格式化中文JSON; +# 3. 合法JSON能够转换为Python字典; +# 4. 三类格式错误都能安全处理; +# 5. 能解释Python对象、JSON文本和网络字节的区别; +# 6. 能指出HTTP报文中的JSON正文和Content-Type。 diff --git a/05_web基础/5_5_RESTful_API设计/README.md b/05_web基础/5_5_RESTful_API设计/README.md new file mode 100644 index 0000000..6d1ad8b --- /dev/null +++ b/05_web基础/5_5_RESTful_API设计/README.md @@ -0,0 +1,249 @@ +# 第5-5课:RESTful API设计 + +## 一、本课目标 + +完成本课后,你能够: + +1. 理解资源、资源标识和资源表现形式; +2. 解释表述性状态转移(Representational State Transfer,REST)的基本思想; +3. 使用路径、HTTP方法和状态码共同描述一个接口; +4. 区分列表接口、详情接口和业务动作接口; +5. 为图书管理系统设计一组结构一致的RESTful API。 + +## 二、前置知识 + +- 已学过URL和HTTP请求方法; +- 已学过HTTP响应和常用状态码; +- 已学过JSON请求体和响应体; +- 不要求接触过FastAPI、Django或任何接口框架。 + +## 三、先理解“资源” + +资源(Resource)是系统希望通过接口管理的业务对象,例如图书、作者、用户和订单。它不是只指数据库中的一行,也不是只指一个Python对象,而是从接口使用者角度看到的业务事物。 + +一本编号为10的图书可以用路径表示: + +```text +/books/10 +``` + +这里: + +- `books`表示图书资源集合; +- `10`用于标识集合中的一本图书; +- 返回的JSON是这本图书在当前时刻的一种表现形式。 + +同一个资源可以有JSON、HTML等不同表现形式,但后端API通常返回JSON。 + +## 四、REST与RESTful是什么 + +REST是一种Web接口架构风格,不是协议、框架或Python库。RESTful表示一个接口的设计遵循REST的资源化思想。 + +入门阶段先掌握三条核心规则: + +1. 路径主要描述资源,优先使用名词; +2. HTTP方法描述想对资源做什么; +3. HTTP状态码描述处理结果。 + +因此创建图书通常写成: + +```text +POST /books → 201 Created +``` + +而不是: + +```text +POST /createBook → 200 OK +``` + +后一种不一定不能运行,但它没有充分利用HTTP已有的语义。 + +## 五、从一个接口开始设计 + +设计接口时按以下顺序思考: + +1. 管理的业务资源是什么?这里是图书; +2. 是操作集合还是单个资源?`/books`是集合,`/books/10`是单个资源; +3. 操作意图是什么?查询、创建、修改还是删除; +4. 输入放在哪里?路径参数、查询参数或JSON请求体; +5. 成功返回什么状态码和数据? +6. 可能失败的情况有哪些? + +## 六、图书资源接口 + +| 方法 | 路径 | 输入位置 | 成功状态 | 作用 | +|---|---|---|---:|---| +| GET | `/books` | 查询参数 | 200 | 查询图书列表 | +| GET | `/books/{book_id}` | 路径参数 | 200 | 查询一本图书 | +| POST | `/books` | JSON请求体 | 201 | 创建图书 | +| PUT | `/books/{book_id}` | JSON请求体 | 200或204 | 整体替换图书 | +| PATCH | `/books/{book_id}` | JSON请求体 | 200或204 | 修改部分字段 | +| DELETE | `/books/{book_id}` | 路径参数 | 204 | 删除图书 | + +表中的`{book_id}`是路径参数占位符。真实请求会写成`/books/10`,花括号不会原样发送。 + +## 七、列表、详情与分页 + +列表接口操作资源集合: + +```text +GET /books?keyword=Python&page=1&page_size=20 +``` + +- `keyword`负责筛选; +- `page`表示第几页; +- `page_size`表示每页多少条。 + +一种清晰的列表响应是: + +```json +{ + "items": [ + {"id": 10, "title": "Python入门"} + ], + "page": 1, + "page_size": 20, + "total": 1 +} +``` + +详情接口操作单个资源: + +```text +GET /books/10 +``` + +存在时返回200和图书JSON;不存在时返回404,而不是返回200和空字符串。 + +## 八、新增接口的完整设计 + +请求: + +```http +POST /books HTTP/1.1 +Content-Type: application/json + +{"title": "Web基础", "price": 59.00} +``` + +成功响应: + +```http +HTTP/1.1 201 Created +Location: /books/10 +Content-Type: application/json + +{"id": 10, "title": "Web基础", "price": 59.00} +``` + +`Location`响应头告诉客户端新资源的地址。常见失败包括JSON格式错误、字段不合规则和图书编号冲突。 + +## 九、PUT与PATCH的区别 + +- PUT通常表示用请求内容整体替换资源的可修改状态; +- PATCH通常表示只修改请求中出现的字段。 + +例如只修改价格时,PATCH请求体可以是: + +```json +{"price": 69.00} +``` + +项目必须明确PUT缺少字段时如何处理。不要仅凭方法名称猜测,接口文档需要写清楚。 + +## 十、业务动作如何设计 + +不是所有业务都能自然表示为基础增删改查。例如“取消订单”是一个带业务规则的动作。 + +可以把取消记录理解成订单下的子资源: + +```text +POST /orders/10/cancellations +``` + +也有项目使用: + +```text +POST /orders/10/cancel +``` + +RESTful不是机械规则。选择能够清楚表达业务、便于权限与审计、并且全项目一致的方案更重要。 + +## 十一、错误响应也需要设计 + +同一个详情接口可能产生不同结果: + +| 情况 | 状态码 | 响应含义 | +|---|---:|---| +| 查询成功 | 200 | 返回资源 | +| 编号格式错误 | 400或422 | 客户端输入不符合要求 | +| 图书不存在 | 404 | 找不到指定资源 | +| 未登录 | 401 | 需要完成身份认证 | +| 没有权限 | 403 | 身份已知但禁止访问 | +| 后端未知异常 | 500 | 服务器未能完成处理 | + +错误响应应保持固定结构,例如包含错误代码和面向调用者的消息,但不能泄露数据库密码或程序堆栈。 + +## 十二、完整示例 + +`rest_api_design_example.py`不建立网络连接,而是把一组接口设计保存为Python列表并输出。这样可以把注意力放在方法、路径和状态码的组合上。 + +## 十三、运行方法 + +```powershell +cd D:\Code\Python\05_web基础\5_5_RESTful_API设计 +python rest_api_design_example.py +``` + +预期输出包含五条图书接口和一个带筛选、分页参数的列表请求示例。 + +## 十四、代码执行顺序 + +1. `main()`调用`build_book_api_design()`; +2. 函数返回由多个字典组成的列表; +3. `print_api_design()`逐条读取接口; +4. 每条接口按统一列顺序输出; +5. 最后输出列表筛选示例。 + +示例中的数据结构只是“接口设计表”,不是服务器,也不会真的新增或删除图书。 + +## 十五、常见错误 + +### 15.1 路径全部使用动词 + +`/getBooks`、`/createBook`和`/deleteBook`会把HTTP方法已有的意图重复写入路径,并使接口越来越不一致。 + +### 15.2 用GET修改数据 + +浏览器、缓存或爬虫可能自动访问GET链接。GET应当用于读取,不能因为输入方便就用它删除数据。 + +### 15.3 列表接口一次返回全部数据 + +数据增长后会拖慢数据库、服务器和客户端。列表接口应尽早明确分页规则和最大每页数量。 + +### 15.4 只设计成功响应 + +客户端真正需要处理的往往是校验失败、资源不存在、状态冲突和权限不足。接口设计必须包含失败边界。 + +### 15.5 认为RESTful只有唯一答案 + +REST提供设计思想,复杂业务动作可能有多种合理表达。项目内一致、语义清楚和文档完整比形式上的绝对统一更重要。 + +## 十六、课堂练习 + +打开`practice.py`,完成作者资源、订单业务动作和反例改造练习。练习文件不含参考答案。 + +## 十七、本课小结 + +RESTful API围绕资源组织路径,用HTTP方法表达操作,用状态码表达结果。设计一个接口时,不仅要考虑成功路径,还要明确输入位置、错误状态和响应结构。 + +## 十八、验收标准 + +- 能解释资源、集合路径和详情路径; +- 能组合路径、方法和状态码; +- 能区分查询参数、路径参数和JSON请求体; +- 能解释PUT与PATCH的基本差异; +- 能设计包含失败结果的图书接口; +- 示例可独立运行; +- 完成`practice.py`中的练习。 diff --git a/05_web基础/5_5_RESTful_API设计/practice.py b/05_web基础/5_5_RESTful_API设计/practice.py new file mode 100644 index 0000000..2c1bc1e --- /dev/null +++ b/05_web基础/5_5_RESTful_API设计/practice.py @@ -0,0 +1,49 @@ +# 第5-5课练习:RESTful API设计 +# +# 本文件只提供题目、预期结果、自查清单和验收标准,不包含参考答案或代码骨架。 + + +# 练习一:设计作者资源接口 +# 1. 设计查询作者列表、查询一个作者、新增作者、修改作者和删除作者的接口。 +# 2. 每个接口写明HTTP方法、路径、成功状态码和用途。 +# 3. 列表接口增加name查询参数和分页参数。 +# 4. 详情接口使用author_id作为路径参数。 + + +# 练习二:设计订单业务接口 +# 1. 设计查询订单列表、查询订单详情和创建订单的接口。 +# 2. 为“取消订单”设计一个能清楚表达业务动作的接口。 +# 3. 分别考虑成功、订单不存在、订单状态不允许取消和请求字段错误。 +# 4. 为上述结果选择合适的状态码,并用注释解释选择原因。 + + +# 练习三:检查不推荐的接口 +# 分析以下路径的问题,并写出更清晰的设计: +# 1. GET /getAllBooks +# 2. POST /createBook +# 3. POST /deleteBook?id=10 +# 4. GET /books/delete/10 + + +# 预期关键结果: +# 1. 资源路径以名词为主,操作意图主要由HTTP方法表达; +# 2. 列表和详情具有不同路径; +# 3. 查询条件使用查询参数,资源编号使用路径参数; +# 4. 成功与失败结果都有明确状态码。 + + +# 自查清单: +# 1. 路径是否围绕资源设计,而不是堆叠动词? +# 2. 同一类资源的路径命名是否一致? +# 3. HTTP方法是否符合读取、创建、修改和删除的意图? +# 4. 是否区分路径参数、查询参数和JSON请求体? +# 5. 是否为创建操作选择201,为无响应体删除选择204? +# 6. 是否考虑400、404、409和422等失败状态? + + +# 最终验收标准: +# 1. practice.py保留为题目文件,不写入参考答案; +# 2. 作者资源至少包含五个接口; +# 3. 订单接口能表达查询、创建和取消; +# 4. 能解释四个不推荐接口的问题; +# 5. 能用一句话说明RESTful是设计风格,不是某个Python框架。 diff --git a/05_web基础/5_5_RESTful_API设计/rest_api_design_example.py b/05_web基础/5_5_RESTful_API设计/rest_api_design_example.py new file mode 100644 index 0000000..4646a31 --- /dev/null +++ b/05_web基础/5_5_RESTful_API设计/rest_api_design_example.py @@ -0,0 +1,63 @@ +"""第5-5课标准示例:用数据表格表达一组RESTful图书接口。""" + + +def build_book_api_design() -> list[dict]: + """返回图书资源的接口设计,不发送真实网络请求。""" + + return [ + { + "method": "GET", + "path": "/books", + "success_status": 200, + "purpose": "分页查询图书", + }, + { + "method": "GET", + "path": "/books/{book_id}", + "success_status": 200, + "purpose": "查询一本图书", + }, + { + "method": "POST", + "path": "/books", + "success_status": 201, + "purpose": "创建图书", + }, + { + "method": "PATCH", + "path": "/books/{book_id}", + "success_status": 200, + "purpose": "局部修改图书", + }, + { + "method": "DELETE", + "path": "/books/{book_id}", + "success_status": 204, + "purpose": "删除图书", + }, + ] + + +def print_api_design(api_design: list[dict]) -> None: + """按“方法、路径、状态码、用途”的顺序输出接口设计。""" + + print("方法 路径 成功状态 用途") + for api in api_design: + print( + f"{api['method']:<6} " + f"{api['path']:<20} " + f"{api['success_status']:<9} " + f"{api['purpose']}" + ) + + +def main() -> None: + """创建并输出图书资源接口设计。""" + + api_design = build_book_api_design() + print_api_design(api_design) + print("列表筛选示例:GET /books?keyword=Python&page=1&page_size=20") + + +if __name__ == "__main__": + main() diff --git a/05_web基础/5_6_HTTP接口调试与综合实践/README.md b/05_web基础/5_6_HTTP接口调试与综合实践/README.md new file mode 100644 index 0000000..b3b87f4 --- /dev/null +++ b/05_web基础/5_6_HTTP接口调试与综合实践/README.md @@ -0,0 +1,307 @@ +# 第5-6课:HTTP接口调试与综合实践 + +## 一、本课定位 + +本课把客户端、服务器、URL、HTTP、状态码、JSON和RESTful API组合成一个可运行的本地闭环。示例使用Python标准库搭建临时图书接口,只服务于协议观察,不替代下一阶段的FastAPI。 + +## 二、本课目标 + +完成本课后,你能够: + +1. 启动一个只监听本机的临时HTTP服务; +2. 使用客户端代码发送GET和POST请求; +3. 同时检查状态码、响应头和JSON响应体; +4. 识别JSON格式错误、字段错误和资源不存在; +5. 设计图书管理系统的RESTful API; +6. 说明HTTP知识如何迁移到FastAPI。 + +## 三、前置知识 + +- 客户端、服务器和请求响应流程; +- URL、HTTP方法、请求头和请求体; +- 常见状态码; +- JSON转换与RESTful API设计; +- Python类、异常、线程和上下文管理器的基本阅读能力。 + +线程(Thread)表示同一程序中的一条执行路线。本课只用后台线程让服务器等待请求,主线程同时充当客户端;不展开并发编程细节。 + +本课示例使用了前面阶段学过的类、异常和上下文管理器,但只要求按调用流程阅读,不重新讲解这些Python语法。 + +## 四、为什么现在才启动真实HTTP服务 + +前五课分别拆开观察了通信角色、URL、请求、响应、JSON和接口设计。如果一开始就使用框架,路由、自动校验和自动序列化会让新手看到结果,却不容易分清是谁完成了哪一步。 + +本课使用标准库建立一个最小闭环: + +1. 程序在本机启动HTTP服务器; +2. 客户端向它发送真实HTTP请求; +3. 服务器读取路径、请求头和请求体; +4. 服务器修改内存数据; +5. 服务器返回状态码、响应头和JSON; +6. 客户端读取并输出响应; +7. 全部测试结束后关闭服务器。 + +它是教学服务器,不是下一阶段要长期使用的项目框架。 + +## 五、先认识本课的新名词 + +| 名词 | 本课中的含义 | +|---|---| +| `localhost` | 表示自己的计算机,常对应`127.0.0.1` | +| 监听(Listen) | 服务器等待某个地址和端口上的请求 | +| 随机端口 | 传入端口0,由操作系统选择当前可用端口 | +| 处理器(Handler) | 收到请求后负责读取并生成响应的代码 | +| 后台线程 | 在同一程序中持续运行服务器,让主流程可以发送请求 | +| 内存数据 | 只存在于当前程序运行期间,结束后不会保存 | +| 超时(Timeout) | 等待超过限定时间后停止继续等待 | + +`127.0.0.1`只指向本机。绑定该地址可以避免本课没有认证的服务被局域网其他设备访问。 + +## 六、完整调用链 + +```text +send_request() + ↓ HTTP请求 +BookApiHandler + ↓ 路径匹配、JSON解析、字段校验 +内存图书列表 + ↓ JSON响应 +send_request()读取状态码、响应头和响应体 +``` + +示例绑定`127.0.0.1`,只接受本机连接;端口传入`0`,由操作系统选择当前可用端口,避免与现有服务冲突。数据只保存在内存中,程序结束即消失。 + +同一个Python程序同时扮演两种角色: + +- `ThreadingHTTPServer`和`BookApiHandler`是服务器; +- `send_request()`是客户端; +- 它们通过本机网络连接交换HTTP消息,不是普通函数直接传参。 + +## 七、服务器如何启动 + +```python +server = ThreadingHTTPServer(("127.0.0.1", 0), BookApiHandler) +``` + +这行代码只创建服务器对象,还没有开始持续等待请求。地址元组中: + +- `127.0.0.1`限制为本机; +- `0`请求操作系统分配端口; +- `BookApiHandler`指定收到请求后由哪个类处理。 + +随后创建后台线程并执行`serve_forever()`。名称表示服务器会持续等待,直到程序调用`shutdown()`。 + +## 八、服务端重点 + +`BaseHTTPRequestHandler`按请求方法调用`do_GET()`或`do_POST()`。`send_json()`负责: + +1. 将Python对象序列化为JSON; +2. 编码成UTF-8字节; +3. 写入状态码和响应头; +4. 按字节数计算`Content-Length`; +5. 写出响应体。 + +中文字符的字符数量与UTF-8字节数量不同,因此必须对编码后的`response_body`调用`len()`。 + +### 8.1 GET请求 + +客户端请求`GET /books`时,标准库调用`do_GET()`: + +1. 检查`self.path`是否等于`/books`; +2. 路径匹配时返回200和图书列表; +3. 路径不匹配时返回404和错误JSON。 + +### 8.2 POST请求 + +客户端请求`POST /books`时,标准库调用`do_POST()`: + +1. 检查路径; +2. 从`Content-Length`得知需要读取多少字节; +3. 读取请求体并按UTF-8解码; +4. 使用`json.loads()`解析JSON; +5. 校验`title`是否为非空字符串; +6. 创建新图书并加入内存列表; +7. 返回201、新图书JSON和`Location`响应头。 + +JSON语法错误返回400,字段不符合要求返回422,未知路径返回404。三个结果代表不同问题。 + +## 九、客户端重点 + +`urllib.request.Request`创建请求,`urlopen()`发送请求。遇到4xx或5xx时,标准库会抛出`HTTPError`,但错误对象仍包含状态码和响应体,需要读取后再判断原因。 + +接口调试不能只看响应体。至少检查: + +- 请求方法与完整URL; +- 请求头和请求体; +- HTTP状态码; +- 响应`Content-Type`; +- JSON响应结构; +- 服务端数据是否按预期变化。 + +客户端发送JSON前,需要完成与服务器相反的转换: + +```text +Python字典 → JSON字符串 → UTF-8字节 → HTTP请求体 +``` + +客户端收到响应后再反向处理: + +```text +HTTP响应体字节 → UTF-8字符串 → 显示或解析JSON +``` + +示例为保持输出直观,只显示JSON字符串。实际业务客户端通常还会使用`json.loads()`转换成对象。 + +## 十、程序完整执行顺序 + +1. 创建服务器对象; +2. 创建并启动后台线程; +3. 从服务器对象取得操作系统分配的端口; +4. 第一次GET查询初始列表; +5. POST创建“HTTP接口实践”; +6. 第二次GET验证列表已经变化; +7. GET未知路径验证404; +8. 无论中间是否出错,`finally`都会关闭服务器; +9. 主线程等待后台线程结束; +10. 程序输出“本地服务已关闭”。 + +这里先新增再查询,是为了验证POST不只是返回201,还真实改变了当前程序的内存状态。 + +## 十一、运行方法 + +```powershell +cd D:\Code\Python\05_web基础\5_6_HTTP接口调试与综合实践 +python local_book_api_example.py +``` + +## 十二、预期结果 + +```text +本地服务已启动:http://127.0.0.1:随机端口号 +GET http://127.0.0.1:相同端口/books -> 200 +响应体:{"items": [{"id": 1, "title": "Python入门"}]} +POST http://127.0.0.1:相同端口/books -> 201 +响应体:{"id": 2, "title": "HTTP接口实践"} +GET http://127.0.0.1:相同端口/books -> 200 +响应体中包含两本图书 +GET http://127.0.0.1:相同端口/missing -> 404 +响应体:{"message": "资源不存在"} +本地服务已关闭。 +``` + +端口号每次不同是正常现象。第二次查询应包含新创建的“HTTP接口实践”。 + +## 十三、怎样判断测试真正通过 + +不要只检查程序是否结束,应逐项确认: + +1. 四次请求使用同一个随机端口; +2. 第一次GET只有初始图书; +3. POST状态码是201而不是200; +4. POST响应中生成了编号2; +5. 第二次GET包含编号1和编号2; +6. 未知路径返回404; +7. 最后一行确认服务器已关闭; +8. 再运行一次时仍从一条初始数据开始。 + +最后一项说明数据只在内存中,没有持久保存,也说明示例可重复运行。 + +## 十四、使用接口调试工具 + +示例服务会自动结束,不适合手工操作。完成`practice.py`时,可以让服务等待键盘输入后再关闭,然后使用Postman、Apifox或IDE内置HTTP客户端发送请求: + +```http +GET http://127.0.0.1:端口/books +``` + +```http +POST http://127.0.0.1:端口/books +Content-Type: application/json + +{"title": "Web实践", "author": "张三"} +``` + +不要把服务绑定到`0.0.0.0`,本课没有实现认证和生产安全配置。 + +调试工具中的请求配置通常分为: + +1. 选择HTTP方法; +2. 输入完整URL; +3. 配置请求头; +4. 选择JSON请求体并输入数据; +5. 发送请求; +6. 查看状态码、耗时、响应头和响应体。 + +不要只复制响应内容,还要记录发送了什么请求,否则别人无法复现问题。 + +## 十五、图书管理API设计草案 + +| 方法 | 路径 | 成功状态 | 作用 | +|---|---|---:|---| +| GET | `/books` | 200 | 分页查询图书 | +| GET | `/books/{book_id}` | 200 | 查询图书详情 | +| POST | `/books` | 201 | 新增图书 | +| PATCH | `/books/{book_id}` | 200 | 修改部分字段 | +| DELETE | `/books/{book_id}` | 204 | 删除图书 | + +常见失败包括400格式错误、404图书不存在、409编号冲突和422字段校验失败。实际项目还要处理认证、权限、日志、数据库事务和并发。 + +## 十六、与FastAPI的衔接 + +下一阶段会由FastAPI代替底层标准库代码: + +- 路由装饰器代替手工判断路径和方法; +- Pydantic模型代替手工字段校验; +- 框架自动序列化JSON并生成接口文档; +- 异常处理器统一生成错误响应。 + +不会改变的是HTTP层的设计:方法、路径、参数、状态码、请求结构和响应结构仍需开发者决定。 + +## 十七、常见错误 + +### 17.1 服务启动后程序不结束 + +服务器会持续等待请求。必须调用`shutdown()`和`server_close()`,并等待后台线程结束。 + +### 17.2 只测试成功情况 + +至少覆盖未知路径、无效JSON和缺少必填字段,否则无法确认错误边界。 + +### 17.3 把标准库示例当作生产服务器 + +本例只用于教学,没有完整认证、限流、日志、安全加固和部署能力。 + +### 17.4 把服务绑定到`0.0.0.0` + +这会让服务监听所有可用网络接口。本课没有认证,不应扩大访问范围。 + +### 17.5 按字符数计算`Content-Length` + +HTTP传输的是字节。包含中文时必须先编码,再计算字节长度。 + +### 17.6 忘记处理`HTTPError` + +标准库遇到4xx和5xx会抛出`HTTPError`。错误对象中仍有响应体,应读取并用于定位问题。 + +### 17.7 端口不同就认为结果错误 + +本课故意使用随机可用端口。只要同一次运行中的请求端口一致即可。 + +## 十八、课堂练习 + +打开`practice.py`,完成图书管理接口设计、服务端实现和客户端调试。练习不提供答案或代码骨架。 + +## 十九、本课小结 + +一个接口是否正确,要从客户端请求、服务器处理和HTTP响应三个方向共同验证。框架会减少底层代码,但不会代替接口语义和业务规则设计。 + +## 二十、验收标准 + +- 本地服务能自动启动和关闭; +- GET、POST和404请求输出符合预期; +- JSON中文编码与`Content-Length`正确; +- 能设计完整的图书管理接口; +- 能说明本阶段知识与FastAPI的对应关系; +- 能完整描述GET和POST在服务器中的处理顺序; +- 能从请求方法、URL、状态码、响应头、响应体和数据变化六方面调试接口; +- 完成`practice.py`综合练习。 diff --git a/05_web基础/5_6_HTTP接口调试与综合实践/local_book_api_example.py b/05_web基础/5_6_HTTP接口调试与综合实践/local_book_api_example.py new file mode 100644 index 0000000..0cc687f --- /dev/null +++ b/05_web基础/5_6_HTTP接口调试与综合实践/local_book_api_example.py @@ -0,0 +1,140 @@ +"""第5-6课标准示例:启动本地HTTP服务并自动调用图书接口。""" + +import json +from http import HTTPStatus +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from threading import Thread +from urllib.error import HTTPError +from urllib.request import Request, urlopen + + +class BookApiHandler(BaseHTTPRequestHandler): + """处理本课图书查询和新增请求的最小HTTP处理器。""" + + # 本课不连接数据库,使用类属性保存当前程序运行期间的图书。 + # 程序退出后这份列表会消失,再次运行时会重新从一条图书开始。 + books = [{"id": 1, "title": "Python入门"}] + + def send_json( + self, + status: HTTPStatus, + data: dict | list, + extra_headers: dict | None = None, + ) -> None: + """把Python对象转换为UTF-8 JSON响应。""" + + # HTTP连接传输的是字节,因此先生成JSON字符串,再编码成UTF-8字节。 + response_body = json.dumps(data, ensure_ascii=False).encode("utf-8") + self.send_response(status) + self.send_header("Content-Type", "application/json; charset=utf-8") + # Content-Length必须填写字节数量,不能直接计算中文字符串的字符数。 + self.send_header("Content-Length", str(len(response_body))) + if extra_headers is not None: + for header_name, header_value in extra_headers.items(): + self.send_header(header_name, header_value) + self.end_headers() + self.wfile.write(response_body) + + def do_GET(self) -> None: + """处理GET /books,并拒绝未知路径。""" + + # 标准库收到GET请求后会自动调用本方法,self.path保存请求目标。 + if self.path == "/books": + self.send_json(HTTPStatus.OK, {"items": self.books}) + return + self.send_json(HTTPStatus.NOT_FOUND, {"message": "资源不存在"}) + + def do_POST(self) -> None: + """处理POST /books,校验JSON并创建图书。""" + + if self.path != "/books": + self.send_json(HTTPStatus.NOT_FOUND, {"message": "资源不存在"}) + return + + try: + # Content-Length告诉服务器本次请求体一共有多少字节。 + content_length = int(self.headers.get("Content-Length", "0")) + request_body = self.rfile.read(content_length).decode("utf-8") + request_data = json.loads(request_body) + except (UnicodeDecodeError, json.JSONDecodeError): + self.send_json(HTTPStatus.BAD_REQUEST, {"message": "JSON格式错误"}) + return + + title = request_data.get("title") + if not isinstance(title, str) or not title.strip(): + self.send_json( + HTTPStatus.UNPROCESSABLE_CONTENT, + {"message": "书名不能为空"}, + ) + return + + # 本课用列表长度模拟生成编号;真实项目通常由数据库生成主键。 + book = {"id": len(self.books) + 1, "title": title.strip()} + self.books.append(book) + self.send_json( + HTTPStatus.CREATED, + book, + {"Location": f"/books/{book['id']}"}, + ) + + def log_message(self, format_text: str, *args: object) -> None: + """关闭标准库默认访问日志,让教学输出保持清晰。""" + + +def send_request( + url: str, + method: str = "GET", + data: dict | None = None, +) -> None: + """作为客户端发送HTTP请求并输出响应。""" + + request_body = None + headers = {"Accept": "application/json"} + if data is not None: + request_body = json.dumps(data, ensure_ascii=False).encode("utf-8") + headers["Content-Type"] = "application/json; charset=utf-8" + + # Request只负责描述请求;urlopen()执行时才真正连接服务器并发送请求。 + request = Request(url, data=request_body, headers=headers, method=method) + try: + with urlopen(request, timeout=3) as response: + response_text = response.read().decode("utf-8") + print(f"{method} {request.full_url} -> {response.status}") + print(f"响应体:{response_text}") + except HTTPError as error: + response_text = error.read().decode("utf-8") + print(f"{method} {request.full_url} -> {error.code}") + print(f"响应体:{response_text}") + + +def main() -> None: + """在随机本地端口启动服务,完成调用后安全关闭。""" + + # 127.0.0.1限制为本机访问;端口0表示让操作系统选择可用端口。 + server = ThreadingHTTPServer(("127.0.0.1", 0), BookApiHandler) + # 服务器需要持续等待请求,因此放入后台线程;主线程继续充当客户端。 + server_thread = Thread(target=server.serve_forever, daemon=True) + server_thread.start() + host, port = server.server_address + base_url = f"http://{host}:{port}" + print(f"本地服务已启动:{base_url}") + + try: + send_request(f"{base_url}/books") + send_request( + f"{base_url}/books", + method="POST", + data={"title": "HTTP接口实践"}, + ) + send_request(f"{base_url}/books") + send_request(f"{base_url}/missing") + finally: + # 即使某次请求失败,也必须释放监听端口并等待后台线程退出。 + server.shutdown() + server.server_close() + server_thread.join() + print("本地服务已关闭。") + + +if __name__ == "__main__": + main() diff --git a/05_web基础/5_6_HTTP接口调试与综合实践/practice.py b/05_web基础/5_6_HTTP接口调试与综合实践/practice.py new file mode 100644 index 0000000..06630ed --- /dev/null +++ b/05_web基础/5_6_HTTP接口调试与综合实践/practice.py @@ -0,0 +1,56 @@ +# 第5-6课练习:HTTP接口调试与综合实践 +# +# 本文件只提供题目、预期结果、自查清单和验收标准,不包含参考答案或代码骨架。 + + +# 综合任务:设计并调试图书管理HTTP接口 +# +# 第一部分:接口设计 +# 1. 设计查询图书列表、查询详情、新增、局部修改和删除接口。 +# 2. 列表接口支持keyword和page查询参数。 +# 3. 为每个接口写出方法、路径、请求体、成功状态码和失败状态码。 +# 4. 不需要连接数据库,可以使用内存列表保存本次运行的数据。 + + +# 第二部分:服务端实现 +# 1. 使用http.server实现GET /books和GET /books/{id}。 +# 2. 实现POST /books,接收JSON格式的title与author。 +# 3. 校验JSON格式和必填字段,返回合理的4xx状态码。 +# 4. 创建成功返回201、图书JSON和Location响应头。 +# 5. 未知路径返回404;服务器只绑定127.0.0.1和随机端口。 + + +# 第三部分:客户端调试 +# 1. 使用urllib.request依次调用列表、详情、新增和未知路径。 +# 2. 输出请求方法、完整URL、状态码、Content-Type和响应体。 +# 3. 额外发送一次无效JSON或缺少书名的请求,检查错误响应。 +# 4. 使用try/finally保证测试完成或失败后都关闭服务器。 + + +# 预期关键结果: +# 1. 初始列表至少包含一条图书; +# 2. 新增接口返回201和新图书编号; +# 3. 再次查询列表能够看到新增图书; +# 4. 查询不存在的编号返回404; +# 5. 缺少必填字段返回400或422; +# 6. 最终输出“本地服务已关闭”。 + + +# 自查清单: +# 1. 服务是否只监听127.0.0.1,而不是暴露到局域网? +# 2. Content-Length是否按UTF-8字节长度计算? +# 3. JSON响应是否声明application/json与UTF-8? +# 4. 是否区分JSON语法错误、字段错误和资源不存在? +# 5. 客户端是否同时检查状态码、响应头和响应体? +# 6. 是否在finally中关闭服务并等待线程结束? +# 7. 是否能画出客户端、HTTP服务、业务数据之间的调用方向? + + +# 最终验收标准: +# 1. practice.py通过语法检查并能连续运行两次; +# 2. 完成至少两个GET接口和一个POST接口; +# 3. 新增后列表数据发生预期变化; +# 4. 成功与失败响应的状态码和JSON结构合理; +# 5. 不访问公网、不连接生产数据库、不长期占用固定端口; +# 6. 能使用接口调试工具复现同样请求; +# 7. 能解释这些接口迁移到FastAPI后哪些业务设计保持不变。 diff --git a/README.md b/README.md index 336bff0..a4e1249 100644 --- a/README.md +++ b/README.md @@ -90,13 +90,18 @@ ### 第五阶段:Web 开发基础 -- 浏览器、客户端和服务器; -- HTTP 请求与响应; -- URL、请求方法和状态码; -- JSON 数据格式; -- RESTful API 设计; -- 使用接口调试工具发送请求; -- 前端与后端的基本交互过程。 +本阶段作为数据库编程与FastAPI之间的衔接,不引入第三方Web框架,重点理解客户端与后端进行通信时共同遵守的HTTP规则。课程按纯新手可以独立学习的标准组织,不要求学习者已经掌握Web开发知识;已经在前四阶段讲过的Python语法、面向对象和数据库内容不重复展开。 + +本阶段共六课: + +1. `5_1_Web运行原理与客户端服务器`:区分Web、互联网、客户端、服务器、前端和后端,梳理一次请求响应的完整过程; +2. `5_2_URL与HTTP请求`:拆解URL,认识请求行、请求头和请求体,区分路径参数与查询参数,并为业务操作选择HTTP方法; +3. `5_3_HTTP响应与状态码`:认识响应结构和状态码分类,为查询、创建、删除、认证、权限和异常等结果选择准确状态码; +4. `5_4_JSON数据交换`:认识JSON语法和数据类型,完成Python对象与JSON文本的转换,理解请求体和响应体中的JSON; +5. `5_5_RESTful_API设计`:理解REST资源化思想,组合资源路径、HTTP方法和状态码,设计图书管理API; +6. `5_6_HTTP接口调试与综合实践`:使用Python标准库启动仅限本机访问的临时HTTP服务,自动发送GET和POST请求,并检查成功与失败响应。 + +阶段示例不访问公网、不连接数据库,也不模拟生产部署。完成本阶段后,应能解释一次HTTP请求和响应,读懂URL、方法、状态码和JSON,并能在进入FastAPI之前独立设计和调试一组基础RESTful API。 ### 第六阶段:FastAPI @@ -206,12 +211,18 @@ Python/ │ ├── 4_4_SQLAlchemy关系映射与工程实践/ │ └── 4_5_数据库综合项目/ ├── 05_web基础/ +│ ├── 5_1_Web运行原理与客户端服务器/ +│ ├── 5_2_URL与HTTP请求/ +│ ├── 5_3_HTTP响应与状态码/ +│ ├── 5_4_JSON数据交换/ +│ ├── 5_5_RESTful_API设计/ +│ └── 5_6_HTTP接口调试与综合实践/ ├── 06_fastapi/ ├── 07_django/ └── 08_工程化与部署/ ``` -这只是目录规划示意,不会提前创建全部目录。正式开始某一课时,才会创建对应的阶段目录、课程目录、中文讲义、示例代码和练习;完成当前课程并确认继续后,再创建下一课。 +这只是目录规划示意,默认不会提前创建全部目录。通常在正式开始某一课时创建对应内容;如果学习者明确要求批量准备某一阶段,则可在确认课次规划后一次创建该阶段课程。 ## 建议环境