【Python精讲 16】实战项目演练(二):用Flask/FastAPI发布你的第一个Web API

摘要:我们已经学会了如何从网上"获取"数据,现在是时候学习如何向外"提供"数据和服务了。本文将带你使用一个轻量级的Web框架(以FastAPI为例,它更现代且自带文档),亲手创建一个简单的Web API。这个API可以接收网络请求,执行Python逻辑,并返回JSON格式的数据。这不仅是后端开发的入门,更是让你编写的任何脚本都能通过网络被调用的关键一步。

前言:让你的代码"上线"

你写了一个很酷的Python脚本,比如一个能根据输入文本生成摘要的函数。现在,你想让你的朋友,甚至全世界的人都能使用它,但他们并没有Python环境。怎么办?

最好的方式就是把它包装成一个Web API (Application Programming Interface)

  • API:就像一个餐厅的服务员。你(客户端)不需要知道后厨(服务器逻辑)是怎么运作的,你只需要按照菜单(API文档)告诉服务员你想要什么(发送一个HTTP请求),服务员就会把菜(JSON数据)端给你。
  • Web框架 (Flask/FastAPI):就是一套标准化的"厨房设备和管理流程"。它帮你处理了所有繁琐的底层网络细节(如处理HTTP请求、路由分发),让你能专注于烹饪(编写核心业务逻辑)。

我们将使用FastAPI,因为它非常快、易于学习,并且能自动生成交互式的API文档,对新手极其友好。

安装必要的库

bash 复制代码
pip install fastapi
pip install uvicorn[standard] # uvicorn是一个运行FastAPI应用的服务器

一、项目目标

我们将创建一个简单的"城市信息查询"API,它将具备以下功能:

  1. 根路径 (/): 访问时返回一个欢迎信息。
  2. 城市列表 (/cities): 响应一个包含多个城市信息的列表。
  3. 特定城市查询 (/cities/{city_id}): 根据传入的城市ID,返回该城市的详细信息。如果城市不存在,返回一个错误信息。

二、代码实现

  1. 创建一个名为 main.py 的文件。
  2. 将以下所有代码复制到 main.py 中。
python 复制代码
# main.py
from fastapi import FastAPI, HTTPException
from fastapi.responses import Response, HTMLResponse

# 1. 创建 FastAPI 实例
# 这就是我们的Web应用核心
app = FastAPI(
    title="城市信息API",
    description="一个用于查询城市信息的简单API",
    version="1.0.0",
)

# 简单的内联 SVG 作为 favicon(避免二进制 .ico 文件)
FAVICON_SVG = """
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
  <defs>
    <linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
      <stop offset="0%" stop-color="#4f46e5"/>
      <stop offset="100%" stop-color="#22d3ee"/>
    </linearGradient>
  </defs>
  <rect width="64" height="64" rx="12" fill="url(#g)"/>
  <path d="M20 44c0-8 6-14 14-14h4v-6h6v26h-6v-8h-4c-2.2 0-4 1.8-4 4v4h-6v-6z" fill="#ffffff"/>
</svg>
"""

# 2. 准备一些模拟数据
# 在真实项目中,这些数据可能来自数据库
CITIES_DB = {
    1: {"name": "北京", "population": 2189, "country": "中国"},
    2: {"name": "东京", "population": 3739, "country": "日本"},
    3: {"name": "纽约", "population": 839, "country": "美国"},
}

# 3. 定义路由和处理函数

# 提供 /favicon.ico,避免浏览器请求 404
@app.get("/favicon.ico")
def favicon():
    return Response(content=FAVICON_SVG, media_type="image/svg+xml")

