HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于
  • Python 新手入门教程

    • Python新手入门教程 - 零基础学Python,8章+HTTP服务实战 | HiHuo
    • Python简介与环境搭建 - 安装配置Python开发环境 | HiHuo
    • Python基础语法 - 缩进、变量、注释、输入输出 | HiHuo
    • Python数据类型详解 - 字符串、列表、字典、元组 | HiHuo
    • Python流程控制 - if条件判断、for/while循环 | HiHuo
    • Python函数与模块 - 定义函数、参数传递、模块导入 | HiHuo
    • Python面向对象编程 - 类、对象、继承、多态 | HiHuo
    • Python文件与异常处理 - 读写文件、try/except异常捕获 | HiHuo
    • Python HTTP服务项目实战 - 构建待办事项API服务 | HiHuo

HTTP 服务项目实战

本章将综合运用前面学习的知识,构建一个完整的 HTTP API 服务。

项目介绍

我们将构建一个待办事项 API 服务,支持:

  • 创建、读取、更新、删除待办事项(CRUD)
  • JSON 数据交互
  • 持久化存储
  • 错误处理

项目结构

todo_api/
├── app.py           # 主应用入口
├── models.py        # 数据模型
├── storage.py       # 数据存储
├── handlers.py      # 请求处理
├── data/
│   └── todos.json   # 数据文件
└── requirements.txt # 依赖

第一步:使用标准库创建简单 HTTP 服务

Python 标准库自带 HTTP 服务器,无需安装任何依赖。

最简单的 HTTP 服务器

# simple_server.py
from http.server import HTTPServer, BaseHTTPRequestHandler

class SimpleHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        # 设置响应状态码
        self.send_response(200)
        # 设置响应头
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.end_headers()
        # 发送响应体
        self.wfile.write("Hello, World! 你好世界!".encode("utf-8"))

# 启动服务器
server = HTTPServer(("localhost", 8000), SimpleHandler)
print("服务器启动: http://localhost:8000")
server.serve_forever()

运行后访问 http://localhost:8000 查看效果。

处理不同路径

# path_server.py
from http.server import HTTPServer, BaseHTTPRequestHandler
import json

class PathHandler(BaseHTTPRequestHandler):

    def do_GET(self):
        if self.path == "/":
            self.send_json({"message": "欢迎访问API"})
        elif self.path == "/hello":
            self.send_json({"message": "Hello, World!"})
        elif self.path == "/time":
            from datetime import datetime
            self.send_json({"time": datetime.now().isoformat()})
        else:
            self.send_error_json(404, "路径不存在")

    def send_json(self, data, status=200):
        self.send_response(status)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.end_headers()
        self.wfile.write(json.dumps(data, ensure_ascii=False).encode("utf-8"))

    def send_error_json(self, status, message):
        self.send_json({"error": message}, status)

    # 禁用默认日志
    def log_message(self, format, *args):
        print(f"[{self.command}] {self.path}")


server = HTTPServer(("localhost", 8000), PathHandler)
print("服务器启动: http://localhost:8000")
server.serve_forever()

第二步:构建待办事项数据模型

# models.py
from dataclasses import dataclass, field, asdict
from datetime import datetime
from typing import Optional
import uuid


@dataclass
class Todo:
    """待办事项数据模型"""
    title: str
    id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
    completed: bool = False
    created_at: str = field(default_factory=lambda: datetime.now().isoformat())
    updated_at: Optional[str] = None

    def to_dict(self):
        """转换为字典"""
        return asdict(self)

    def complete(self):
        """标记为完成"""
        self.completed = True
        self.updated_at = datetime.now().isoformat()

    def update(self, title: str = None, completed: bool = None):
        """更新待办事项"""
        if title is not None:
            self.title = title
        if completed is not None:
            self.completed = completed
        self.updated_at = datetime.now().isoformat()

    @classmethod
    def from_dict(cls, data: dict):
        """从字典创建实例"""
        return cls(
            id=data.get("id", str(uuid.uuid4())[:8]),
            title=data["title"],
            completed=data.get("completed", False),
            created_at=data.get("created_at", datetime.now().isoformat()),
            updated_at=data.get("updated_at")
        )

第三步:实现数据存储层

# storage.py
import json
from pathlib import Path
from typing import List, Optional
from models import Todo


