实战:把采集结果做成 API 与看板

采集到的数据如果只躺在数据库里,价值就只属于写脚本的人。把它包装成「一个能查的接口 + 一块能看的看板」,数据就能被团队复用:运营自己筛、分析师自己导、其他系统直接调。本章用 FastAPI 做最小可用接口,并补上缓存、部署与数据质量校验。

从表到服务只有三件事:查询接口GET /items 支持筛选与分页,决定用 offset 还是游标分页、要不要鉴权)、看板(趋势图与明细表,决定静态导出还是实时查询)、更新节奏(定时任务频率与接口缓存 TTL 的配合)。经验规则是:接口只做查询,不做采集——采集与查询混在一个进程里,一次慢查询就能把采集节奏拖垮。

FastAPI 最小接口

# api.py —— Python 3.10+,依赖 fastapi、uvicorn、pydantic
import sqlite3
from typing import Optional
from fastapi import Depends, FastAPI, Header, HTTPException, Query
from pydantic import BaseModel, Field
app = FastAPI(title="采集数据查询接口", version="1.0")
DB_PATH = "price.db"
API_KEY = "change-me-in-production"          # 生产环境从环境变量或配置中心读取

class Item(BaseModel):
    """对外暴露的字段白名单:不要直接把数据库行原样返回"""
    sku_id: str = Field(..., description="SKU 标识")
    collect_date: str = Field(..., description="采集日期 YYYY-MM-DD")
    price: Optional[float] = Field(None, description="券后到手价,缺货为 null")
    in_stock: int = Field(1, description="1 在售 0 缺货")

def get_db():
    """每个请求一个连接,用完即关;高并发场景换成连接池"""
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    try:
        yield conn
    finally:
        conn.close()

def verify_key(x_api_key: str = Header(default="")):
    if x_api_key != API_KEY:
        raise HTTPException(status_code=401, detail="无效的 API Key")

@app.get("/items", response_model=list[Item], dependencies=[Depends(verify_key)])
def list_items(
    sku_id: Optional[str] = None,
    start: Optional[str] = Query(None, description="起始日期,含"),
    end: Optional[str] = Query(None, description="结束日期,含"),
    page: int = Query(1, ge=1),
    size: int = Query(20, ge=1, le=200),     # 限制上限,防止 size=100000 拖垮服务
    db: sqlite3.Connection = Depends(get_db),
):
    """按 SKU 与日期区间筛选并分页;值全部走占位符,杜绝注入"""
    where, params = [], []
    if sku_id:
        where.append("sku_id = ?"); params.append(sku_id)
    if start:
        where.append("collect_date >= ?"); params.append(start)
    if end:
        where.append("collect_date <= ?"); params.append(end)
    clause = ("WHERE " + " AND ".join(where)) if where else ""
    rows = db.execute(
        f"SELECT sku_id, collect_date, price, in_stock FROM price_history {clause} "
        f"ORDER BY collect_date DESC, sku_id LIMIT ? OFFSET ?",
        (*params, size, (page - 1) * size)).fetchall()
    return [dict(row) for row in rows]

@app.get("/stats/sku/{sku_id}")
def sku_stats(sku_id: str, db: sqlite3.Connection = Depends(get_db)):
    """聚合视图:近 30 天最低价、最高价与采样点数,看板直接消费"""
    row = db.execute(
        "SELECT MIN(price) AS low, MAX(price) AS high, COUNT(*) AS samples "
        "FROM price_history WHERE sku_id = ? AND price IS NOT NULL "
        "AND collect_date >= date('now', '-30 day')", (sku_id,)).fetchone()
    if not row or row["samples"] == 0:
        raise HTTPException(status_code=404, detail="该 SKU 近 30 天无数据")
    return dict(row)
pip install fastapi uvicorn pydantic
uvicorn api:app --host 127.0.0.1 --port 8000 --workers 2
curl -s -H 'x-api-key: change-me-in-production' \
  'http://127.0.0.1:8000/items?sku_id=demo-1001&page=1&size=10'

