本篇是《Python 从入门到初级》第 15 / 20 篇。示例使用 Python 3.12 兼容语法,完整目录结构与数据见配套源码包。
Python 不只处理本地文件,也能与其他系统通信。本篇在本机运行一个教学接口,练习 GET 读取商品、POST 发送 JSON,以及将响应转回列表和字典。

1. 请求与响应先看四件事
请求包含方法、URL、请求头和可能的正文;响应包含状态码、响应头和正文。GET 通常用于读取,POST 常用于提交数据,真实业务中的写入行为必须按接口约定判断。
收到 HTTP 200 也不代表正文一定符合预期,仍需检查 JSON 能否解析、字段是否存在及含义是否正确。
本例只连接 127.0.0.1:18084,无需外部账号。服务是教学程序,不作为生产 Web 服务使用。
2. 先启动配套教学服务
将源码包中的 api_demo.py 保留在示例目录,在终端 A 运行:
python api_demo.py
服务提供两个端点:GET /api/products 返回商品列表;POST /api/echo 回显 title,明确返回 stored=false,不会写数据库。
服务端完整代码如下,初次学习可以先运行,把注意力放在后面的客户端:
import json
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
class Handler(BaseHTTPRequestHandler):
def send_json(self, status, payload):
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
try:
self.wfile.write(body)
except ConnectionError:
pass
def do_GET(self):
if self.path != "/api/products":
self.send_json(404, {"error": "not found"})
return
self.send_json(200, {"items": [
{"name": "键盘", "price_cents": 19900},
{"name": "鼠标", "price_cents": 5900},
]})
def do_POST(self):
if self.path != "/api/echo":
self.send_json(404, {"error": "not found"})
return
try:
size = int(self.headers.get("Content-Length", "0"))
if not 0 < size <= 4096:
raise ValueError("invalid body size")
payload = json.loads(self.rfile.read(size).decode("utf-8"))
if not isinstance(payload, dict) or not isinstance(payload.get("title"), str):
raise ValueError("title must be text")
except (ValueError, UnicodeDecodeError):
self.send_json(400, {"error": "invalid JSON or title"})
return
self.send_json(200, {"received": payload["title"], "stored": False})
if __name__ == "__main__":
ThreadingHTTPServer(("127.0.0.1", 18084), Handler).serve_forever()
3. 完整客户端:读取和提交 JSON
终端 B 运行 python lesson15.py:
import json
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
def main(base_url="http://127.0.0.1:18084"):
try:
with urlopen(base_url + "/api/products", timeout=3) as response:
payload = json.loads(response.read(65536).decode("utf-8"))
for item in payload["items"]:
print(f"{item['name']}:{item['price_cents'] / 100:.2f} 元")
data = json.dumps({"title": "学习 Python 调用接口"}, ensure_ascii=False).encode("utf-8")
request = Request(base_url + "/api/echo", data=data,
headers={"Content-Type": "application/json"}, method="POST")
with urlopen(request, timeout=3) as response:
result = json.loads(response.read(65536).decode("utf-8"))
print("POST 回显:", result["received"])
print("写入数据库了吗:", result["stored"])
except HTTPError as exc:
print("HTTP 请求失败:", exc.code)
exc.close()
return 1
except (URLError, TimeoutError) as exc:
print("连接或等待失败:", type(exc).__name__)
return 1
except (ValueError, KeyError, TypeError) as exc:
print("响应结构不符合教学约定:", type(exc).__name__)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
预期显示键盘 199.00 元、鼠标 59.00 元,POST 回显学习标题,stored 为 False。
response.read() 得到字节;decode 按约定变成文字;json.loads 再把 JSON 文字解析成 Python 数据结构。请求方向则相反:字典先 json.dumps,再 encode 成字节提交。
4. 请求头和超时在做什么
POST 设置 Content-Type: application/json,告诉接收端正文采用什么格式。不同接口也可能需要 Authorization 等身份信息,但本地示例没有这一需求,不要给它随意加入真实令牌。
timeout 限制相关阻塞操作的等待,不是严格覆盖 DNS、重定向和完整处理的总时限。示例最多读取 65536 字节,适合小型教学响应;更大的 API 需要分页或其他受控读取方式。
urllib 的 URL 打开、代理和重定向行为见 官方文档。访问 HTTPS 时保持默认的证书验证,不要为了消除证书报错直接跳过校验。
5. 报错时按层看
服务器没启动或端口不对,通常是连接阶段失败。路径不对,可能收到 404;POST 内容不符合约定,教学服务返回 400;响应不是约定的 JSON,则可能在解析或访问字段时失败。
客户端先捕获 HTTPError,再处理 URLError,因为前者属于后者的子类。保留状态码有助于区分“对方返回错误”和“没拿到有效响应”。
这些异常并不自动证明具体业务根因,仍要结合 URL、服务日志和接口文档。站内 HTTP 巡检脚本文章 提供了更多失败路径的练习。
6. 练习与收尾
把 title 改成自己的学习目标,预期回显同步变化;把请求路径改成不存在的地址,观察 HTTP 状态错误;停止终端 A 的服务后再运行客户端,观察连接失败。
实验结束时在终端 A 按 Ctrl+C。真实接口中的 POST 可能创建资源,不应像本地回显一样随意重复发送;是否可重试,需要按接口的幂等约定决定。