chui_zhi_video_api.md 24 KB

垂直视频查询 API 文档

本文档说明垂直视频查询 API 的调用协议、参数规则、数据库查询逻辑、去重与分页语义、错误处理、日志、部署和性能注意事项。

1. 接口概览

项目 内容
业务接口 POST /api/v1/crawler/videos/query
请求格式 application/json
鉴权 接口自身不校验 Token;公网访问由 Nginx 控制
时间标准 东八区(UTC+8)
默认时间范围 最近 3 天
默认页大小 500 条
最大页大小 API_MAX_LIMIT 控制,默认 500 条
分页方式 基于去重结果最大 id 的游标分页
去重方式 相同 out_video_id 保留最大 id 对应的记录
数据库操作 只读,不修改数据库结构和数据

此外提供两个运维探针:

路径 说明
GET /health 进程存活检查,不访问数据库
GET /ready 就绪检查,执行 SELECT 1 AS ok 验证数据库连接

探针由 Nginx 示例限制为仅本机访问,不应暴露为公网业务接口。

2. 访问控制与请求追踪

2.1 公网访问控制

业务接口自身不校验 Token。公网部署时应在 Nginx 层限制新加坡调度服务器的固定出口 IP, 并启用请求速率和连接数限制;不要将后端 8888 端口直接暴露到公网。

2.2 Request ID

调用方可以传入:

X-Request-ID: scheduler-task-123-page-1

允许字符为字母、数字、点、下划线和短横线,最长 128 个字符。未传或格式不正确时,API 自动生成 UUID。

响应头会返回同一个 X-Request-ID。本地日志和阿里云 SLS 也记录该值,可用于串联调度日志、API 日志和数据库异常。

3. 请求参数

请求体为 JSON 对象。未定义字段会被拒绝,避免拼写错误被静默忽略。

参数 类型 必填 默认值 限制与说明
platforms string[] xiaoniangaoxiaoniangaotuijianliu 只支持这两个平台;1~20 个;单项最长 50;自动去除首尾空格和重复值
keywords string[] [] 最多 20 个;单项最长 200;按标题模糊匹配
filter_match_mode integer 2 1 表示过滤条件 OR;2 表示过滤条件 AND
filters object[] [] 最多 50 个结构化过滤条件
limit integer 500 范围为 1~API_MAX_LIMIT;超出范围直接返回参数错误
cursor object null 第一页不传;下一页原样传回响应的 next_cursor

3.1 创建时间规则

创建时间和其他筛选字段一样放在 filters 中,field 固定为 create_time。时间值支持:

  • 13 位毫秒时间戳,例如 1786982400000
  • 10 位秒时间戳,例如 1786982400
  • 仅包含数字的时间戳字符串
  • ISO 日期时间字符串,例如 2026-08-18 00:00:002026-08-18T00:00:00

时间戳统一按东八区转换为数据库查询时间。

单边或双边范围示例:

"filters": [
  {"field": "create_time", "operator": ">=", "value": 1786896000000},
  {"field": "create_time", "operator": "<", "value": 1786982400000}
]

完全没有 create_time 条件时,API 默认查询当前时间之前3天。多条创建时间条件固定使用 AND,不受 filter_match_mode 影响。默认时间范围的首屏时间锚点会写入 next_cursor.query_time,下一页原样回传游标即可保持范围不漂移。

3.2 关键词规则

关键词只匹配 video_title,每个关键词使用:

video_title LIKE '%关键词%'

多个关键词之间固定使用 OR。例如:

{"keywords": ["早", "养生"]}

等价于:

(video_title LIKE '%早%' OR video_title LIKE '%养生%')

关键词条件与平台、创建时间、视频地址和 filters 之间使用 AND。

%关键词% 无法有效利用普通 B-Tree 索引,短关键词或高频词可能扫描较多数据。

3.3 结构化过滤条件

每个过滤条件格式为:

{
  "field": "like_cnt",
  "operator": ">",
  "value": 3
}

每个 filters 元素严格只允许以下三个 key:

key 必填 说明
field 白名单过滤字段
operator 白名单比较操作符
value 与操作符匹配的单值或数组

