RSS
菜单
全部文章快讯开发科技深度热点

网络请求与 Web 基础(Python 从精通到入门 · 17)

内容摘要

从 urllib 到 requests,从发一个 GET 到起一个 Web 服务;这篇把「客户端请求」和「服务端响应」两头都讲清楚,并补上 HTTP 与 REST 的常识。

从 urllib 到 requests,从发一个 GET 到起一个 Web 服务;这篇把「客户端请求」和「服务端响应」两头都讲清楚,并补上 HTTP 与 REST 的常识。

你将学到

  • urllib 的最小用法,以及为什么实际都用 requests
  • requests 的 GET/POST、参数、headers、超时、重试、session 复用
  • 处理 JSON 响应、状态码与异常
  • 解析 HTML 的思路:正则的边界与 BeautifulSoup 简介
  • 用标准库 http.server 或 Flask 写一个最小 Web 服务
  • HTTP 状态码与 REST 概念扫盲

前置知识

需要 JSON 处理与异常基础,见 上一篇:数据处理 json/csv/dataclasses。

从 urllib 说起

标准库 urllib.request 能发请求,不用装任何东西,但 API 偏啰嗦。

# 标准库版:能跑,但样板代码多
import urllib.request, json

with urllib.request.urlopen("", timeout=5) as resp:
    print(resp.status)                                  # 输出: 200
    print(json.loads(resp.read().decode("utf-8"))["args"])   # 输出: {'name': 'python'}

要发 POST、带表单、加 header,还得手动构造 Request 和编码 body,很快就烦了。这就是 requests 存在的原因,先 pip install requests。

requests:GET 与参数

import requests

# 参数交给 params,requests 负责编码和拼接
resp = requests.get(
    "",
    params={"q": "python 教程", "page": 2},
    timeout=5,                    # ⚠️ 永远设超时,否则可能永久挂起
)
print(resp.status_code)          # 输出: 200
print(resp.url)                  # 输出: 
print(resp.json()["args"])       # 输出: {'q': 'python 教程', 'page': '2'}

别自己拼 URL 字符串——中文、空格、& 都会出问题,params 帮你转义。

headers 与 POST

import requests

headers = {"User-Agent": "Mozilla/5.0 (compatible; MyBot/1.0)", "Accept": "application/json"}

# 表单格式(application/x-www-form-urlencoded):用 data=
r = requests.post("",
                  data={"username": "alice", "password": "secret"},
                  headers=headers, timeout=5)
print(r.json()["form"])     # 输出: {'username': 'alice', 'password': 'secret'}

# JSON 格式:用 json=,自动设置 Content-Type: application/json
r = requests.post("",
                  json={"title": "hi", "tags": ["a", "b"]}, headers=headers, timeout=5)
print(r.json()["json"])     # 输出: {'title': 'hi', 'tags': ['a', 'b']}

注意区分:data= 是表单,json= 是 JSON body。用错了服务端会返回 400 或解析失败。

超时、重试与 session

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

# ✅ 推荐做法:session + 自动重试
session = requests.Session()
retry = Retry(
    total=3,                                 # 最多重试 3 次
    backoff_factor=0.5,                      # 退避:0.5s, 1s, 2s
    status_forcelist=[429, 500, 502, 503, 504],   # 这些状态码才重试
    allowed_methods=["GET", "POST"],
)
session.mount("https://", HTTPAdapter(max_retries=retry))

# session 自动复用 TCP 连接、保持 cookies,比每次 requests.get 快
resp = session.get("", timeout=(3.05, 10))  # (连接超时, 读取超时)
print(resp.status_code)   # 输出: 200

timeout 传单值表示连接和读取都用这个值;传元组 (connect, read) 更精确。重试要小心:只对幂等的 GET 重试,POST 乱重试可能重复下单。

状态码与异常

import requests
from requests.exceptions import (
    HTTPError, Timeout, ConnectionError, RequestException,
)

try:
    resp = requests.get("", timeout=5)
    resp.raise_for_status()          # 4xx / 5xx 抛 HTTPError
    data = resp.json()
except HTTPError as e:
    print("HTTP 错误:", e.response.status_code)   # 输出: HTTP 错误: 404
except Timeout:
    print("请求超时")
except ConnectionError:
    print("连接失败")
except RequestException as e:
    print("其他请求异常:", e)

raise_for_status() 是把「HTTP 状态码错误」转成「异常」的关键一步,否则 resp.json() 可能在错误页面上抛更莫名其妙的错。

处理 JSON 响应:

resp = requests.get("", timeout=5)
resp.encoding = "utf-8"          # 服务端没声明对编码时可手动纠正
try:
    print(resp.json()["slideshow"]["title"])   # 输出: Sample Slide Show
except ValueError:                             # Content-Type 不是 JSON 时抛 ValueError
    print("响应不是合法 JSON,原始内容:", resp.text[:200])

简单 HTML 解析

拿到 HTML 后想提取内容,两条路:

import re
import requests

html = requests.get("", timeout=5).text

# ✅ 正则适合结构规整、目标单一的场景(如提取 <title>)
m = re.search(r"<title>(.*?)</title>", html, re.S)
print(m.group(1) if m else "无标题")

# ❌ 别用正则解析嵌套 HTML —— 结构一变就崩,属于经典反模式
# 复杂提取请用解析库
from bs4 import BeautifulSoup

soup = BeautifulSoup(html, "lxml")           # pip install beautifulsoup4 lxml,lxml 比内置 html.parser 快
print(soup.title.string)                     # 输出: Example Domain