response_model 是白名单,能把原始 HTML、内部备注挡在接口外;f-string 只拼接 WHERE 结构,值一律走占位符。部署用 systemd 托管:unit 里设 WorkingDirectoryEnvironment="API_KEY=..."ExecStart=.../uvicorn api:app --host 127.0.0.1 --port 8000 --workers 2Restart=always,然后 systemctl enable --now crawler-api;采集任务由 APScheduler 或 cron 单独调度,接口只读。

看板:静态导出、Streamlit 还是 Grafana

方案优点缺点适用
定时导出 CSV/Excel零运维、业务方熟悉手动打开、无交互每日/每周报表
Streamlit十几行代码出交互页(st.line_chart + st.dataframe每次访问都跑查询,不适合高并发内部小工具、分析师自助
Grafana + 时序库图表专业、告警一体需维护 Prometheus/MySQL 数据源长期运行的运营看板
自建前端完全定制成本最高对外产品

Grafana 的做法是把 MySQL 配成数据源,用 SELECT collect_date AS time, price FROM price_history WHERE sku_id = $sku 这样的查询变量做面板,并配合 Prometheus 的采集指标看「数据是否在按时更新」——两块看板同屏就能一眼看出采集是否正常。

缓存与更新频率

数据特征更新频率缓存策略
每日快照(价格、职位)每天 1 次接口加 5~10 分钟 TTL 或进程内 lru_cache
小时级指标每小时缓存 1 分钟,或读时物化到汇总表
实时明细随采集更新不缓存,但要用游标分页 + 索引
聚合统计与明细同频预计算成汇总表,接口只查汇总

别让接口每次请求都扫明细表做聚合,这是最常见的性能坑:把 MIN/MAX/COUNT 在采集完成后算一次写入 summary 表。

权限、限流与数据质量

  • 鉴权:内部系统用固定 API Key(Header 校验)即可;对外或多角色场景改用 JWT/OAuth2,权限收敛到「能查哪些字段、哪些时间范围」。
  • 限流:入口用 Nginx limit_req 按 IP 限流,或应用内用 slowapi 按 Key 限流;聚合接口设更严的配额,因为它最贵。
  • 数据质量校验当天数据条数低于近 7 日均值 30% 时返回警告并在看板标红,避免展示「采集已经坏了」的旧数据;判定逻辑应与第 22 章的告警共用同一份实现,口径必须一致。
校验项规则动作
条数突降今日条数 < 近 7 日均值 × 30%看板标红 + 告警
数据陈旧最新 collect_date 距今 > 2 天接口返回 stale=true
空值率关键字段空值率 > 20%标记质量异常,不进对外报表
口径混用同一 SKU 同一天出现多个价格口径拒绝返回,先修数据

常见坑

现象原因处理
接口返回了内部字段直接 SELECT * 并原样返回response_model 定义字段白名单
深分页越来越慢OFFSET 扫描改游标分页(按 id > last_id
看板数据不是最新缓存 TTL 过长或采集失败未告警暴露数据时间戳并加质量校验
数据库连接被耗尽每次请求新建连接且未关闭用依赖注入的 finally 关闭或换连接池
采集慢查询拖垮接口采集与查询共用库且无隔离读写分离,或让采集走独立库

合规提示:把数据变成服务会显著放大合规责任——一旦对外提供接口,你就从「自己看数据」变成「对外提供数据」。只采集公开数据并遵守 robots.txt 与站点服务条款,不采集个人隐私信息,不绕过登录、验证码与付费墙;接口应做鉴权、限流与字段白名单,避免被批量拉取;对外提供或再分发前需确认《个人信息保护法》《数据安全法》《著作权法》相关要求并取得授权,优先使用官方 API 与开放数据。

小结:接口只做查询,用 FastAPIpydantic 白名单和参数化 SQL,分页限制 size 上限并做鉴权限流;看板按场景在静态导出、Streamlit 与 Grafana 之间选;把聚合预计算成汇总表,并用「条数突降 + 数据时间戳」两道校验保证看板展示的永远是可信数据。

笔记加载中…