缺少任意 key 会返回“不能为空”;增加 columnsqlname 等任何未知 key 会返回 400。例如:

参数校验失败: 不支持的参数: filters.0.column
参数校验失败: filters.0.operator不能为空

允许字段:

字段 含义 值类型
play_cnt 播放数 数字
like_cnt 点赞数 数字
share_cnt 分享数 数字
collection_cnt 收藏数 数字
comment_cnt 评论数 数字
duration 视频时长,单位秒 数字
publish_time 视频发布时间 时间或时间戳
create_time 数据创建时间 时间或时间戳

允许操作符:

操作符 value 要求 示例
> 单值 {"field":"like_cnt","operator":">","value":3}
>= 单值 {"field":"play_cnt","operator":">=","value":1000}
= 单值 {"field":"duration","operator":"=","value":60}
< 单值 {"field":"duration","operator":"<","value":60}
<= 单值 {"field":"comment_cnt","operator":"<=","value":100}
between 恰好两个值的数组,包含边界 {"field":"duration","operator":"between","value":[60,300]}
in 非空数组 {"field":"play_cnt","operator":"in","value":[100,500]}
not_in 非空数组 {"field":"duration","operator":"not_in","value":[0,1]}

数字字段拒绝布尔值、NaN、正负无穷、SQL 片段和不能转换为有限数字的字符串。字段名和操作符均为白名单,所有值都通过 SQL 参数绑定,不直接拼接到 SQL。

between 的起始值不能大于结束值;innot_in 以及其他数组型筛选值最多允许 100 项。过滤条件对象中的未知字段也会被拒绝。

publish_time 当前数据库类型为 varchar(100),API 会把时间值格式化成 YYYY-MM-DD HH:MM:SS 后比较。数据库中的历史值需要保持相同且可排序的格式,否则筛选结果可能不准确。本接口不修改数据库字段类型。

3.4 过滤条件组合

"filter_match_mode": 2
  • 1:除 create_time 外的过滤条件使用 OR
  • 2:除 create_time 外的过滤条件使用 AND,默认值

例如:

{
  "filter_match_mode": 2,
  "filters": [
    {"field": "like_cnt", "operator": ">", "value": 3},
    {"field": "duration", "operator": "between", "value": [60, 300]}
  ]
}

对应:

like_cnt > 3 AND duration BETWEEN 60 AND 300

filter_match_mode 不改变关键词之间的 OR 规则。同一字段的多条条件始终使用 AND, 因此 play_cnt >= 100play_cnt <= 1000 会组成播放量区间;不同字段之间才应用 filter_match_mode。多条 create_time 条件也始终使用 AND,并作为整个查询的时间范围。

3.5 游标参数

第一页不传 cursor

{
  "keywords": ["早"],
  "limit": 500
}

如果响应 has_more=true,下一页原样传入:

{
  "keywords": ["早"],
  "limit": 500,
  "cursor": {
    "id": 6815000,
    "query_time": 1786982400000
  }
}

cursor.id 必须是大于 0 的整数。query_time 仅在使用默认近3天范围时返回。不要自行修改游标,也不要在分页过程中修改其他筛选参数。

4. 完整请求示例

{
  "platforms": ["xiaoniangao", "xiaoniangaotuijianliu"],
  "keywords": ["早", "养生"],
  "filter_match_mode": 2,
  "filters": [
    {"field": "create_time", "operator": ">=", "value": 1786896000000},
    {"field": "create_time", "operator": "<", "value": 1786982400000},
    {"field": "play_cnt", "operator": ">=", "value": 1000},
    {"field": "like_cnt", "operator": ">", "value": 3},
    {"field": "duration", "operator": "between", "value": [60, 300]},
    {"field": "publish_time", "operator": ">=", "value": 1785772800000}
  ],
  "limit": 500
}

4.1 参数校验失败示例

不支持的平台:

{
  "platforms": ["douyin"]
}

返回:

{
  "code": 400,
  "msg": "参数校验失败: platforms: 不支持的平台: douyin",
  "data": [
    {
      "type": "value_error",
      "loc": ["platforms"]
    }
  ]
}

不支持的顶层参数:

