浅谈FastAPI入门,从手撸一个Markdown转PDF网站开始

254 阅读9分钟

什么是FastAPI?

FastAPI 是一个现代、高性能且易于使用的 Python Web 框架,专门用于构建 RESTful API 服务。它基于 Python 3.6+ 的类型提示,并依赖于 Starlette(轻量 ASGI 框架)和 Pydantic(数据验证与序列化库)构建而成。

核心特点

  • 高性能:FastAPI 使用了 uvicorn,一个基于 uvloop 的异步服务器,因此具有非常高的性能。与 Node.js 和 Go 相媲美,是目前最快的 Python Web 框架之一。
  • 自动文档生成:FastAPI 使用了类型提示,因此可以轻松地定义 API 的输入和输出参数,并自动生成 Swagger UI 和 ReDoc 交互式 API 文档。
  • 类型提示:利用 Python 类型注解进行数据验证、序列化与 IDE 智能补全。
  • 异步支持:原生支持 async/await,适合高并发和 IO 密集型任务。
  • 数据验证:基于 Pydantic,自动校验请求和响应数据格式,减少运行时错误。

常见使用场景

  • 构建 RESTful API 后端服务
  • 微服务架构 中的轻量级服务
  • 实时通信(支持 WebSocket)
  • 数据接口服务(如 GraphQL、OpenAPI 等)

FastAPI和主流的Python Web框架比较

维度DjangoFlaskFastAPI
定位全栈“全家桶”轻量级微框架高性能 API 框架
架构MTV(类 MVC)WSGI 微核心ASGI 异步原生
性能(简单接口)~15 ms~8 ms~3 ms
并发能力~1 000 RPS~2 500 RPS~6 000 RPS
异步支持需 Channels 扩展需外部库原生 async/await
自动 API 文档需 drf-yasg 等需 flask-restx 等Swagger UI & ReDoc 开箱即用
数据验证Form + ORM手动或扩展Pydantic 自动校验
内置 ORM / Admin✅ 完整 ORM + Admin❌ 需 SQLAlchemy 等❌ 可自由选型
学习曲线陡峭平缓中等
适用场景CMS、电商、后台管理微服务、小型站点高并发 API、ML 推理、实时推送
通过这个比较可以看出来,FastAPI 适合高并发、高性能的 API 服务,而 Django 和 Flask 适合开发 Web 应用。尤其是在机器学习推理相关的应用中,FastAPI的高并发,低延迟更具优势,所以还是值得关注下FastAPI这个框架。

代码示例

入门必备的Hello World代码:

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello, FastAPI!"}

运行这个代码,访问 http://127.0.0.1:8000/,可以看到返回 {"message": "Hello, FastAPI!"}。

pip install fastapi uvicorn
uvicorn main:app --reload

到此为止,恭喜你,可以说已经完成了FastAPI的入门。就是这么简单!

会写Hello World真的够了嘛?

只是用来显摆下会用FastAPI,这就差不多了。但是如果想用FastAPI做项目,还有很多待解决的问题:

  • 如何添加数据库支持?
  • 如何处理用户认证?
  • 如何处理文件上传?
  • 如何处理错误?
  • 如何处理并发? 等等...

先来看下,一般FastAPI的项目结构是怎么样的?

最简单的,或者说最小MVP(单文件)

myapi/
├── main.py          # 所有路由、模型、启动放一起
└── requirements.txt

main.py

from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def root():
    return {"msg": "Hello FastAPI!"}

启动

uvicorn main:app --reload

发现没有,这个就是我们的DEMO了。所以说,FastAPI入门很简单吧。这个模式,适用这类项目 demo、脚本、一次性接口。

最常用的项目结构,标准中型结构

myapi/
├── app/
│   ├── __init__.py
│   ├── main.py              # FastAPI 实例 & 全局中间件
│   ├── api/                 # 按业务划分子路由
│   │   ├── __init__.py
│   │   ├── v1/
│   │   │   ├── __init__.py
│   │   │   ├── users.py     # 用户相关路由
│   │   │   └── items.py
│   ├── core/                # 配置、安全、常量
│   │   ├── config.py
│   │   └── security.py
│   ├── models/              # Pydantic 模型
│   │   ├── __init__.py
│   │   └── user.py
│   ├── crud/                # 数据库操作
│   │   └── user.py
│   └── db/
│       ├── __init__.py
│       └── session.py       # 创建 engine / SessionLocal
├── tests/
│   └── test_user.py
├── .env                     # 环境变量
├── requirements.txt
└── Dockerfile

启动

uvicorn app.main:app --reload

特点

  • 分层清晰(路由 / 业务 / 数据 / 配置)
  • 可扩多版本(v1、v2 并存)
  • 单测、Docker、CI 模板齐全

生产级微服务大仓(多服务复用)

