FastAPI + Playwright(headed Chrome/xvfb 过 Cloudflare)抓内容详情页元数据; 可选把 m3u8 视频转 mp4 + 封面上传对象存储(配置由调用方下发,本服务不持密钥); 走主站 lobster 执行器契约(invoke/poll),回传 ResourceIngestItem 形式的 resources。 当前内置 xchina.co 的 scraper(core/scraper.py)。
6.3 KiB
6.3 KiB
content-harvester
内容采集服务(FastAPI + Playwright)。给一个来源页 URL,用有头 Chrome(容器内靠 Xvfb,能过 Cloudflare)逐个打开内容详情页,抓出每条内容的 标题 / 详情页 URL / 封面图 / 媒体地址;如果调用方下发了对象存储(MinIO/S3)配置,就把媒体(目前是 m3u8 视频)拉成 mp4、连同封面一起上传,把 video_url/cover_image 换成对象存储上的稳定地址(很多站点的媒体地址是带签名的短时 URL)。结果以 resources 列表回传。
当前内置一个 xchina.co 的 scraper(core/scraper.py);以后接别的来源就再加一个 scraper 模块。
设计文档见 PRD-minio-upload.md,实现细节见 CLAUDE.md。
用法定位
对接「启源 AI 平台」主站的 lobster 执行器:主站「多内容数据采集」任务里选「来源大类 = 内容网站」时,把作业 POST 给本服务,本服务返回 resources,主站写进资源库。
HTTP 接口
POST /agents/{agent_id}/invoke— body 是主站的调用载荷:content_url(必填,来源页/详情页 URL)、platform_name(可选 →source_name,默认xchina)、source_kind(可选,非website直接 400)、start_page/end_page(可选,默认只爬content_url这一页/这一条)、minio_endpoint/minio_access_key/minio_secret_key/minio_bucket(可选,主站把对应 Skill 变量并进来)。立即返回{"task_id":"...","status":"running"},作业后台跑。GET /tasks/{task_id}—{"status":"running"|"succeeded"|"failed","output":{...}|null,"error":str|null,"log_tail":[...]}。- 成功时
output = {"resources":[...], "pages_scanned":n, "item_errors":[{"source_url","reason"},...]}。resources每项字段对齐主站ResourceIngestItem:source_type:"video" / source_name / source_url / content_type:"video" / title / cover_image / video_url / extra_data。 extra_data含referer / page_no / collector / video_id;上传成功还有original_m3u8 / original_cover / minio_object_video / minio_object_cover / video_bytes;没上传则有upload_skipped_reason(如minio config absent/download failed: .../video too large: ...)。
- 成功时
POST /tasks/{task_id}/cancel— 标记取消(仅 running)。GET /healthz— 健康检查。
鉴权:设了 HARVESTER_TOKEN 则要求 Authorization: Bearer <token>(= 主站 Skill 的 lobster_api_key);没设则不鉴权(仅内网/本地)。
并发:进程内单作业串行(防风控)。作业状态只在内存,重启即丢。
环境变量
| 变量 | 默认 | 说明 |
|---|---|---|
HARVESTER_TOKEN |
空 | Bearer token;空=不鉴权 |
HEADLESS |
0 |
0=有头(容器内靠 Xvfb);过 Cloudflare 必须;1=无头(本地调试,过不了详情页) |
BROWSER_CHANNEL |
空 | x86_64 服务器可设 chrome(需 Dockerfile 里 playwright install chromium chrome);arm64 无 Chrome 包 |
BROWSER_PROFILE_DIR |
/data/browser_profile |
持久浏览器资料目录(挂卷;可预置过了 Cloudflare 的 cookie) |
ALLOWED_HOSTS |
xchina.co |
content_url host 白名单(SSRF) |
MAX_PAGES |
50 |
单作业最多翻页数 |
CONCURRENT_FRAGMENTS |
8 |
yt-dlp 并发分片数 |
MAX_VIDEO_MB |
0 |
单视频大小上限(0=不限),超过则跳过上传、降级保留原媒体地址 |
TMP_DIR |
/tmp |
临时下载目录(<TMP_DIR>/content-harvester/<jobid>/,用完即删) |
MINIO_ENDPOINT / MINIO_ACCESS_KEY / MINIO_SECRET_KEY / MINIO_BUCKET |
空 | 仅作 payload 缺失时的兜底;生产默认不配,由主站下发 |
本地跑
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
python -m playwright install chromium
# 本地没 Xvfb 用 HEADLESS=1(但详情页会被 Cloudflare 拦)
HEADLESS=1 uvicorn server.app:app --reload # http://localhost:8000
curl -s -X POST localhost:8000/agents/xchina/invoke -H 'Content-Type: application/json' \
-d '{"source_kind":"website","content_url":"https://xchina.co/videos/series-XXXX.html","minio_endpoint":"<MINIO_ENDPOINT>","minio_access_key":"<KEY>","minio_secret_key":"<SECRET>","minio_bucket":"<BUCKET>"}'
curl -s localhost:8000/tasks/<task_id>
Docker(推荐)
docker build -t content-harvester .
docker run -d --name content-harvester -p 8000:8000 \
-e HARVESTER_TOKEN=xxxxx \
-v harvester_profile:/data/browser_profile \
--memory=2g --cpus=2 \
content-harvester
镜像里已装 xvfb(跑有头浏览器)+ ffmpeg(yt-dlp 合并 m3u8 分片)。部署到主站所在 VPS 时,把这个容器加进主站 docker compose 同一网络,主站 Skill 的 lobster_api_url 填 http://content-harvester:8000。
主站侧对接
- 「Skill 管理」新建/编辑那个采集 Skill:
executor_type=LOBSTER、skill_category=CRAWL、lobster_api_url=http://content-harvester:8000、lobster_api_key=<HARVESTER_TOKEN>、lobster_agent_id=xchina(随意);input_schema含source_kind字段(前端才渲染采集表单)+ 声明content_url(必填),可选start_page/end_page;变量关联里勾上minio_endpoint/minio_access_key/minio_secret_key/minio_bucket这 4 个 Skill 变量(它们的实际值由运维在主站配置,本服务不持有任何密钥)。 - 主站后端配套改动:非批次 lobster 改后台跑、成功后自动
_maybe_ingest_to_resource_pool、ingest 兼容output.resources、_dispatch_lobster把 Skill 变量并入 payload、crawl 类用CRAWL_LOBSTER_TIMEOUT(env,默认 7200s)。 - 创建任务:选该 Skill → 来源大类「内容网站」→「地址」填来源页/详情页 URL → 设可见性/去重/入库后状态 → 提交。任务成功后资源库出现这些内容,
video_url指向对象存储,长期有效。
反爬 / 限制
- 详情页常被 Cloudflare 挡,只有真实有头浏览器(
HEADLESS=0+ Xvfb,或真实 Chrome)才能自动过 JS 质询;_assert_not_blocked会等质询自动放行(最多 ~24s),仍卡住则该条进item_errors。站点策略变化可能导致采集失效,退路是预置一份过了验证的browser_profile卷。 - 下载+转码+上传慢:每条
15s 抓 + 几十秒几分钟下载 + 上传;多页/大量视频会很慢,属预期。 - 任一条目下载/上传失败 → 该条降级:
video_url保留原始媒体地址(extra_data.upload_skipped_reason记原因),不让整作业白跑。