{
  "page_index": 2
}

返回消息包含:

参数校验失败: 不支持的参数: page_index

不支持的过滤字段或操作符会指出完整路径,例如:

参数校验失败: filters.0.field不支持值 unknown_field
参数校验失败: filters.0.operator不支持值 contains

5. 数据库查询逻辑

查询依次应用以下规则:

  1. platform IN (...)
  2. 应用显式的 create_time 条件;未提供时限定为最近3天
  3. video_url <> ''
  4. 有关键词时,执行标题模糊匹配
  5. 有结构化过滤条件时,按 filter_match_mode 组合
  6. 在完整筛选范围内按 out_video_id 去重
  7. 在去重结果上应用 id 游标
  8. 多取 1 条判断是否还有下一页
  9. 通过主键回表读取完整视频字段

核心 SQL 形态:

SELECT cv.<视频字段>
FROM crawler_video AS cv
INNER JOIN (
    SELECT MAX(source.id) AS selected_id
    FROM crawler_video AS source
    WHERE <平台、时间、视频地址、关键词、过滤条件>
    GROUP BY
        CASE
            WHEN source.out_video_id = '' THEN source.id
            ELSE 0
        END,
        source.out_video_id
    HAVING MAX(source.id) < :cursor_id
    ORDER BY selected_id DESC
    LIMIT :page_size_plus_one
) AS dedup ON dedup.selected_id = cv.id
ORDER BY cv.id DESC;

第一页没有 HAVING 条件。

5.1 去重语义

  • 相同非空 out_video_id 只保留 id 最大的一条,即最后入库记录。
  • out_video_id='' 的记录按各自 id 分组,因此不会全部合并成一条。
  • 去重发生在分页之前,所以重复记录不占用返回页容量,也不会跨页再次出现。
  • 当前语义是“最大 id/最后入库”,不是严格比较 publish_time

5.2 分页语义

  • 排序方式为 id DESC
  • 下一页通过 HAVING MAX(id) < cursor.id 应用于分组后的结果。
  • 不使用 OFFSET,因此翻到后续页面时不会产生越来越大的跳过成本。
  • 固定时间范围后,新插入且 id 大于当前游标的数据不会插入到本轮分页中间。

6. 响应格式

6.1 成功响应

{
  "code": 0,
  "msg": "",
  "data": {
    "data": [
      {
        "video_id": 10001,
        "user_id": 20001,
        "out_user_id": "external-user-id",
        "platform": "xiaoniangao",
        "strategy": "recommend",
        "out_video_id": "external-video-id",
        "video_title": "早上好",
        "cover_url": "https://example.com/cover.jpg",
        "video_url": "https://example.com/video.mp4",
        "duration": 60,
        "publish_time": "2026-08-18 08:00:00",
        "play_cnt": 1000,
        "like_cnt": 20,
        "share_cnt": 2,
        "collection_cnt": 3,
        "comment_cnt": 5,
        "width": 1080,
        "height": 1920,
        "id": 6815000,
        "create_time": "2026-08-18 08:10:00"
      }
    ],
    "count": 1,
    "has_more": true,
    "next_cursor": {"id": 6815000, "query_time": 1786982400000}
  }
}

字段说明:

字段 说明
code 业务码;成功固定为 0
msg 成功时为空字符串
data.data 当前页视频数组
data.count 当前页实际返回数量,不包含额外探测行
data.has_more 是否存在下一页
data.next_cursor 下一页游标;没有下一页时为 null

响应头包含:

X-Request-ID: <本次请求ID>
Cache-Control: no-store

7. 错误响应

错误响应统一为:

{
  "code": 422,
  "msg": "like_cnt筛选值必须是数字: invalid",
  "data": null
}
HTTP 状态 场景 调用方处理建议
400 JSON 无效、字段类型错误、未知参数、时间范围错误 修正请求,不重试
401 Token 缺失或错误 修正鉴权,不重试
422 结构化过滤值不符合业务要求 修正参数,不重试
500 数据库查询异常或内部异常 记录 X-Request-ID,排查日志;谨慎重试
503 查询并发已满,或 /ready 检查失败 指数退避后重试
504 数据库查询超过 API_QUERY_TIMEOUT 缩小时间范围/关键词或稍后重试