class TodoStorage:
    """待办事项存储类"""

    def __init__(self, data_file: str = "data/todos.json"):
        self.data_file = Path(data_file)
        self.data_file.parent.mkdir(parents=True, exist_ok=True)
        self.todos: List[Todo] = self.load()

    def load(self) -> List[Todo]:
        """从文件加载数据"""
        try:
            if self.data_file.exists():
                with open(self.data_file, "r", encoding="utf-8") as f:
                    data = json.load(f)
                    return [Todo.from_dict(item) for item in data]
        except (json.JSONDecodeError, KeyError) as e:
            print(f"加载数据失败: {e}")
        return []

    def save(self):
        """保存数据到文件"""
        try:
            with open(self.data_file, "w", encoding="utf-8") as f:
                data = [todo.to_dict() for todo in self.todos]
                json.dump(data, f, ensure_ascii=False, indent=2)
        except Exception as e:
            print(f"保存数据失败: {e}")

    def get_all(self) -> List[Todo]:
        """获取所有待办事项"""
        return self.todos

    def get_by_id(self, todo_id: str) -> Optional[Todo]:
        """根据ID获取待办事项"""
        for todo in self.todos:
            if todo.id == todo_id:
                return todo
        return None

    def create(self, title: str) -> Todo:
        """创建待办事项"""
        todo = Todo(title=title)
        self.todos.append(todo)
        self.save()
        return todo

    def update(self, todo_id: str, title: str = None, completed: bool = None) -> Optional[Todo]:
        """更新待办事项"""
        todo = self.get_by_id(todo_id)
        if todo:
            todo.update(title=title, completed=completed)
            self.save()
        return todo

    def delete(self, todo_id: str) -> bool:
        """删除待办事项"""
        todo = self.get_by_id(todo_id)
        if todo:
            self.todos.remove(todo)
            self.save()
            return True
        return False

    def clear_completed(self) -> int:
        """清除已完成的待办事项"""
        original_count = len(self.todos)
        self.todos = [t for t in self.todos if not t.completed]
        self.save()
        return original_count - len(self.todos)

第四步:实现 HTTP 请求处理

# handlers.py
from http.server import BaseHTTPRequestHandler
import json
from urllib.parse import urlparse, parse_qs
from storage import TodoStorage


class TodoHandler(BaseHTTPRequestHandler):
    """待办事项 API 处理器"""

    # 类级别的存储实例(所有请求共享)
    storage = TodoStorage()

    def do_GET(self):
        """处理 GET 请求"""
        parsed = urlparse(self.path)
        path = parsed.path

        if path == "/api/todos":
            # 获取所有待办事项
            todos = [t.to_dict() for t in self.storage.get_all()]
            self.send_json({"todos": todos, "count": len(todos)})

        elif path.startswith("/api/todos/"):
            # 获取单个待办事项
            todo_id = path.split("/")[-1]
            todo = self.storage.get_by_id(todo_id)
            if todo:
                self.send_json(todo.to_dict())
            else:
                self.send_error_json(404, f"待办事项 {todo_id} 不存在")

        elif path == "/":
            # API 首页
            self.send_json({
                "message": "待办事项 API",
                "version": "1.0",
                "endpoints": {
                    "GET /api/todos": "获取所有待办事项",
                    "GET /api/todos/:id": "获取单个待办事项",
                    "POST /api/todos": "创建待办事项",
                    "PUT /api/todos/:id": "更新待办事项",
                    "DELETE /api/todos/:id": "删除待办事项"
                }
            })

        else:
            self.send_error_json(404, "路径不存在")

    def do_POST(self):
        """处理 POST 请求"""
        if self.path == "/api/todos":
            try:
                data = self.get_json_body()
                if not data.get("title"):
                    self.send_error_json(400, "title 字段必填")
                    return

                todo = self.storage.create(data["title"])
                self.send_json(todo.to_dict(), 201)

            except json.JSONDecodeError:
                self.send_error_json(400, "无效的 JSON 数据")
        else:
            self.send_error_json(404, "路径不存在")

    def do_PUT(self):
        """处理 PUT 请求"""
        if self.path.startswith("/api/todos/"):
            todo_id = self.path.split("/")[-1]

            try:
                data = self.get_json_body()
                todo = self.storage.update(
                    todo_id,
                    title=data.get("title"),
                    completed=data.get("completed")
                )

                if todo:
                    self.send_json(todo.to_dict())
                else:
                    self.send_error_json(404, f"待办事项 {todo_id} 不存在")

            except json.JSONDecodeError:
                self.send_error_json(400, "无效的 JSON 数据")
        else:
            self.send_error_json(404, "路径不存在")

    def do_DELETE(self):
        """处理 DELETE 请求"""
        if self.path.startswith("/api/todos/"):
            todo_id = self.path.split("/")[-1]

            if self.storage.delete(todo_id):
                self.send_json({"message": f"待办事项 {todo_id} 已删除"})
            else:
                self.send_error_json(404, f"待办事项 {todo_id} 不存在")
        else:
            self.send_error_json(404, "路径不存在")

    def get_json_body(self):
        """获取请求体的 JSON 数据"""
        content_length = int(self.headers.get("Content-Length", 0))
        body = self.rfile.read(content_length)
        return json.loads(body.decode("utf-8"))

    def send_json(self, data, status=200):
        """发送 JSON 响应"""
        self.send_response(status)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Access-Control-Allow-Origin", "*")  # CORS
        self.end_headers()
        self.wfile.write(json.dumps(data, ensure_ascii=False, indent=2).encode("utf-8"))

    def send_error_json(self, status, message):
        """发送错误响应"""
        self.send_json({"error": message, "status": status}, status)

    def do_OPTIONS(self):
        """处理预检请求(CORS)"""
        self.send_response(200)
        self.send_header("Access-Control-Allow-Origin", "*")
        self.send_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
        self.send_header("Access-Control-Allow-Headers", "Content-Type")
        self.end_headers()

    def log_message(self, format, *args):
        """自定义日志格式"""
        print(f"[{self.command}] {self.path} - {args[1] if len(args) > 1 else ''}")