# 优化后的可视化首页:展示城市列表、按ID查询,并链接到Swagger文档
@app.get("/", response_class=HTMLResponse)
def read_root():
    html = """
    <!DOCTYPE html>
    <html lang=\"zh-CN\">
    <head>
      <meta charset=\"utf-8\" />
      <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />
      <title>城市信息 API 演示</title>
      <style>
        :root { --bg: #0f172a; --card:#0b1220; --text:#e5e7eb; --muted:#9aa0aa; --primary:#22d3ee; --accent:#4f46e5; }
        * { box-sizing: border-box; }
        body { margin:0; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial; background: linear-gradient(180deg, #0b1020, #0f172a); color: var(--text); }
        header { padding: 28px 20px; background: linear-gradient(90deg, var(--accent), var(--primary)); color: white; }
        .wrap { max-width: 1100px; margin: 0 auto; }
        h1 { margin: 0; font-size: 24px; font-weight: 700; letter-spacing: .5px; }
        main { padding: 26px 16px 40px; }
        .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; }
        .card { background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.06); border-radius: 14px; padding: 16px; backdrop-filter: blur(6px); box-shadow: 0 8px 22px rgba(0,0,0,.25); }
        .card h3 { margin: 0 0 8px; font-size: 18px; }
        .muted { color: var(--muted); font-size: 13px; }
        .row { display:flex; gap: 16px; flex-wrap: wrap; margin-bottom: 18px; }
        .panel { flex: 1 1 320px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.06); border-radius: 14px; padding: 16px; }
        label { display:block; margin-bottom: 8px; color: var(--muted); }
        input { width: 100%; padding: 10px 12px; border-radius: 10px; border: 1px solid rgba(255,255,255,.15); background: rgba(255,255,255,.06); color: var(--text); outline: none; }
        button { margin-top: 8px; padding: 10px 14px; border: none; border-radius: 10px; color:#0b1020; background: linear-gradient(90deg, var(--primary), var(--accent)); cursor: pointer; font-weight: 600; }
        button:disabled { opacity: .6; cursor: not-allowed; }
        .result { margin-top: 10px; font-size: 14px; }
        footer { text-align:center; padding: 18px; color: var(--muted); border-top: 1px solid rgba(255,255,255,.06); }
        a { color: #7dd3fc; text-decoration: none; }
        a:hover { text-decoration: underline; }
      </style>
    </head>
    <body>
      <header>
        <div class=\"wrap\"><h1>城市信息 API 演示</h1></div>
      </header>
      <main class=\"wrap\">
        <div class=\"row\">
          <section class=\"panel\" style=\"min-height:160px\">
            <h2 style=\"margin:0 0 10px\">按 ID 查询</h2>
            <label for=\"cityId\">输入城市 ID(例如 1、2、3)</label>
            <input id=\"cityId\" placeholder=\"如:1\" />
            <button id=\"btnQuery\">查询</button>
            <div id=\"queryResult\" class=\"result muted\">等待查询...</div>
          </section>
          <section class=\"panel\">
            <h2 style=\"margin:0 0 10px\">API 文档</h2>
            <p class=\"muted\">你可以在 <a href=\"/docs\" target=\"_blank\">/docs</a> 查看交互式接口文档(Swagger UI)。</p>
            <p class=\"muted\">或者直接访问 <a href=\"/api\" target=\"_blank\">/api</a> 获取欢迎信息(JSON)。</p>
          </section>
        </div>
        <section>
          <h2 style=\"margin:6px 0 12px\">城市列表</h2>
          <div id=\"cityList\" class=\"grid\"></div>
        </section>
      </main>
      <footer>
        由 FastAPI 提供服务 · 示例数据仅用于演示
      </footer>
      <script>
        async function loadCities() {
          const container = document.getElementById('cityList');
          container.innerHTML = '<div class="muted">加载中...</div>';
          try {
            const res = await fetch('/cities');
            const data = await res.json();
            if (!Array.isArray(data)) throw new Error('响应格式不正确');
            container.innerHTML = data.map((c, idx) => `
              <div class="card">
                <h3>${c.name}</h3>
                <div class="muted">国家/地区:${c.country}</div>
                <div class="muted">人口(万):${c.population}</div>
                <div class="muted">序号:${idx + 1}</div>
              </div>
            `).join('');
          } catch (e) {
            container.innerHTML = '<div class="muted">加载失败:' + (e.message || e) + '</div>'
          }
        }

        async function queryById() {
          const id = document.getElementById('cityId').value.trim();
          const btn = document.getElementById('btnQuery');
          const result = document.getElementById('queryResult');
          if (!id) { result.textContent = '请输入城市ID'; return; }
          btn.disabled = true; result.textContent = '查询中...';
          try {
            const res = await fetch('/cities/' + encodeURIComponent(id));
            if (res.ok) {
              const data = await res.json();
              result.innerHTML = `<b>${data.name}</b> · 国家/地区:${data.country} · 人口(万):${data.population}`;
            } else if (res.status === 404) {
              const j = await res.json();
              result.textContent = '未找到:' + (j.detail || '城市不存在');
            } else {
              result.textContent = '请求失败:' + res.status;
            }
          } catch (e) {
            result.textContent = '网络错误:' + (e.message || e);
          } finally {
            btn.disabled = false;
          }
        }

        document.getElementById('btnQuery').addEventListener('click', queryById);
        loadCities();
      </script>
    </body>
    </html>
    """
    return HTMLResponse(content=html)

# 保留原本欢迎信息(JSON)在 /api
@app.get("/api")
def api_root():
    return {"message": "欢迎使用城市信息查询API!"}