backend/
├── gateway/                 # BFF / 网关
├── services/
│   ├── user-service/
│   ├── order-service/
│   └── notification-service/
│       ├── app/
│       │   ├── main.py
│       │   ├── api/
│       │   ├── models/
│       │   └── ...
│       ├── tests/
│       ├── Dockerfile
│       └── requirements.txt
├── shared/                  # 公共库(日志、模型、工具)
│   ├── logger.py
│   └── models/
├── k8s/                     # Helm Chart / manifests
├── docker-compose.yml
└── skaffold.yaml
  • 每个服务独立 FastAPI 实例
  • 共享代码通过 内部 wheel / git submodule 管理
  • 部署:Docker → K8s / Skaffold / Helm

接下来,就用最常用的中型结构,来写一个完整的项目。

MarkDown 转 PDF

为什么要做这个功能?Markdown 写得爽,PDF 拿得出手,一键让技术文档秒变正式报告。或者是要发小红书,最终可以是PDF再转成一页一页的图片。

项目需求

# 项目需求文档(PRD)
**项目名称**:Markdown 转 PDF Web工具  
**版本**:v1.0  
**作者**:Demo PM  
**日期**:2025-08-05  

---

## 1. 项目背景
技术团队日常用 Markdown 编写方案、周报、SOP,但对外交付、审计、客户汇报必须提供 **排版统一的 PDF**。  
现有做法:  
- 人工复制到 Word → 调格式 → 导出 PDF,耗时 15-30 min/次,易出错。  
**机会点**:一条自动化流水线 **“Markdown → PDF”**,节省人力,统一品牌视觉。

---

## 2. 目标与范围
| 目标 | 指标 |
|---|---|
| 让任何同事把 .md 文件拖进浏览器即可下载 PDF | 单文件 ≤ 5 s 完成 |
| 生成的 PDF 符合公司视觉规范 | 字体、页眉、页脚 100% 命中模板 |

**本期做**  
- 单文件上传、UTF-8 文件名、PDF 下载、24h 有效期  
**本期不做**  
- 多文件批量、在线预览、付费套餐

---

## 3. 用户故事
| 角色 | 故事 | 验收标准(GWT) |
|---|---|---|
| 技术工程师 | 写完设计文档后,能在 10 秒内拿到 PDF | Given 已写完 `design.md`<br>When 上传文件<br>Then 5 秒内浏览器自动下载 `design.pdf` |
| 项目经理 | 给客户的周报无需再手动排版 | Given 周报 repo 更新<br>When CI 触发<br>Then 邮件附带 PDF 附件 |

---

## 4. 功能需求
| 编号 | 描述 | 验收点 |
|---|---|---|
| FR-1 | 文件上传 | 支持拖拽/点击,大小 ≤ 100 MB |
| FR-2 | 文件名解码 | `%E4%BD%A0.md` → `你.md` |
| FR-3 | 异步转 PDF | FastAPI + ReportLab,不阻塞其他请求 |
| FR-4 | 下载链接 | 7 位随机码 + 24 h 有效期 |

---

## 5. 非功能需求
- **性能**:并发 ≥ 500 RPS;P99 ≤ 800 ms(100 KB 文件)  
- **可靠性**:失败率 ≤ 0.1%,自动重试 3 次  
- **安全**:文件类型白名单 `.md/.markdown`,扫描病毒  
- **可观测**:Prometheus 指标 `md2pdf_requests_total`

---