第五步:主应用入口

# app.py
from http.server import HTTPServer
from handlers import TodoHandler


def run_server(host="localhost", port=8000):
    """启动 HTTP 服务器"""
    server_address = (host, port)
    httpd = HTTPServer(server_address, TodoHandler)

    print(f"""
╔══════════════════════════════════════════════════════╗
║           待办事项 API 服务已启动                    ║
╠══════════════════════════════════════════════════════╣
║  地址: http://{host}:{port}                        ║
║                                                      ║
║  API 端点:                                           ║
║    GET    /api/todos      - 获取所有待办事项        ║
║    GET    /api/todos/:id  - 获取单个待办事项        ║
║    POST   /api/todos      - 创建待办事项            ║
║    PUT    /api/todos/:id  - 更新待办事项            ║
║    DELETE /api/todos/:id  - 删除待办事项            ║
║                                                      ║
║  按 Ctrl+C 停止服务器                               ║
╚══════════════════════════════════════════════════════╝
    """)

    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        print("\n服务器已停止")
        httpd.shutdown()


if __name__ == "__main__":
    run_server()

第六步:测试 API

使用 curl 测试

# 1. 创建待办事项
curl -X POST http://localhost:8000/api/todos \
  -H "Content-Type: application/json" \
  -d '{"title": "学习 Python"}'

# 2. 获取所有待办事项
curl http://localhost:8000/api/todos

# 3. 获取单个待办事项
curl http://localhost:8000/api/todos/abc123

# 4. 更新待办事项
curl -X PUT http://localhost:8000/api/todos/abc123 \
  -H "Content-Type: application/json" \
  -d '{"completed": true}'

# 5. 删除待办事项
curl -X DELETE http://localhost:8000/api/todos/abc123

使用 Python 测试

# test_api.py
import requests

BASE_URL = "http://localhost:8000/api"

def test_api():
    # 1. 创建待办事项
    print("=== 创建待办事项 ===")
    resp = requests.post(f"{BASE_URL}/todos", json={"title": "学习 Python"})
    print(resp.json())
    todo_id = resp.json()["id"]

    # 2. 获取所有待办事项
    print("\n=== 获取所有待办事项 ===")
    resp = requests.get(f"{BASE_URL}/todos")
    print(resp.json())

    # 3. 更新待办事项
    print("\n=== 更新待办事项 ===")
    resp = requests.put(f"{BASE_URL}/todos/{todo_id}", json={"completed": True})
    print(resp.json())

    # 4. 获取单个待办事项
    print("\n=== 获取单个待办事项 ===")
    resp = requests.get(f"{BASE_URL}/todos/{todo_id}")
    print(resp.json())

    # 5. 删除待办事项
    print("\n=== 删除待办事项 ===")
    resp = requests.delete(f"{BASE_URL}/todos/{todo_id}")
    print(resp.json())


