TinyFish:网站转结构化API

这个项目始于我无法释怀的一个问题:如果网站不提供 API,我仍然可以将它们视为 API 吗……而无需构建一个在有人移动按钮时就会崩溃的脆弱爬虫?

TinyFish:网站转结构化API
AI编程/Vibe Coding 遇到问题需要帮助的,联系微信 ezpoda,免费咨询。

这个项目始于我无法释怀的一个问题:

如果网站不提供 API,我仍然可以将它们视为 API 吗……而无需构建一个在有人移动按钮时就会崩溃的脆弱爬虫?

这个问题变成了 河豚项目(Project Blowfish):一个小型的"图书最优报价"查找器,它可以并行访问多个荷兰(以及少数国际)书店,规范化结果,并选择最佳匹配。

是的,我的第一个查询是我能想到的最具"Raymon 风格"的东西:

"Harry Potter en de Steen der Wijzen"

因为 "wijs" 在荷兰语中字面意思是"聪明",这正是这里的感觉:一个感觉莫名……智能的小工具

1、什么是 TinyFish

我对 TinyFishMino 的理解很简单:大语言模型是大脑,但网络仍然需要手。

网站是混乱、不一致的,通常对传统自动化充满敌意。TinyFish/Mino 方法(如我所用)是让 Web 交互更具确定性和结构化。我可以描述一个意图并获得干净的 JSON 返回。

这正是为什么 河豚项目(Project Blowfish) 作为一个具有严肃故事的副项目能够成功的原因。我无需维护脆弱的 CSS 选择器或将 HTML 考古学变成我的周末爱好。我给出一个目标,平台在真实网站上执行,然后我保持自己的代码专注于真正重要的事情:

  • 规范化和排名逻辑
  • 安全上限和成本控制
  • 密钥处理和可观察性

如果你曾经构建过爬虫,你就会明白这种痛苦:布局更改,选择器崩溃,突然你的"快速脚本"变成了一个维护产品。我喜欢这里的是可以将网络视为执行表面。我请求结果,接收结构化输出,并将时间花在产品逻辑上,而不是追逐 UI 变化。

从企业的角度来看,价值很直接:这种模式将难以集成的网站转变为具有可预测护栏的自动化表面。将其与 Vault 用于密钥卫生相结合,你就会得到既快速原型化又实际可操作的东西。

更少脆弱脚本更好安全姿态,以及从"很酷的演示"到"这实际上可以运行"的更清晰路径

2、为什么我在河豚项目中集成了 Mino

因为它让我保持在有趣的区域。

我想要:

  • 一个简单的 Node + Express 后端
  • 一个流式传输实时进度的单页 UI(SSE)
  • 针对糟糕情况的强大护栏("哎呀,我刚刚烧掉了积分")
  • 一个干净的存储 API 密钥的地方(Vault KV),但仍然保持 .env 作为默认的"开发模式"
  • 一个微小的"企业触感",使这值得作为博客文章发布:密钥源、可观察性和安全默认值
我希望它变得容易。

不是"三个周末和一个疗程后容易"那种容易。

TinyFish(通过 Mino)使其成为可能。他们的 API 很简单:你提供 URL 和目标,然后获得结构化输出。这种简单性使得将 Blowfish 构建为一个有趣的副项目变得可行,而不是意外发明一个新的爬虫平台。

3、我构建的内容:河豚项目(Project Blowfish)

河豚项目(Project Blowfish) 是图书的"最优报价"聚合器。

河豚项目运行中

它做三件大事:

并行扩展到多个网站

  • 想想 bruna.nl、bol.com 和其他几个网站。

将每个结果规范化为一个一致的架构

  • 标题、URL、ISBN、价格、运费、语言、格式、可用性、状况。

选择最优报价

  • 优先考虑荷兰语、新书、平装本。优先考虑 ISBN 匹配。回退到标题相似度。

