FoxProfile 自动化教程:通过 REST API 管理独立浏览器任务

安装 FoxProfile,创建固定引擎的 Profile,通过认证 API 完成启动、页面查询、结果保存与停止,并解释复用、冲突和持久状态。

亿牛云技术团队2026年10月28日6 分钟阅读

把浏览器任务接入后端时,除了点击和读取,还要明确程序使用哪个 Profile、是否已经被其他任务占用,以及结束后谁负责停止浏览器。FoxProfile 的 REST API 可以把这些动作串起来。

本文建立一个单独的目录采集 Profile,启动后读取图书列表、保存 JSON,最后停止,再用同一个 Profile 重跑。重点是程序化生命周期,不重复界面式多账号管理,也不承诺任何外部检测结果。

准备输入地址

先在浏览器找到下列页面并确认内容,再从地址栏复制完整地址。服务或仓库地址从相应账户设置、官方项目的接入说明或克隆菜单取得。不要从示例输出中反推真实地址。

  • FOXPROFILE_REPO_URL:chienbm98/foxprofile 官方仓库的克隆地址。
  • BOOKS_LIST_URL:Books to Scrape 商品列表页地址。

在执行后续命令的同一个 Bash/WSL 终端中设置:

export FOXPROFILE_REPO_URL='<paste-the-actual-foxprofile-repo-url>'
export BOOKS_LIST_URL='<paste-the-actual-books-list-url>'

下文 JSON 或任务参数中的 <变量名> 是待替换标记,提交前填写刚复制的实际值;JSON 不会自动展开环境变量。结果示例中的 example.16yun.cn 只表示来源字段的形状,不是本文部署的演示站。

安装服务与浏览器引擎

使用 Python 3.12 和 Git。以下命令采用 Bash:

git clone "$FOXPROFILE_REPO_URL" foxprofile
cd foxprofile
python3.12 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
python -m camoufox fetch
export FOXPROFILE_API_TOKEN='replace-with-at-least-24-random-characters'
python -m src.server

这条安装路径使用仓库代码;保留实际版本与依赖信息,升级前对照 API。本文采用项目 2.3 系列的 API 结构。浏览器下载与首次使用的数据准备需要网络,不应把首次准备慢误判成目标页面超时。

服务默认在本机 8000 端口,API 交互文档位于 http://localhost:8000/docs。本文固定使用 Camoufox;Chrome 是另一种引擎,已创建 Profile 的 engine 不能原地更换。安装和端点说明见 FoxProfile 官方仓库。

创建一次,再反复使用

在另一个终端配置相同 token,创建 Profile:

export FOXPROFILE_API_TOKEN='replace-with-the-same-server-token'
curl --fail-with-body   -H "Authorization: Bearer $FOXPROFILE_API_TOKEN"   -H 'Content-Type: application/json'   -X POST http://localhost:8000/api/v1/profiles   -d '{"name":"catalog-demo","engine":"camoufox","os_type":"linux","locale":"en-US","timezone":"UTC"}'

os_type 应与实际运行环境选择一致,示例是 Linux。固定 locale 与 timezone 是为了让本次任务条件明确,不表示所有地区任务都应使用 UTC。

创建成功后不要每次运行再创建同名 Profile。查询现有配置,确认名称和引擎:

curl --fail-with-body   -H "Authorization: Bearer $FOXPROFILE_API_TOKEN"   http://localhost:8000/api/v1/profiles/catalog-demo

Profile 名称标识持久状态,任务运行是暂时租用它。两个并发任务不应同时操纵同一个 Profile 的活动标签;需要并行时分配不同 Profile,或者在调度层保证独占。

用完整客户端串起启动、查询与停止

创建 profile_job.py。只用 Python 标准库,不额外安装 HTTP SDK:

import json
import os
from pathlib import Path
from urllib.request import Request, urlopen
 
BASE = "http://localhost:8000/api/v1"
TOKEN = os.environ["FOXPROFILE_API_TOKEN"]
NAME = "catalog-demo"
 