if __name__ == "__main__":
    test_api()

进阶:使用 Flask 框架

Flask 是一个轻量级 Web 框架,开发效率更高。

安装 Flask

pip install flask

Flask 版本实现

# flask_app.py
from flask import Flask, request, jsonify
from storage import TodoStorage

app = Flask(__name__)
storage = TodoStorage()


@app.route("/")
def index():
    """API 首页"""
    return jsonify({
        "message": "待办事项 API (Flask 版)",
        "version": "1.0"
    })


@app.route("/api/todos", methods=["GET"])
def get_todos():
    """获取所有待办事项"""
    todos = [t.to_dict() for t in storage.get_all()]
    return jsonify({"todos": todos, "count": len(todos)})


@app.route("/api/todos/<todo_id>", methods=["GET"])
def get_todo(todo_id):
    """获取单个待办事项"""
    todo = storage.get_by_id(todo_id)
    if todo:
        return jsonify(todo.to_dict())
    return jsonify({"error": "待办事项不存在"}), 404


@app.route("/api/todos", methods=["POST"])
def create_todo():
    """创建待办事项"""
    data = request.get_json()
    if not data or not data.get("title"):
        return jsonify({"error": "title 字段必填"}), 400

    todo = storage.create(data["title"])
    return jsonify(todo.to_dict()), 201


@app.route("/api/todos/<todo_id>", methods=["PUT"])
def update_todo(todo_id):
    """更新待办事项"""
    data = request.get_json()
    todo = storage.update(
        todo_id,
        title=data.get("title"),
        completed=data.get("completed")
    )

    if todo:
        return jsonify(todo.to_dict())
    return jsonify({"error": "待办事项不存在"}), 404


@app.route("/api/todos/<todo_id>", methods=["DELETE"])
def delete_todo(todo_id):
    """删除待办事项"""
    if storage.delete(todo_id):
        return jsonify({"message": "删除成功"})
    return jsonify({"error": "待办事项不存在"}), 404


@app.errorhandler(404)
def not_found(error):
    return jsonify({"error": "路径不存在"}), 404


@app.errorhandler(500)
def internal_error(error):
    return jsonify({"error": "服务器内部错误"}), 500


if __name__ == "__main__":
    print("Flask 服务器启动: http://localhost:5000")
    app.run(debug=True, port=5000)

完整项目代码

将所有文件组合在一起:

# 完整的单文件版本 todo_api.py

from http.server import HTTPServer, BaseHTTPRequestHandler
from dataclasses import dataclass, field, asdict
from datetime import datetime
from typing import List, Optional
from pathlib import Path
import json
import uuid


# ==================== 数据模型 ====================

@dataclass
class Todo:
    title: str
    id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
    completed: bool = False
    created_at: str = field(default_factory=lambda: datetime.now().isoformat())
    updated_at: Optional[str] = None

    def to_dict(self):
        return asdict(self)

    def update(self, title=None, completed=None):
        if title is not None:
            self.title = title
        if completed is not None:
            self.completed = completed
        self.updated_at = datetime.now().isoformat()

    @classmethod
    def from_dict(cls, data):
        return cls(
            id=data.get("id", str(uuid.uuid4())[:8]),
            title=data["title"],
            completed=data.get("completed", False),
            created_at=data.get("created_at", datetime.now().isoformat()),
            updated_at=data.get("updated_at")
        )


# ==================== 数据存储 ====================

class TodoStorage:
    def __init__(self, data_file="todos.json"):
        self.data_file = Path(data_file)
        self.todos: List[Todo] = self.load()

    def load(self):
        try:
            if self.data_file.exists():
                with open(self.data_file, "r", encoding="utf-8") as f:
                    return [Todo.from_dict(item) for item in json.load(f)]
        except Exception as e:
            print(f"加载失败: {e}")
        return []

    def save(self):
        with open(self.data_file, "w", encoding="utf-8") as f:
            json.dump([t.to_dict() for t in self.todos], f, ensure_ascii=False, indent=2)

    def get_all(self):
        return self.todos

    def get_by_id(self, todo_id):
        return next((t for t in self.todos if t.id == todo_id), None)

    def create(self, title):
        todo = Todo(title=title)
        self.todos.append(todo)
        self.save()
        return todo

    def update(self, todo_id, title=None, completed=None):
        todo = self.get_by_id(todo_id)
        if todo:
            todo.update(title=title, completed=completed)
            self.save()
        return todo

    def delete(self, todo_id):
        todo = self.get_by_id(todo_id)
        if todo:
            self.todos.remove(todo)
            self.save()
            return True
        return False