调度端当前只自动重试网络异常以及 HTTP 429、502、503、504,最多 3 次;退避约为 1 秒、2 秒、4 秒并附加随机抖动。400、401、422 不会重试。

8. 并发、超时与资源保护

默认配置:

配置 默认值 说明
API_HOST 127.0.0.1 只监听本机,由 Nginx 对外代理
API_PORT 8888 API 本地监听端口
DB_POOL_SIZE 12 单进程数据库连接池上限
DB_POOL_RECYCLE 3600 数据库连接回收秒数
API_MAX_CONCURRENT_REQUESTS 8 查询并发配置上限
API_QUEUE_TIMEOUT 2.0 等待查询并发槽位的最长秒数
API_QUERY_TIMEOUT 30 单条查询最长秒数
API_MAX_LIMIT 500 单页最大返回条数
API_LOG_QUEUE_SIZE 1000 SLS 异步日志队列长度
API_LOG_FLUSH_TIMEOUT 5.0 单批 SLS 上报及关闭刷新超时

实际查询并发取以下两者最小值:

min(API_MAX_CONCURRENT_REQUESTS, DB_POOL_SIZE)

这样可以避免大量查询进入数据库连接池内部无界等待。超过并发槽位且 2 秒内未获得执行机会时返回 503。

如果使用多个 API 进程,总数据库连接数约为:

进程数 × DB_POOL_SIZE

扩容进程前必须确认数据库最大连接数和实际负载。

9. 日志与监控

9.1 本地日志

本地访问日志记录:

  • request_id
  • 请求方法和 URL
  • 脱敏后的请求参数
  • HTTP 状态码
  • 请求总耗时
  • 失败时的错误原因和脱敏堆栈

不记录响应视频内容,避免日志量过大。

9.2 阿里云 SLS

请求完成后只写入有界内存队列,由后台任务批量上报,业务响应不等待 SLS。

后台策略:

  • 每批最多 50 条
  • 上报失败最多重试 3 次
  • 队列满时写本地错误日志并丢弃当前 SLS 事件
  • 服务关闭时尝试刷新队列,超时后取消日志任务并优雅关闭数据库连接池

主要字段:

  • TraceId / request_id
  • pathmethod
  • status_codesuccess
  • request_duration_ms
  • query_duration_ms
  • query_result_count
  • query_success
  • failure_stage
  • error_type
  • message
  • 脱敏后的 request_params

API日志全部使用SLS顶层字段,不再写入重复的 data 字符串。字典和数组字段使用紧凑JSON字符串, 布尔值统一为小写 true/false;值为空的 failure_stageerror_type 等字段不会写入。 account=None 和自定义 timestamp 也不会出现在API日志中,时间以SLS自带的 __time__ 为准。 这样可以直接统计每日请求量、成功率、P95/P99耗时和错误分布。

API访问日志使用独立的SLS目的地,默认配置为:

API_ALIYUN_LOG_PROJECT=crawler-log-prod
API_ALIYUN_LOGSTORE=crawler-api-access
API_ALIYUN_LOG_ENDPOINT=cn-hangzhou.log.aliyuncs.com

部署前需要在阿里云日志服务中创建 crawler-api-access Logstore,并为上面的核心指标建立索引。 原有爬虫日志继续写入 crawler-fetch,两者不会混在一起。如果需要使用其他Project或Logstore, 只修改环境变量并重启API即可。

10. curl 示例

以下示例假设:

export CHUI_ZHI_API_URL="https://api.example.com/api/v1/crawler/videos/query"

10.1 默认最近 3 天

curl --request POST "$CHUI_ZHI_API_URL" \
  --header "Content-Type: application/json" \
  --header "X-Request-ID: manual-test-001" \
  --data '{}'

10.2 关键词模糊搜索

curl --request POST "$CHUI_ZHI_API_URL" \
  --header "Content-Type: application/json" \
  --data '{
    "keywords": ["早"],
    "limit": 500
  }'

10.3 指定时间及数值条件