## 6. 原型 & 交互
![Figma 原型](https://www.figma.com/embed/demo.png)  
1. 拖放区 → 2. 进度条 → 3. 下载按钮

---

## 7. 技术方案
| 层级 | 选型 | 理由 |
|---|---|---|
| 框架 | FastAPI + uvicorn | 原生异步,Swagger 自动生成 |
| 转换库 | ReportLab + markdown2 | Python 内一站式,无需 Pandoc |
| 存储 | 临时本地卷 + S3 备份 | 简化部署,后续可切 MinIO |
| 部署 | Docker + K8s| 按需水平扩容 |

---

## 8. 里程碑
| 阶段 | 日期 | 交付物 |
|---|---|---|
| 需求冻结 | 2025-08-10 | PRD v1.0 |
| 开发完成 | 2025-08-20 | 代码 + 单元测试覆盖率 ≥ 90% |
| 灰度上线 | 2025-08-25 | 20% 流量,监控 OK |
| 全量上线 | 2025-08-30 | 100% 流量,周报复盘 |

---

## 9. 风险 & 对策
| 风险 | 概率 | 对策 |
|---|---|---|
| 大文件 OOM | 中 | 限制 100 MB + 流式读取 |
| 字体版权纠纷 | 低 | 使用开源思源黑体 |
| 并发洪峰 | 低 | K8s HPA + 队列削峰 |

---

## 10. 附录
- 术语表:SLA、RPS、P99  
- 参考链接:  
  - FastAPI 官方文档 https://fastapi.tiangolo.com  
  - ReportLab 用户手册

核心代码注意点

1. 文件的读写需要异步的方式

FastAPI中的请求采用异步时间的方式,如果请求中涉及到文件的读写,则需要使用异步的方式来处理,否则可能会导致请求阻塞。

异步读写文件,用aiofiles模块

    async with aiofiles.open(source_file_path, "wb") as source_output_file:
        while chunk := await file.read(1024 * 1024):
            await source_output_file.write(chunk)

想要测试异步时间请求中,同步带来的阻塞影响,可以使用time模块来测试 例如:

import time


@router.get("/test_sync_time/")
async def test_sync_time():
    time.sleep(2) # 模拟同步请求耗时 
    return {"message": "Hello World"}

模拟同时5个并发请求

async def a_sync_requests():
    tasks = []
    for i in range(5):
        tasks.append(asyncio.create_task(a_sync_request()))

    await asyncio.gather(*tasks)


async def a_sync_request():
    async with aiohttp.ClientSession() as session:
        async with session.get(
                'http://localhost:8000/test_sync_time/',
        ) as response:
            result_json = await response.json()
            print(result_json)


@pytest.mark.asyncio
async def test_a_sync_sync_time():
    s = time.monotonic()
    await a_sync_requests()
    print('执行时间:', time.monotonic() - s)

执行结果:

{"message": "Hello World"}
{"message": "Hello World"}
{"message": "Hello World"}
{"message": "Hello World"}
{"message": "Hello World"}
执行时间: 10.000000000000582

使用异步time sleep方法

@router.get("/test_async_time/")
async def test_sync_time():
    await asyncio.sleep(2)  # 测试代码 # 模拟异步请求耗时 
    return {"message": "Hello World"}

模拟同时5个并发请求

async def a_async_requests():
    tasks = []
    for i in range(5):
        tasks.append(asyncio.create_task(a_async_request()))

    await asyncio.gather(*tasks)


async def a_async_request():
    async with aiohttp.ClientSession() as session:
        async with session.get(
                'http://localhost:8000/test_sync_time/',
        ) as response:
            result_json = await response.json()
            print(result_json)


@pytest.mark.asyncio
async def test_a_sync_sync_time():
    s = time.monotonic()
    await a_async_requests()
    print('执行时间:', time.monotonic() - s)

执行结果:

{"message": "Hello World"}
{"message": "Hello World"}
{"message": "Hello World"}
{"message": "Hello World"}
{"message": "Hello World"}
执行时间: 2.010000000000582

通过以上例子可以非常直观的看到,如果在异步事件中,使用了同步代码,则会阻塞事件线程,异步代码变成了同步代码。

2. asyncio.to_thread

asyncio.to_thread 是 Python 3.9 引入的一个函数,用于在异步代码中执行同步阻塞操作,而不会阻塞整个事件循环。

掌握 asyncio.to_thread,你就能在保持代码简洁的同时,充分利用 Python 异步编程的性能优势。

我们的案例里面,reportlab SimpleDocTemplate 的 build 方法并没有提供异步方法,或者异步的库。因此需要用asyncio.to_thread将该方法放到线程中执行,以避免阻塞。

示例

asyncio.to_thread(func, /, *args, **kwargs)
  • func:要执行的同步函数。
  • *args 和 **kwargs:传递给 func 的参数。 示例 1:执行阻塞的 I/O 操作
import asyncio
import time

def blocking_io():
    print("开始读取文件...")
    time.sleep(5)  # 模拟一个耗时的 I/O 操作
    print("文件读取完成")
    return "文件内容"

async def main():
    print("开始异步任务")
    # 使用 to_thread() 在单独的线程中执行阻塞的 I/O 操作
    result = await asyncio.to_thread(blocking_io)
    print(f"读取到的文件内容: {result}")
    print("异步任务完成")

asyncio.run(main())

示例 2:执行阻塞的 CPU 密集型操作

import asyncio
import math

def cpu_bound_task(n):
    print(f"开始计算 {n} 的阶乘...")
    result = math.factorial(n)
    print(f"{n} 的阶乘计算完成")
    return result

async def main():
    print("开始异步任务")
    # 使用 to_thread() 在单独的线程中执行阻塞的 CPU 密集型操作
    result = await asyncio.to_thread(cpu_bound_task, 100000)
    print(f"计算结果: {result}")
    print("异步任务完成")

asyncio.run(main())

注意事项

  • 适用场景判断:只有在处理 I/O 密集型或计算密集型的同步任务时才需要 to_thread。
  • 线程池大小:默认使用标准库 concurrent.futures.ThreadPoolExecutor,池大小通常为 min(32, os.cpu_count() + 4)。
  • 避免过度使用:轻量级操作不需要 to_thread,直接调用即可。
  • 异步函数不需要:如果函数本身就是 async def 定义的,直接用 await 调用即可,不需要 to_thread。
  • 长时间 CPU 密集型任务:由于 GIL 的限制,对于长时间的 CPU 密集型任务,建议使用多进程(multiprocessing)。
  • 超高并发场景:线程太多会有调度开销,对于超高并发场景,建议使用纯异步 I/O。

好了,我们用一个项目大概介绍了FastAPI的用法。入门简单,想用用好FastAPI以及在项目中实践,还是有很多需要学习的内容。