(十五)运行本地服务并发出请求
1. 在项目环境准备客户端与练习服务
进入配套的 network_project。目录中的 labserver 提供固定书目与错误响应,netreading 稍后完成并发导入,bookdata 继续使用第五章的记录和 SQLite 实现。
# 项目文件:network_project/pyproject.toml
# HTTPX 是运行依赖;pytest 继续放在开发组,src 下的包由构建工具发现。
[build-system]
requires = ["setuptools>=80,<83"]
build-backend = "setuptools.build_meta"
[project]
name = "reading-network-notes"
version = "0.1.0"
requires-python = ">=3.12,<3.13"
dependencies = ["httpx>=0.28,<1"]
[project.scripts]
reading-fetch = "netreading.cli:main"
[dependency-groups]
dev = ["pytest>=9.1,<10"]
[tool.setuptools.packages.find]
where = ["src"]
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra --import-mode=importlib"# 在 network_project 根目录使用 Python 3.12,同步配套的锁定记录。
uv sync --locked
# 手动查看服务时启动它,终端显示地址;Ctrl+C 结束。
uv run python -m labserver --port 8765
另一个终端或浏览器可以访问显示的地址。短例使用 with running_server() as server 临时启动相同服务,由系统选择空闲端口,结束后关闭,因此不用每次手动守着一个固定端口。
pip 路线在 Python 3.12 虚拟环境中执行 python -m pip install -e . 与 python -m pip install -r requirements-dev.txt,再用项目解释器运行相同代码。配套 HTTP 短例可在本目录执行 uv run python ../demos/http_get.py。
2. 先用标准库取得一条 JSON 记录
# 项目文件:demos/http_get.py
# 请求返回的正文先是 bytes;按响应声明的编码还原文本,再解释为 JSON。
import json
from urllib.request import Request, urlopen
from labserver import running_server
if __name__ == "__main__":
with running_server() as server:
request = Request(
server.base_url + "/api/books/1", headers={"Accept": "application/json"}
)
with urlopen(request, timeout=1) as response:
text = response.read().decode(
response.headers.get_content_charset() or "utf-8"
)
record = json.loads(text)
print(response.status, response.headers.get_content_type())
print(record["title"], record["finished"])输出 200 application/json 与 Python False。Request 保存目标、头部与可选正文;urlopen 执行阻塞请求,read 取得正文。这里把响应放进 with,读取后关闭资源,后续要重复请求时也清楚每次在哪里结束。urllib 请求接口
练习服务使用 ThreadingHTTPServer 接收本机请求,固定生成响应;/api/books/1?delay=0.1 会在发送响应前等待 0.1 秒。它让客户端可以观察等待重叠,服务器不会因为一个请求等待就停止接收其他请求。完整实现已分成书目、线程安全状态、路由与启动文件,见配套的 src/labserver/。标准库 HTTP 服务
3. 查询参数与 JSON 请求体分别传入
# 项目文件:demos/http_query_post.py
# GET 的查询放在 URL,POST 的 JSON 放在请求体;服务器分别解析这两种位置。
import json
from urllib.parse import urlencode
from urllib.request import Request, urlopen
from labserver import running_server
if __name__ == "__main__":
with running_server() as server:
query = urlencode({"tag": ["基础", "语法"]}, doseq=True)
with urlopen(
server.base_url + "/api/echo?" + query + "#client-only", timeout=1
) as response:
print(json.load(response)["query"])
data = json.dumps({"minutes": 35}).encode("utf-8")
request = Request(
server.base_url + "/api/echo",
data=data,
method="POST",
headers={"Content-Type": "application/json"},
)
with urlopen(request, timeout=1) as response:
print(json.load(response))
print("服务收到片段", any("#" in path for path in server.state.paths))输出查询中的两个 tag、{'method': 'POST', 'body': {'minutes': 35}},最后为 服务收到片段 False。json.load 也能从这种提供 read 的二进制响应中解析 JSON,按 JSON 的字节编码规则处理正文。