curl --request POST "$CHUI_ZHI_API_URL" \
  --header "Content-Type: application/json" \
  --data '{
    "filter_match_mode": 2,
    "filters": [
      {"field": "create_time", "operator": ">=", "value": 1786896000000},
      {"field": "create_time", "operator": "<", "value": 1786982400000},
      {"field": "like_cnt", "operator": ">", "value": 3},
      {"field": "duration", "operator": "between", "value": [60, 300]}
    ],
    "limit": 200
  }'

10.4 下一页

curl --request POST "$CHUI_ZHI_API_URL" \
  --header "Content-Type: application/json" \
  --data '{
    "keywords": ["早"],
    "limit": 500,
    "cursor": {"id": 6815000, "query_time": 1786982400000}
  }'

后续页必须继续使用第一页的关键词、过滤条件和 limit,并原样传递响应中的 next_cursor

10.5 健康检查

在 API 所在服务器执行:

curl --fail http://127.0.0.1:8888/health
curl --fail http://127.0.0.1:8888/ready

11. 启动与公网部署

11.1 使用现有 deploy.sh 一起部署(推荐)

现有 deploy.sh 已同时管理主爬虫和垂直视频 API。在服务器的 /root/AutoScraperX/.env 中增加:

API_HOST=127.0.0.1
API_PORT=8888
API_ALIYUN_LOG_PROJECT=crawler-log-prod
API_ALIYUN_LOGSTORE=crawler-api-access
API_ALIYUN_LOG_ENDPOINT=cn-hangzhou.log.aliyuncs.com

执行完整部署:

cd /root/AutoScraperX
bash deploy.sh

部署脚本按以下顺序执行:

  1. 拉取 master 最新代码。
  2. 创建或更新 /root/AutoScraperX/venv
  3. 安装 requirements.txt
  4. 在停止旧服务前检查 API 配置。
  5. 根据 PID 文件优雅停止旧 API 和主爬虫;首次升级时兼容清理旧版无 PID 进程。
  6. 启动 python -m api.app(Uvicorn单Worker)。
  7. 最多等待 15 秒调用 /ready,确认进程和数据库都可用。
  8. API 就绪后启动 main.py
  9. 主进程启动检查失败时,同时停止本次启动的 API,避免留下半部署状态。

部署产物:

文件 用途
logs/autoscraperx_deploy.log 部署过程日志
logs/api_startup.log API 标准输出和启动异常
logs/main_startup.log 主爬虫标准输出和启动异常
run/api.pid API 进程 PID
run/main.pid 主爬虫进程 PID

部署完成后验证:

curl --fail http://127.0.0.1:8888/health
curl --fail http://127.0.0.1:8888/ready
cat /root/AutoScraperX/run/api.pid
cat /root/AutoScraperX/run/main.pid
tail -n 100 /root/AutoScraperX/logs/autoscraperx_deploy.log

如果服务器目录或虚拟环境路径不同,可以覆盖:

APP_DIR=/data/AutoScraperX VENV_DIR=/data/AutoScraperX/venv bash deploy.sh

11.2 单独启动 API(仅用于本地调试)

cd /root/AutoScraperX
./run_api.sh prod

run_api.sh 优先使用 venv/bin/python,不存在时使用 .venv/bin/python

默认监听:

127.0.0.1:8888

启动窗口会打印所有已注册的路径和处理方法。

11.3 Nginx(首次部署配置一次)

示例文件:

deploy/nginx/chui_zhi_api.conf.example

部署要点:

  • API 保持监听 127.0.0.1:8888
  • Nginx 对外监听 HTTPS 443
  • 只允许业务路径使用 POST
  • 请求体最大 1 MB
  • 开启 JSON gzip
  • 配置请求速率和单 IP 连接限制
  • 透传 X-Request-ID、真实 IP 和协议
  • /health/ready 只允许本机访问
  • 如果新加坡调度服务器有固定出口 IP,建议启用 IP 白名单

安装示例:

cp deploy/nginx/chui_zhi_api.conf.example /etc/nginx/conf.d/chui_zhi_api.conf
# 编辑域名、证书路径和可选IP白名单
nginx -t
systemctl reload nginx