除此之外,它会将所有内容实时流式传输到 UI,因此你可以实时看到每个站点、每次尝试正在发生的事情。

🧰 仓库在这里:https://github.com/raymonepping/project_blowfish

4、它是如何工作的

1) UI 启动流式运行

浏览器打开一个 SSE 连接:

  • /api/best-offer/stream?query=...&aggression=...

UI 呈现实时表:

  • 站点状态
  • 尝试次数
  • 最后进度消息
  • 如果提供商发出,可选的"监视"链接

2) 服务器始终强制执行上限

即使 UI 滑块显示"激进",服务器也会对其进行限制。

这很重要,因为网络不是沙箱。并发和重试是"有趣的演示"变成"意外账单"的地方。

因此,Blowfish 强制执行如下硬限制:

  • 每站最大尝试次数
  • 最大全局并发
  • 每站最大并发
  • 每个请求的最大总运行次数

3) 每个站点运行多次尝试,直到一个产生报价

对于每个站点,我并行运行尝试(由每站和全局限制器限制)。一旦一次尝试返回报价,我就中止其他尝试。

这是那些在代码中看起来很小但在实践中感觉很大的事情之一:当你已经有获胜者时,它避免了浪费运行。

4) 结果被规范化和排名

我使用以下方式计算"有效总计":

  • 如果存在,使用列出的总计
  • 否则,如果已知运费,则使用价格 + 运费
  • 否则,仅使用价格(带有警告)

然后我使用以下方式排名:

  • "首选"过滤器(荷兰语、新书、平装本)
  • 如果存在 ISBN,使用 ISBN 分组
  • 否则,使用标题相似度

5) 图书结果,目标达成

图书结果

5、关于隐身的快速诚实的说明

错误界面

在某个时刻,bol.com 礼貌地通知我:

"你的 IP 地址已被封锁。"

那是我的"好吧,有道理"时刻。

这就是 Mino 的隐身执行配置文件变得有意义的地方。这不是关于做见不得人的事。这是关于当网站应用破坏合法自动化流程的自动机器人检测时具有韧性。

话虽如此:

  • 尊重网站条款
  • 速率限制
  • 添加退避
  • 避免猛击
  • 像手术刀而不是链锯一样对待自动化

Blowfish 已经强制执行上限以防止意外"运行风暴"。

6、"免费层级"护栏

我很早就添加的一件事是一组始终在服务器端强制执行的硬上限

在我的 .env 中,我运行:

BLOWFISH_MAX_ATTEMPTS_PER_SITE=2
BLOWFISH_MAX_GLOBAL_CONCURRENCY=2
BLOWFISH_MAX_PER_SITE_CONCURRENCY=2
BLOWFISH_MAX_TOTAL_RUNS_PER_REQUEST=2

这看起来几乎小得可笑……直到你意识到它在做什么:

  • 每站尝试次数使我不会将一个站点重试到天荒地老。
  • 全局并发防止同时跨所有商店的"扩展混乱"。
  • 每站并发避免猛击特定的网上商店。
  • 每个请求的总运行次数是最终的"积分安全捕获",因此一次点击永远不会变成昂贵的惊喜。
有趣的是:架构本身不受限于这些值。

Mino 专为大规模运行 Web 任务而构建,如果你超越业余模式,你可以安全地按数量级调高它。当你的用例和预算合理时,考虑数百数千个并行作业。

我特意保持在"免费层级边界"心态。

不是因为系统无法扩展,而是因为我希望演示是:

  • 便宜运行
  • 难以滥用
  • 安全分享
规模是一个旋钮。护栏是一个设计选择。

7、Vault 角度:你的 API 密钥就是你的信用卡

我不希望 Mino API 密钥硬编码在任何地方。 而且我不希望"把它放在 .env 中"成为安全故事的终点。