# ==================== HTTP 处理器 ====================

class TodoHandler(BaseHTTPRequestHandler):
    storage = TodoStorage()

    def do_GET(self):
        if self.path == "/api/todos":
            todos = [t.to_dict() for t in self.storage.get_all()]
            self.send_json({"todos": todos, "count": len(todos)})
        elif self.path.startswith("/api/todos/"):
            todo_id = self.path.split("/")[-1]
            todo = self.storage.get_by_id(todo_id)
            if todo:
                self.send_json(todo.to_dict())
            else:
                self.send_error_json(404, "待办事项不存在")
        elif self.path == "/":
            self.send_json({"message": "待办事项 API", "version": "1.0"})
        else:
            self.send_error_json(404, "路径不存在")

    def do_POST(self):
        if self.path == "/api/todos":
            try:
                data = self.get_json_body()
                if not data.get("title"):
                    self.send_error_json(400, "title 必填")
                    return
                todo = self.storage.create(data["title"])
                self.send_json(todo.to_dict(), 201)
            except Exception as e:
                self.send_error_json(400, str(e))
        else:
            self.send_error_json(404, "路径不存在")

    def do_PUT(self):
        if self.path.startswith("/api/todos/"):
            todo_id = self.path.split("/")[-1]
            try:
                data = self.get_json_body()
                todo = self.storage.update(todo_id, data.get("title"), data.get("completed"))
                if todo:
                    self.send_json(todo.to_dict())
                else:
                    self.send_error_json(404, "待办事项不存在")
            except Exception as e:
                self.send_error_json(400, str(e))
        else:
            self.send_error_json(404, "路径不存在")

    def do_DELETE(self):
        if self.path.startswith("/api/todos/"):
            todo_id = self.path.split("/")[-1]
            if self.storage.delete(todo_id):
                self.send_json({"message": "删除成功"})
            else:
                self.send_error_json(404, "待办事项不存在")
        else:
            self.send_error_json(404, "路径不存在")

    def get_json_body(self):
        length = int(self.headers.get("Content-Length", 0))
        return json.loads(self.rfile.read(length).decode("utf-8"))

    def send_json(self, data, status=200):
        self.send_response(status)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.end_headers()
        self.wfile.write(json.dumps(data, ensure_ascii=False, indent=2).encode("utf-8"))

    def send_error_json(self, status, message):
        self.send_json({"error": message}, status)

    def log_message(self, format, *args):
        print(f"[{self.command}] {self.path}")


# ==================== 启动服务器 ====================

if __name__ == "__main__":
    server = HTTPServer(("localhost", 8000), TodoHandler)
    print("服务器启动: http://localhost:8000")
    print("API: http://localhost:8000/api/todos")
    print("按 Ctrl+C 停止")
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        print("\n服务器已停止")

项目扩展思路

  1. 添加用户认证

    • 实现用户注册/登录
    • 使用 JWT Token 认证
    • 待办事项关联用户
  2. 使用数据库

    • SQLite 本地数据库
    • MySQL/PostgreSQL 生产环境
  3. 添加前端页面

    • HTML + JavaScript 前端
    • Vue/React 单页应用
  4. 部署上线

    • 使用 Gunicorn/uWSGI
    • Docker 容器化
    • 部署到云服务器

小结

通过这个项目,我们实践了:

  • Python 基础语法:变量、函数、类
  • 数据类型:字典、列表、dataclass
  • 面向对象:类的设计、封装
  • 文件操作:JSON 读写、持久化
  • 异常处理:try-except、错误响应
  • 模块化:代码组织、分层设计
  • HTTP 服务:请求处理、RESTful API

恭喜你完成了 Python 入门教程!继续练习,尝试更多项目!

Prev
Python文件与异常处理 - 读写文件、try/except异常捕获 | HiHuo