deploy.sh 不会自动覆盖 Nginx 配置、域名和证书。Nginx 首次配置完成后,后续运行 deploy.sh 只需重启其后端 API。

11.4 systemd(可选替代方案)

示例文件:

deploy/systemd/autoscraperx-api.service.example

systemd 配置了进程异常自动重启、SIGTERM 优雅退出和文件句柄上限。它是 deploy.sh + nohup 的替代方案,同一 API 不要同时由两套方式管理。

12. 调度端调用约定

调度端配置:

export CHUI_ZHI_API_URL="https://api.example.com/api/v1/crawler/videos/query"

调度流程:

  1. 把抓取计划转换为 API 的结构化参数。
  2. 请求第一页。
  3. 上传并保存当前页视频。
  4. has_more=true 时原样传递 next_cursor 请求下一页。
  5. has_more=false 时结束。

调度端复用 requests.Session,保持 TCP 连接;每一页处理完成后才请求下一页,避免一次性把全部视频加载到内存。

调度端仍会在入库前检查该计划是否已经存在对应内容,保证任务重试时尽量避免重复上传和保存。

13. 当前性能参考

2026-08-18 使用当前实现实测:

  • 平台:默认两个平台
  • 时间:最近 3 天
  • 关键词:
  • 页大小:500,SQL 实际多取 1 条
  • 三次纯 SQL 耗时:约 4.03 秒、4.22 秒、4.18 秒
  • 平均纯 SQL:约 4.14 秒
  • 首次数据库连接:约 0.15 秒

该结果只代表当时数据库数据量、网络和负载,不是固定 SLA。短关键词 %早% 命中范围较大,通常比长关键词更慢。

建议持续通过 SLS 的 query_duration_ms 观察:

  • 平均查询耗时
  • P95/P99 查询耗时
  • 超时数量
  • 503 并发拒绝数量
  • 各关键词和时间范围的结果量

如果查询长期接近 30 秒,应优先缩小时间范围或页大小,并结合生产数据执行 EXPLAIN。数据库结构优化需单独评估和审批,本接口不会自动执行建索引或字段变更。

14. 代码位置

功能 文件
稳定ASGI启动入口 api/app.py
FastAPI应用、请求上下文、校验错误兜底和生命周期 api/fastapi_app.py
API参数基类、注册、执行、响应、异常及数据库查询保护 api/base.py
本地访问日志、SLS上报、脱敏及日志队列 api/reporting.py
业务Router集中注册 api/routes.py
API公共异常 api/errors.py
垂直视频入参、SQL、查询、分页及Router api/chui_zhi/videos.py
API 和数据库配置 config/base.py
异步 MySQL 连接池 core/base/async_mysql_client.py
阿里云 SLS 批量日志 core/utils/log/aliyun_log.py
启动脚本 run_api.sh
Nginx 示例 deploy/nginx/chui_zhi_api.conf.example
systemd 示例 deploy/systemd/autoscraperx-api.service.example
API 测试 test/test_video_query_api.py

15. 新增接口约定

每个接口模块只管理请求模型和业务实现。请求模型继承 ApiParams,接口继承 BaseApi; 子类只需声明必填的 path 并实现 logic,直接返回业务数据。请求方法默认是 POST,路由名称和说明自动生成。 路由注册、code/msg/data 响应、业务异常、请求ID、本地访问日志和阿里云SLS上报均由基类处理。 数据库列表查询统一使用基类的 fetch_all(),并发限制、查询超时、耗时和结果数量也会自动处理。 参数在进入子类前校验失败、404/405 和健康检查由全局中间件兜底,使用同一套响应和上报逻辑,且不会重复上报。

from fastapi import APIRouter, Request
from api.base import ApiParams, BaseApi

router = APIRouter(prefix='/api/v1/example', tags=['示例'])


class ExampleParams(ApiParams):
    value: str


class ExampleApi(BaseApi):
    """执行示例功能。"""

    path = '/run'

    async def logic(self, params: ExampleParams, request: Request):
        return {'value': params.value}


ExampleApi().register(router)

最后只需在 api/routes.pyinclude_router。子类不要捕获公共异常、不要重复封装 code/msg/data,也不要自行记录或上报访问日志。