在代理工作流程的世界中,API 调用可能会变得昂贵,你的 API 密钥基本上就是你的信用卡。从第一天起就以企业卫生对待它们不是过度设计。这是一个先决条件。

所以我构建了一个简单的开关:

  • 默认:使用 .env
  • 可选:从 Vault KV 拉取 API 密钥
  • 始终:记录正在使用哪个源(不泄露密钥)
密钥源开关

.env 中,我可以这样配置 Vault KV:

# 默认行为是 env
BLOWFISH_SECRET_SOURCE=env

# Vault 行为
VAULT_ADDR=http://127.0.0.1:8200
VAULT_TOKEN=hvs.REDACTED

BLOWFISH_VAULT_KV_MOUNT=kv
BLOWFISH_VAULT_KV_PATH=blowfish
BLOWFISH_VAULT_KV_KEY=mino_api_key

当启用 Vault 时,我会得到干净、令人放心的日志,如:

  • "Using Vault KV for secrets"
  • "Vault secrets loaded"(带有小指纹,不是密钥)

而在 UI 方面,我添加了一个小徽章,以便页面可以显示:

  • Secrets: env
  • Secrets: vault

而不暴露任何敏感信息。

为什么这很重要

这种模式远不止于图书价格:

  • 它将代码凭证分离
  • 它支持轮换而无需代码更改
  • 它使"演示代码"更安全分享
  • 它从"本地原型"到"团队使用"创建了一个清晰的步骤

即使你还没有完全转向 AppRole 或动态身份验证,只需将密钥从 .env 移到 Vault KV 就已经改变了故事。

8、"由 Vault 保护"徽章(不泄露任何内容)

我不希望 UI 尖叫"演示模式"或"Vault 模式",但我确实希望它对读者清楚。

因此 UI 调用一个无害的端点:

  • GET /api/meta
  • 响应:{ "secret_source": "vault" }{ "secret_source": "env" }

这给你一个整洁的徽章,如:

  • "Secrets: vault"
  • 加上 "🔐 Secured by HashiCorp Vault"

它很小,但它做两件事:

  • 它使演示感觉真实
  • 它在不泄露密钥的情况下保持安全故事可见

9、对最终客户意味着什么

这是当我们只谈论技术时经常被忽视的部分。

对于最终客户,这种模式意味着:

  • 更少的手动检查多个站点
  • 更快的比较,更少的错误
  • 即使供应商不提供 API 也能自动化
  • 可预测的成本控制,因为上限在服务器端强制执行
  • 更安全的安全姿态,因为密钥不会散布在笔记本电脑和存储库中

他们不在乎它是 SSE 还是 JSON 规范化。他们在乎它有效,保持可预测,并且不会成为维护陷阱。

10、我遇到的一个有趣问题

在某个时刻,我得到了来自提供者的一堵 HTTP 403 错误墙:

Insufficient credits. You have 0 credits remaining.

烦人?当然。 但也完美。

因为它验证了整个设计:

  • UI 仍然工作
  • 系统仍然流式传输进度
  • 每个站点干净地显示错误
  • 服务器仍然尊重上限
  • 我可以通过切换密钥源安全地翻转 API 密钥

这正是你在构建任何"代理"东西时想要的:可观察和无聊的失败模式。

无聊是好的。无聊才能发布。

11、结束语

这是一个有趣的副项目,但也是一个非常真实的模式:

  • 将网络视为可编程执行表面
  • 保持输出结构化
  • 在服务器端放置安全上限
  • 流式传输进度以实现透明度
  • 将 API 密钥存储在 Vault 中,并使密钥源显式

最重要的是:让它变得容易。

这里的"魔法"不是一个庞大的框架。

它是如此快地从想法工作的 UI,带有干净的 JSON、出色的 UX 和实际上值得发布的安全故事。


原文链接: Stop Writing Brittle Scrapers: Project Blowfish 🐡 with TinyFish Web Agents

汇智网翻译整理,转载请标明出处