for a in soup.find_all("a"):                 # 按标签查找并取属性
    print(a.get("href"), "->", a.text)

原则:正则提取单点、结构库解析整体。真要抓大站还要考虑 robots.txt、频率限制和法律问题,别乱来。

最小 Web 服务:http.server

标准库自带一个 HTTP 服务器,几行就能起一个接口。

# server.py —— 标准库实现,零依赖
from http.server import BaseHTTPRequestHandler, HTTPServer
import json

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/health":
            body = json.dumps({"status": "ok"}).encode("utf-8")
            self.send_response(200)                     # 状态码
            self.send_header("Content-Type", "application/json; charset=utf-8")
            self.end_headers()
            self.wfile.write(body)
        else:
            self.send_response(404); self.end_headers(); self.wfile.write(b"not found")

    def log_message(self, *args):
        pass                                            # 静音默认日志

if __name__ == "__main__":
    HTTPServer(("127.0.0.1", 8000), Handler).serve_forever()
python server.py          # 另一个终端执行:
curl       # 输出: {"status": "ok"}

http.server 适合本地调试和小工具,不要用于生产——它是单线程的,没有并发、安全和性能加固。

最小 Web 服务:Flask

先 pip install flask。

# app.py —— Flask hello world,含一个 JSON 接口
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.get("/")
def index():
    return "Hello, Web!"

@app.get("/api/greet")
def greet():
    name = request.args.get("name", "world")       # 取查询参数
    return jsonify(message=f"Hello, {name}!")

@app.post("/api/echo")
def echo():
    payload = request.get_json(silent=True) or {}  # 解析 JSON body
    return jsonify(received=payload), 201          # 返回 201 Created

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=5000, debug=True)
python app.py   # 另开终端:
curl ""          # 输出: {"message":"Hello, py!"}
curl -X POST  -H "Content-Type: application/json" -d '{"a": 1}'
# 输出: {"received":{"a":1}}

连起来看:你的客户端用 requests 发 JSON,服务端用 Flask 收 JSON——这就是绝大多数前后端交互的最小单元。

HTTP 状态码与 REST 扫盲

状态码分段记忆:

  • 1xx 信息,基本见不到
  • 2xx 成功:200 OK、201 Created、204 No Content
  • 3xx 重定向:301 永久、302 临时、304 未修改(配合缓存)
  • 4xx 客户端错:400 参数错、401 未认证、403 无权限、404 找不到、429 限流
  • 5xx 服务端错:500 内部错误、502 网关错、503 不可用、504 网关超时

REST 的核心约定(务实版,不必教条):

  • 用名词复数表示资源:/users、/orders/42
  • 用HTTP 方法表达动作:GET 查、POST 建、PUT 全量改、PATCH 局部改、DELETE 删
  • 用状态码表达结果,而不是在 body 里返回 {"code": 200}
  • 无状态:每个请求自带认证信息,服务端不依赖上次请求的会话
GET    /users          列出用户
POST   /users          新建用户
GET    /users/42       查看某个用户
PUT    /users/42       整体更新
DELETE /users/42       删除

常见坑

# ❌ 不设 timeout,网络卡住时程序永远挂着
requests.get("")

# ✅ 显式设置超时
requests.get("", timeout=5)

# ❌ 把查询参数直接拼进 URL,中文/空格/& 全乱
requests.get(" world&x=1")

# ✅ 用 params
requests.get("", params={"q": "hello world", "x": 1})
# ❌ 不看状态码就 .json(),404 页面解析失败抛奇怪的错
data = requests.get(url).json()

# ✅ 先 raise_for_status,再解析
resp = requests.get(url, timeout=5)
resp.raise_for_status()
data = resp.json()
# ❌ 循环里每次 requests.get,反复建连接、还容易被限流
for u in urls:
    requests.get(u, timeout=5)

# ✅ 用 Session 复用连接,或加 Retry 处理偶发失败
sess = requests.Session()
for u in urls:
    sess.get(u, timeout=5)

# ❌ 把 http.server 拿去跑生产接口
HTTPServer(("0.0.0.0", 80), Handler).serve_forever()

# ✅ 生产用成熟框架 + WSGI 服务器:Flask + gunicorn/waitress,或 FastAPI + uvicorn

小结

  • requests 是事实标准;params 管查询参数,json= 发 JSON,data= 发表单。
  • timeout 必设;重试只对幂等请求,退避用 Retry;循环请求用 Session 复用连接。
  • 先 raise_for_status() 再解析;把 RequestsException 分门别类捕获。
  • HTML 提取:正则适合单点,整体解析用 BeautifulSoup/lxml。
  • 起服务:http.server 只用于本地调试,Flask 是入门 Web 的顺手起点。
  • 记住状态码分段与 REST 的名词 + 方法约定,接口设计就八九不离十。

延伸阅读

  • 官方/项目文档:requests、http.server、Flask、BeautifulSoup
  • 想要现代异步 Web,看 08-asyncio 异步编程全解
  • 下一篇进入上手篇 18-面向对象编程

上一篇:数据处理 json/csv/dataclasses · 下一篇:面向对象编程

— 全文完 —回到顶部 ↑
下载推广海报

文章推广海报

《网络请求与 Web 基础(Python 从精通到入门 · 17)》完整推广海报
DISCUSSION

文章回复

0 条公开回复
未登录回复需要审核后公开
还没有回复,欢迎参与讨论。