Python 标准库 Day 6:网络与测试
1. 问题引入
继续博客项目。这些场景需要今天的工具:
- 抓取 RSS 源、调第三方 API(天气、统计服务)——怎么发 HTTP 请求?
- 想给同事临时分享
output/目录里的报告——能不能一行命令起个文件服务器? - 需要直接和 SMTP / Redis / 自定义协议通信——比 HTTP 更底层
- 写好的函数怎么验证它真的对?怎么防止以后改代码改坏了?
今天讲五个模块,前四个是网络,最后一个是测试。彼此独立,不强行串联。
2. 主题讲解
2.1 urllib —— 标准库 HTTP 客户端
模块结构
urllib 是个"包",包含几个子模块:
urllib.request # 发 HTTP 请求
urllib.parse # URL 解析、编码
urllib.error # 异常类
基本 GET 请求
from urllib.request import urlopen
with urlopen("https://api.github.com") as response:
data = response.read() # bytes
text = data.decode("utf-8") # str
status = response.status # 200
headers = response.headers # 类 dict
urlopen 返回的是个类文件对象——支持 read()、readline() 这些操作。这种"网络资源像文件一样读"的设计是 Unix 哲学的体现。
POST 请求 + 自定义 Header
from urllib.request import urlopen, Request
import json
data = json.dumps({"name": "Alice"}).encode("utf-8") # 必须是 bytes
req = Request(
"https://api.example.com/users",
data=data,
headers={"Content-Type": "application/json"},
method="POST",
)
with urlopen(req) as response:
result = json.loads(response.read())
URL 编码
from urllib.parse import urlencode, quote
params = {"q": "python 教程", "page": 2}
query = urlencode(params) # 'q=python+%E6%95%99%E7%A8%8B&page=2'
url = f"https://example.com/search?{query}"
quote("hello world") # 'hello%20world'
urlencode 和 quote 帮你处理中文、空格、特殊字符。永远不要自己拼 URL 不转义——会出 bug。
异常处理
from urllib.request import urlopen
from urllib.error import HTTPError, URLError
try:
with urlopen("https://example.com/404", timeout=10) as r:
data = r.read()
except HTTPError as e:
# 服务器返回了 4xx/5xx
print(f"HTTP {e.code}: {e.reason}")
except URLError as e:
# 连接失败、DNS 错误、超时等
print(f"网络错误: {e.reason}")
timeout 一定要加,否则可能永久阻塞。
urllib vs requests
requests 是第三方库,API 远比 urllib 友好:
req = Request(url, data=json.dumps(d).encode(), headers={"Content-Type": "application/json"})
with urlopen(req) as r:
result = json.loads(r.read())
result = requests.post(url, json=d).json()
为什么还要会 urllib:
- 标准库自带,写小工具不想加依赖时用
- Docker 镜像精简、嵌入式 Python 等无 pip 环境
- 读老代码会遇到
新代码生产环境推荐 requests 或 httpx。今天学 urllib 是为了理解原理,不是推荐你日常用。
2.2 http.client —— 更底层的 HTTP 客户端
urllib.request 内部就是用 http.client 实现的。直接用 http.client 的场景:
- 需要复用同一个 TCP 连接发多个请求(性能优化)
- 需要精细控制请求过程
- 实现 HTTP 库(你自己造轮子时)
from http.client import HTTPSConnection
conn = HTTPSConnection("api.github.com", timeout=10)
conn.request("GET", "/")
response = conn.getresponse()
print(response.status) # 200
print(response.read().decode())
conn.close()
实际工作中 95% 用 **urllib** 或 **requests** 就够——http.client 知道存在即可。
2.3 http.server —— 一行起本地服务器
命令行用法(最常用)
cd /path/to/files
python -m http.server 8000
打开浏览器访问 http://localhost:8000——当前目录文件全列出来。
典型场景:
- 临时给同事分享文件
- 测试前端能不能加载本地资源
- 调试 webhook(先用本地 server 看请求长啥样)
- 演示静态网站
代码方式(自定义路由)
from http.server import BaseHTTPRequestHandler, HTTPServer
class MyHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/":
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.end_headers()
self.wfile.write("<h1>Hello!</h1>".encode("utf-8"))
elif self.path == "/api/time":
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(b'{"time": "now"}')
else:
self.send_error(404)
server = HTTPServer(("localhost", 8000), MyHandler)
server.serve_forever()
**http.server**** 只用于开发、演示、内部工具——绝不能上生产**。它单线程、没安全特性、性能极差。生产用 nginx / uvicorn / gunicorn。
2.4 socket —— TCP/UDP 底层网络编程
socket 是网络编程的最底层——HTTP、SMTP、Redis 协议、WebSocket 全部建立在它之上。99% 的应用代码用不到它,但知道它长什么样,看别人的网络代码会轻松很多。
TCP 客户端示例
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("example.com", 80))
s.sendall(b"GET / HTTP/1.0\r\nHost: example.com\r\n\r\n")
data = b""
while True:
chunk = s.recv(4096)
if not chunk:
break
data += chunk
s.close()
print(data.decode("utf-8")[:200])
这就是手撸了一个最简陋的 HTTP 客户端——拼字符串发出去,接收返回。urlopen 内部本质就是这套,外加协议解析。
TCP 服务器示例
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(("localhost", 9999))
s.listen()
print("等待连接...")
conn, addr = s.accept() # 阻塞等待
print(f"客户端 {addr} 连进来了")
data = conn.recv(1024)
print(f"收到: {data.decode()}")
conn.sendall(b"Hello back!")
conn.close()
什么时候真的需要 socket
- 实现自定义协议(IoT、游戏、私有 RPC)
- 直接和 Redis / Memcached 等对话(虽然有现成库)
- 写底层网络工具
绝大多数应用层场景用 urllib / requests / 框架级 API 就够了。今天只要知道 socket 是网络的底层基石就行。
2.5 unittest —— 单元测试
为什么写测试
- 验证代码"现在是对的"
- 以后改代码时,测试能告诉你有没有改坏——这才是测试真正的价值
- 重构时心里有底
- 文档作用——好的测试展示了函数怎么用
基本用法
import unittest
from my_module import add, divide
class TestMath(unittest.TestCase):
def test_add(self):
self.assertEqual(add(2, 3), 5)
self.assertEqual(add(-1, 1), 0)
def test_divide(self):
self.assertEqual(divide(10, 2), 5)
self.assertAlmostEqual(divide(1, 3), 0.333, places=2)
def test_divide_by_zero(self):
# 测试"应该抛异常"
with self.assertRaises(ZeroDivisionError):
divide(10, 0)
if __name__ == "__main__":
unittest.main()
跑测试:
python -m unittest test_my_module.py
python -m unittest discover # 自动发现当前目录所有 test_*.py
常用断言
| 方法 | 含义 |
|---|---|
assertEqual(a, b) | a == b |
assertNotEqual(a, b) | a != b |
assertTrue(x)/ assertFalse(x) | 布尔判断 |
assertIsNone(x)/ assertIsNotNone(x) | None 判断 |
assertIn(a, b) | a in b |
assertAlmostEqual(a, b, places=7) | 浮点近似相等 |
assertRaises(Exc) | 在 with 块里期望抛异常 |
assertGreater(a, b) | a > b |
setUp / tearDown —— 测试前后的钩子
class TestDatabase(unittest.TestCase):
def setUp(self):
"""每个 test_ 方法运行前调用。"""
self.conn = create_test_db()
def tearDown(self):
"""每个 test_ 方法运行后调用。"""
self.conn.close()
def test_insert(self):
self.conn.insert(...)
...
def test_query(self):
self.conn.query(...)
...
每个测试都拿到干净的 self.conn,互不干扰。
一个完整测试示例
def humanize(seconds):
if seconds < 60:
return f"{seconds}秒前"
elif seconds < 3600:
return f"{seconds // 60}分钟前"
else:
return f"{seconds // 3600}小时前"
import unittest
from my_module import humanize
class TestHumanize(unittest.TestCase):
def test_seconds(self):
self.assertEqual(humanize(30), "30秒前")
def test_minutes(self):
self.assertEqual(humanize(120), "2分钟前")
def test_hours(self):
self.assertEqual(humanize(7200), "2小时前")
def test_boundary(self):
# 边界值测试
self.assertEqual(humanize(59), "59秒前")
self.assertEqual(humanize(60), "1分钟前")
self.assertEqual(humanize(3599), "59分钟前")
self.assertEqual(humanize(3600), "1小时前")
def test_zero(self):
self.assertEqual(humanize(0), "0秒前")
if __name__ == "__main__":
unittest.main()
unittest vs pytest
pytest 是第三方,但事实上是 Python 测试圈的标准:
class TestMath(unittest.TestCase):
def test_add(self):
self.assertEqual(add(2, 3), 5)
def test_add():
assert add(2, 3) == 5
pytest 更简洁,自动发现测试,错误提示更友好,有强大的 fixture 系统。新项目推荐 pytest,但 unittest 是标准库自带、无依赖,写小工具时仍有用。
测试覆盖率
写完测试要看覆盖率——多少代码被测到了:
pip install coverage
coverage run -m unittest discover
coverage report
工业界一般要求 60-80%+ 覆盖率。
3. Maybe Useful 旁注
旁注 1:HTTPS 证书问题:用 urllib 访问自签名证书的服务会报 SSL 错误。临时关闭验证(仅开发用):
import ssl
ctx = ssl._create_unverified_context()
urlopen(url, context=ctx)
生产环境绝对不能这样——会被中间人攻击。
旁注 2:**python -m http.server**** 的妙用**:远程服务器调试时,python -m http.server 8000 起来后用 curl 或浏览器访问能立刻确认 "端口能不能通"。这比 telnet 直观。
旁注 3:socket 编程的字节序问题:跨平台传二进制数据时要处理 big-endian / little-endian。struct 模块用来打包/解包二进制。今天用不到,但写自定义协议会撞见。
旁注 4:测试金字塔:单元测试(多)> 集成测试(中)> 端到端测试(少)。今天讲的是单元测试——快、独立、不依赖外部。集成测试测多个模块协作,端到端测试测整个用户流程。
旁注 5:mock 的概念:测试中需要"假装"某个依赖——比如测一个调 API 的函数,你不想真的发请求。unittest.mock 模块解决这个,但今天先不展开,看到代码里 @patch(...) 知道是 mock 就行。
旁注 6:面试高频题:
- "GET 和 POST 区别?" → 必答
- "怎么测试一个函数?" → 必答
- "什么是 mock?什么时候用?" → 加分
- "TCP 和 UDP 区别?" → 加分(socket 相关)
- "为什么要写测试?" → 必答
旁注 7:**http.server**** 的安全坑**:默认会列出当前目录的所有文件——不要在含敏感数据的目录跑。.git/``.env 都会被暴露。
4. 代码实践
四个独立的小例子,对应今天每个主题。不串成一个综合项目。
4.1 用 urllib 抓 GitHub API
import json
from urllib.request import urlopen, Request
from urllib.error import HTTPError, URLError
def get_repo_info(owner: str, repo: str) -> dict | None:
url = f"https://api.github.com/repos/{owner}/{repo}"
req = Request(url, headers={"User-Agent": "Python-urllib"})
try:
with urlopen(req, timeout=10) as response:
return json.loads(response.read())
except HTTPError as e:
print(f"HTTP {e.code}: {e.reason}")
except URLError as e:
print(f"网络错误: {e.reason}")
return None
if __name__ == "__main__":
info = get_repo_info("python", "cpython")
if info:
print(f"{info['stargazers_count']}")
print(f"{info['description']}")
4.2 起一个简单文件服务器
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(b'{"status": "ok"}')
if __name__ == "__main__":
HTTPServer(("localhost", 8000), Handler).serve_forever()
4.3 socket 简易 echo 客户端
import socket
def http_get_raw(host: str, path: str = "/") -> bytes:
"""手撸 HTTP GET,纯 socket。"""
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)
s.connect((host, 80))
request = f"GET {path} HTTP/1.0\r\nHost: {host}\r\n\r\n".encode()
s.sendall(request)
data = b""
while True:
chunk = s.recv(4096)
if not chunk:
break
data += chunk
s.close()
return data
if __name__ == "__main__":
response = http_get_raw("example.com")
print(response[:200].decode("utf-8", errors="ignore"))
4.4 给一个函数写测试
def humanize_size(bytes_count: int) -> str:
"""把字节数转成 'X KB' / 'X MB' 字符串。"""
if bytes_count < 1024:
return f"{bytes_count} B"
elif bytes_count < 1024 ** 2:
return f"{bytes_count / 1024:.1f} KB"
elif bytes_count < 1024 ** 3:
return f"{bytes_count / (1024**2):.1f} MB"
else:
return f"{bytes_count / (1024**3):.1f} GB"
import unittest
from my_module import humanize_size
class TestHumanizeSize(unittest.TestCase):
def test_bytes(self):
self.assertEqual(humanize_size(0), "0 B")
self.assertEqual(humanize_size(512), "512 B")
self.assertEqual(humanize_size(1023), "1023 B")
def test_kilobytes(self):
self.assertEqual(humanize_size(1024), "1.0 KB")
self.assertEqual(humanize_size(1536), "1.5 KB")
def test_megabytes(self):
self.assertEqual(humanize_size(1024 * 1024), "1.0 MB")
self.assertEqual(humanize_size(5 * 1024 * 1024), "5.0 MB")
def test_gigabytes(self):
self.assertEqual(humanize_size(1024 ** 3), "1.0 GB")
if __name__ == "__main__":
unittest.main()
跑测试:
$ python -m unittest test_my_module.py
....
----------------------------------------------------------------------
Ran 4 tests in 0.001s
OK
5. 练习题
理论题
urllib和requests各自适合什么场景?http.server为什么不能用于生产?- socket 在网络协议栈里处于什么位置?哪些日常使用的库底层是 socket?
- 单元测试和集成测试的区别?
setUp和tearDown的作用是什么?
代码实操题
题目 A:写一个 download_json(url) 函数,用 urllib 发 GET 请求并返回解析后的 JSON。处理超时、HTTP 错误、JSON 解析失败三种异常。
提示:try 三层或者捕获多种异常类型。
题目 B:用 http.server 写一个返回当前时间的 HTTP 服务,访问 / 返回 JSON {"time": "2026-04-29T..."}。
提示:在 do_GET 里用 datetime.now(tz=timezone.utc).isoformat() 生成时间。
题目 C:给 Day 4 写过的 pv_level 函数(PV 等级映射)写至少 5 个测试用例,覆盖各种边界。
提示:测最小值、最大值、刚好在分界点上、分界点 +1 / -1。
思考题
你给项目写了 100 个测试,每次跑要 30 秒。改一行代码就要等半分钟测试通过。怎么优化测试速度?哪些测试可以并行?哪些必须串行?测试连数据库的慢测试怎么处理?
6. 当天总结
今天学了什么:
四个网络模块 + 一个测试模块:
urllib—— 标准库 HTTP 客户端,不想加依赖时用http.client—— 更底层,95% 用不到http.server—— 一行起本地服务器(开发/演示用)socket—— 最底层,理解网络通信的基石unittest—— 标准库测试框架
最重要的概念:
- urllib 知道就行,生产用 requests/httpx
**python -m http.server**** 是个高频 trick**,临时分享文件、调试都能用- socket 是基石,但日常用不到——理解就行
- 写测试比写代码更重要——尤其是改代码后能立刻知道有没有改坏
- 断言 + setUp/tearDown 就能覆盖 80% 测试需求
需要反复练习:
- urllib 的 GET / POST 标准写法
- assertEqual / assertRaises / assertAlmostEqual 这几个常用断言
- 给自己写的函数加测试