# 路由:/cities
@app.get("/cities")
def get_cities():
    """
    返回所有城市信息的列表。
    """
    return list(CITIES_DB.values())

# 路由:/cities/{city_id}
# {city_id} 是一个"路径参数",它的值会被传给函数的同名参数
@app.get("/cities/{city_id}")
def get_city_by_id(city_id: int):
    """
    根据城市ID查询特定城市的信息。
    
    - **city_id**: 要查询的城市ID (整数)。
    """
    city = CITIES_DB.get(city_id)
    
    if not city:
        # 如果在数据库中找不到城市,抛出一个HTTP 404错误
        raise HTTPException(status_code=404, detail=f"ID为 {city_id} 的城市未找到")
        
    return city

# 假设我们还想添加一个新城市 (POST请求)
@app.post("/cities")
def create_city(name: str, population: int, country: str):
    """
    创建一个新城市。
    (这是一个简单的示例,不会真的保存)
    """
    new_id = max(CITIES_DB.keys()) + 1
    new_city = {"name": name, "population": population, "country": country}
    # 在真实应用中,这里会执行 CITIES_DB[new_id] = new_city 并保存到数据库
    print(f"模拟创建新城市: ID={new_id}, Data={new_city}")
    return {"message": "城市创建成功(模拟)", "city_id": new_id, "data": new_city}

三、运行与测试你的API

3.1 启动服务器

在你的终端中,进入 main.py 所在的目录,然后运行以下命令:

bash 复制代码
uvicorn main:app --reload
  • main: 指的是 main.py 文件。
  • app: 指的是我们在代码中创建的 FastAPI 实例 app = FastAPI()
  • --reload: 这个参数非常有用,它会让服务器在你修改并保存代码后自动重启。

如果一切顺利,你会看到类似这样的输出:

复制代码
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

这表明你的API服务器已经在你的本地 8000 端口上运行了!

3.2 使用自动生成的交互式文档进行测试 (FastAPI的魅力!)

现在,打开你的浏览器,访问以下两个URL中的任意一个:

你会看到一个精美的、可交互的API文档页面。FastAPI自动根据你的Python代码生成了这一切!

在这个页面上,你可以:

  1. 看到你定义的所有API"端点"(Endpoints)。
  2. 展开每一个端点,查看它的详细说明、参数、可能的响应。
  3. 点击 "Try it out" 按钮,直接在浏览器中输入参数,然后点击 "Execute" 发送请求,并立即看到真实的服务器响应!

尝试一下

  • /cities/{city_id} 端点,输入 city_id2,执行,看看是否返回东京的信息。
  • 输入 city_id99,执行,看看是否返回我们定义的404错误。

总结

恭喜你,你已经成功地构建并发布了一个Web API!虽然它很简单,但它包含了后端开发的全部核心要素:

  • Web框架 :使用FastAPI处理网络请求。
  • 路由 :通过@app.get等装饰器,将URL路径映射到具体的处理函数。
  • 业务逻辑 :在处理函数中编写你的Python代码(如从CITIES_DB中查询数据)。
  • 数据交换:自动将Python字典和列表转换为客户端需要的JSON格式。
  • 错误处理 :通过HTTPException向客户端返回清晰的错误信息。

现在,你的任何Python代码,无论是复杂的机器学习模型,还是一个简单的数据计算脚本,都可以通过这种方式包装成一个服务,被网页、手机App或其他任何能上网的程序所调用。你已经打开了通往后端开发和服务化编程的大门。

预告:【Python精讲 完结】技术栈梳理:从Python开发者到专家的进阶之路

相关推荐
fenghx2583 小时前
vscode使用arcpy-选择arcgis带的python+运行错误解决
vscode·python·arcgis
王嘉俊9253 小时前
Flask 入门:轻量级 Python Web 框架的快速上手
开发语言·前端·后端·python·flask·入门
爱刘温柔的小猪3 小时前
Python 基于 MinIO 的文件上传服务与图像处理核心实践
python·minio
(●—●)橘子……3 小时前
记力扣2271.毯子覆盖的最多白色砖块数 练习理解
数据结构·笔记·python·学习·算法·leetcode
做运维的阿瑞3 小时前
Python 面向对象编程深度指南
开发语言·数据结构·后端·python
木木子99993 小时前
Python的typing模块:类型提示 (Type Hinting)
开发语言·windows·python
MediaTea4 小时前
Python 编辑器:PyCharm
开发语言·ide·python·pycharm·编辑器
小熊出擊4 小时前
[pytest] 一文掌握 fixture 的作用域(scope)机制
python·功能测试·单元测试·自动化·pytest
Cherry Zack4 小时前
Django 视图与路由基础:从URL映射到视图函数
后端·python·django