def api(path, payload=None):
    request = Request(
        BASE + path,
        data=None if payload is None else json.dumps(payload).encode(),
        headers={"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"},
        method="GET" if payload is None else "POST",
    )
    with urlopen(request, timeout=120) as response:
        return json.load(response)
 
running = False
try:
    api(f"/browser/{NAME}/launch", {})
    running = True
    api(f"/browser/{NAME}/page/navigate", {
        "url": os.environ["BOOKS_LIST_URL"],
        "wait_until": "domcontentloaded",
    })
    api(f"/browser/{NAME}/page/wait", {
        "selector": "article.product_pod", "timeout": 15000,
    })
    response = api(f"/browser/{NAME}/page/evaluate", {
        "script": """() => ({
          source_url: location.href,
          rows: [...document.querySelectorAll('article.product_pod')].map(card => ({
            title: card.querySelector('h3 a')?.getAttribute('title'),
            price_display: card.querySelector('.price_color')?.textContent?.trim(),
            detail_url: card.querySelector('h3 a')?.href
          }))
        })""",
    })
    result = response["result"]
    if not result["rows"] or any(
        not row.get("title") or not row.get("price_display")
        for row in result["rows"]
    ):
        raise ValueError("商品列表为空或缺少字段")
    Path("catalog.json").write_text(
        json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8"
    )
    print({"profile": NAME, "records": len(result["rows"])})
finally:
    if running:
        api(f"/browser/{NAME}/stop", {})

执行:

python profile_job.py

launch 默认等待就绪;如果 Profile 已被其他运行占用,应该检查冲突,不接管并停止别人的浏览器。running 只有在当前 launch 成功后才置为真,因此启动冲突不会进入本任务的停止路径。

检查结果与复用效果

catalog.json 保存当前列表 URL 与商品数组。抽查标题是否完整、价格是否对应同一商品、详情链接是否可打开。练习站价格是演示数据;本例没有翻页,不能根据当前文件声称已经取得全站商品。

运行结束后查询 /api/v1/browser/catalog-demo/status,确认停止状态。再执行同一个脚本,应该使用同一个 Profile,而不是新建身份。公开目录没有登录,不能以它证明真实后台 Cookie 永远有效。

需要登录任务时,先在该 Profile 中建立会话,明确何时允许人工重新登录;停止浏览器与删除 Profile 数据不是同一个动作。不要在 finally 中删除 Profile 来“清理”,否则下一轮无法复用它。

持久状态与配置变更

代理、语言和时区属于 Profile 条件,应记录变更理由。依赖固定出口的账号,不在任务中途随机切换代理。引擎需要更换时创建新的 Profile,并建立明确迁移步骤,不能仅 PATCH 一个 engine 字段。

导出 Cookie 与 Profile ZIP 是敏感状态操作。需要备份时按文档选择包含哪些数据,并确认存储位置;不要把它们当普通采集结果一起上传代码仓库。

服务端非本机部署还需要明确认证、网络和 TLS。本文回环地址的使用方式不能直接变成公开服务。

常见问题与处理

launch 返回 409。 查看运行状态与当前使用者。等待合法任务结束或分配另一个 Profile,不通过无限重试抢占活动浏览器。

launch 返回 502。 检查引擎准备、下载、启动环境与服务日志,再检查代理。此时还未进入列表字段提取。

API 返回 401。 核对 server 与客户端使用的 token,并确认 Authorization 是 Bearer 格式,不要把浏览器登录密码当成管理 token。

evaluate 返回结构不符。 查看当前版本 /docs 和页面是否正确。示例使用 script 字段及 result 返回值,不应替换成别的浏览器库的 expression 字段。

结果混入另一个任务的页面。 同一 Profile 的活动标签被并发使用。先解决调度独占,再检查 DOM;加长等待不能保证身份隔离。

脚本异常后浏览器仍在。 查看停止 API 是否成功,按实际状态处理属于本任务的实例。finally 被执行不代表远程停止一定成功。

REST 接入的关键是让 Profile 与运行有清楚边界。用稳定身份保存会话,用独占任务管理一次浏览器操作,结果才能重跑、排错和复用。

需要企业代理方案?

我们可根据目标站点、并发规模与稳定性目标提供定制方案。