Dify + Scrapeless:通过自定义工具为您的代理提供实时网页数据
Advanced Data Extraction Specialist
TL;DR:
- Dify 市场中的 Deep SerpApi 插件暴露了一个工具,只有一个参数
query,因此任何需要结果垂直、页面偏移或不同网站的请求必须来自其他地方。 - 自定义工具是一个 OpenAPI 文件。Dify 将其解析为一个操作
scraperRequest,通过一个端点访问整个 Scrapelessscraper.*演员系列。 - Dify 预填充两个认证字段,使用该 API 拒绝的值:头名称默认为
Authorization,头前缀默认为Basic。任一默认值返回401与{"code":14404,"message":"invalid access token"}。 - Dify 将嵌套的
input对象标记为 字符串 参数,因此发出 JSON 文本的代码节点是将其构建在工作流中的可靠方式。 - 一个亚马逊产品调用返回约 2.2 MB,其中 1.9 MB 是原始
html。在有效负载到达模型之前,在代码节点中选择result。 - 一个免费的 Scrapeless 账户涵盖本指南中的每个请求。
没有网页工具的 Dify 代理从其模型权重和您上传到其知识库的内容中回答。请求它今天的顶级页面、竞争对手的当前价格或在特定城市运营的水管工,它将生成某种流利且平淡的结果。
Dify 通过工具解决了这一点,有两种方法可以添加一个。此指南涵盖第二种:从 OpenAPI 文件构建的自定义工具,将 Scrapeless Scraping API 变成您工作区中每个代理和工作流中的可调用操作。
自定义工具添加的内容与插件的不同
官方的 Deep SerpApi 列表在 Dify 市场 提供一个工具,只有一个必需的参数 query 和一个 API 密钥的凭证字段。如果您的工作流只需要一个简单的 Google 查询,安装它并停止阅读即可——这是两次点击,它可以工作,而 基于 Dify 构建的商业新闻监测 显示围绕它组装的完整工作流。
其背后的 Scrapeless HTTP 端点接受的请求远不止查询字符串。同一请求形状可以选择当地包而不是网页结果、偏移到这些结果的第二页,或完全切换到亚马逊列表。所有这些都无法通过单个 query 字段访问。
自定义工具填补了这一空白。您粘贴一个 OpenAPI 文档,Dify 从中读取操作,整个演员系列成为一个可附加的工具。无需安装,也无需部署,同一个文件在 Dify Cloud 和自托管实例上均可使用。
抓取 API 返回的内容
一个端点接受每个请求: POST https://api.scrapeless.com/api/v1/scraper/request。请求体包含两个字段—— actor 命名抓取器,input 包含该抓取器的参数。
响应是解析后的 JSON 而不是 HTML。scraper.google.search 调用将 organic_results 放在顶层,旁边是 metadata、pagination 和 search_information。将 tbm: lcl 添加到同一个演员中,将其替换为 local_results.places,这是一组带有评级、电话号码和地址的企业块。scraper.amazon 调用将解析后的产品嵌套在 result 下。
这种单一形状设计使一个 OpenAPI 操作变得足够。每个演员参数的详细信息在 Scraping API 文档 中。
前提条件
- 一个 Dify 工作区 — Cloud 或自托管在 1.0.0 或更高版本。这里描述的行为是在自托管 1.16.1 实例上测量的。
- 来自仪表板的 Scrapeless API 密钥。
- 工作区添加工具的权限。Dify 将自定义工具端点限制为工作区管理员和所有者。
步骤 1:导入 OpenAPI 架构
在 Dify 中,打开 工具 → 自定义 → 创建自定义工具 并粘贴下面的文档。它在 OpenAPI 3.0.3 规范 下有效,这是 Dify 的解析器所期望的版本。
yaml
openapi: 3.0.3
info:
title: Scrapeless Scraper API
version: "1.0.0"
servers:
- url: https://api.scrapeless.com
paths:
/api/v1/scraper/request:
post:
operationId: scraperRequest
summary: Run a scraper actor and return structured data
security:
- ApiTokenAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [actor, input]
properties:
actor:
type: string
description: Which scraper to run.
enum: [scraper.google.search, scraper.amazon]
example: scraper.google.search
input:
type: object
description: Actor parameters. Keys depend on the actor.
additionalProperties: true
examples:
googleSearch:
summary: Google SERP
value:
actor: scraper.google.search
input:
q: web scraping api
googleLocalPack:
summary: Google local pack
value:
actor: scraper.google.search
input:
q: plumbers in Austin, TX
tbm: lcl
amazonProduct:
summary: Amazon product by URL
value:
actor: scraper.amazon
input:
action: product
url: https://www.amazon.com/dp/B09B8V1LZ3
responses:
'200':
description: Parsed result. Shape depends on the actor.
content:
application/json:
schema:
type: object
additionalProperties: true
components:
securitySchemes:
ApiTokenAuth:
type: apiKey
in: header
name: x-api-token
Dify 将其解析为恰好一个工具。名称来自 operationId,因此工具被称为 scraperRequest,并且它接受两个参数: actor 和 input。这三个命名示例出现在请求构建器中,这样就可以避免手动输入亚马逊 URL。
步骤 2:填写所有四个认证字段
选择 API 密钥 认证并设置每个字段。四个字段中的两个预填充了该 API 拒绝的值:
| 字段 | 应设置的内容 | Dify 预填充的内容 |
|---|---|---|
| 认证类型 | API Key (存储为 api_key_header) |
None |
| 头名称 | x-api-token |
Authorization |
| 值 | 您的 Scrapeless API 密钥 | 空 |
| 头前缀 | Custom |
Basic |
前缀字段是最容易让人困惑的。Dify 将其连接到值上,因此将其保留在 Basic 上会发送头 x-api-token: Basic <your-key>。这并不是 基本 HTTP 认证方案 的含义——一个真实的基本凭证是一个 base64 编码的 user:password 对——而 Scrapeless 只需要裸密钥,因此请求会被拒绝。Bearer 也同样失败。只有 Custom 会将值原封不动地传递。
将头名称留在 Authorization 上也以相同的方式失败,原因相同:密钥从未放入 API 所读取的头中。
这两种错误产生相同的响应,您可以在触摸 Dify 之前从终端重现其中任何一个:
bash
# Correct: bare key in x-api-token
curl -s -o /dev/null -w 'bare key -> %{http_code}\n' \
-X POST https://api.scrapeless.com/api/v1/scraper/request \
-H "Content-Type: application/json" \
-H "x-api-token: $SCRAPELESS_API_KEY" \
-d '{"actor":"scraper.google.search","input":{"q":"web scraping api"}}'
# What Dify sends with the default prefix
curl -s -w '\nBasic prefix -> %{http_code}\n' \
-X POST https://api.scrapeless.com/api/v1/scraper/request \
-H "Content-Type: application/json" \
-H "x-api-token: Basic $SCRAPELESS_API_KEY" \
-d '{"actor":"scraper.google.search","input":{"q":"web scraping api"}}'
# What Dify sends with the default header name
curl -s -w '\nAuthorization -> %{http_code}\n' \
-X POST https://api.scrapeless.com/api/v1/scraper/request \
-H "Content-Type: application/json" \
-H "Authorization: $SCRAPELESS_API_KEY" \
-d '{"actor":"scraper.google.search","input":{"q":"web scraping api"}}'
text
bare key -> 200
{"code":14404,"message":"invalid access token"}
Basic prefix -> 401
{"code":14404,"message":"invalid access token"}
Authorization -> 401
这里的 401 是服务器告诉您接收到的凭证不是它接受的,这是 HTTP 语义规范 为此状态保留的内容。响应体进一步缩小了问题:代码 14404 是一个特定于不可用的令牌,而不是格式错误的请求。
步骤 3:运行内置测试
Dify 的测试面板使用您刚刚输入的凭证调用端点。填写两个参数:
json
{
"actor": "scraper.google.search",
"input": "{\"q\": \"web scraping api\"}"
}
一个有效的配置返回大约 15 KB 的 SERP JSON。一个错误的前缀返回一个单一的错误字符串,该字符串逐字传递上游体:Request failed with status code 401 and {"code":14404,"message":"invalid access token"}。
注意测试负载中的引号。Dify 将嵌套的请求主体属性扁平化,因此 input 被注册为 字符串 参数而不是对象——解析的架构将 actor 和 input 都报告为 string,都是必需的。一个真正的 JSON 对象在面板中也能正常工作,因为 Dify 将任一形式规范化为 API 预期的对象。这个转换很重要:手动构建的请求必须发送一个对象,而那里的字符串返回为 400 {"message":"invalid input body"}。
测试返回数据后保存提供者。然后 scraperRequest 会出现在工作区中每个应用的工具列表中。
在免费计划上构建这个? 创建一个 Scrapeless 账户,本指南中的请求以免费配额运行。
返回内容
信封依赖于参与者,每种形状在下游需要不同的处理。
网络搜索。 scraper.google.search 与 {"q": "web scraping api"} 返回了八个 organic_results,响应为 15 KB,此外还有 metadata, pagination, search_information, related_searches 和一个 inline_videos 块。每个结果带有 title, link, snippet, source, position 和 snippet_highlighted_words。
本地包。 添加 tbm: lcl 会将 organic_results 替换为 local_results.places——每个请求 20 家企业。设置 start: 20 会返回下一页;在一个查询的两个连续页面中,40 条记录中的 37 条是不同的,因此存储这两个页面的流程应该基于某种稳定性,而不是假设没有重复。
本地包字段在到达 CRM 或电子表格之前需要清理处理:
phone,type, 和hours带有前导空格,某些小时字符串使用窄的不换行空格而不是正常空格。phone在一次捕获的 20 条记录中有 15 条包含电话号码;其余 记录含有营业时间或服务标签,如Online estimates。place_id,place_id_search,lsig, 和thumbnail在所有 20 条记录中均为空。gps_coordinates存在但读取为{"latitude": 0, "longitude": 0},因此在传递真值检查时没有携带位置。
亚马逊。 scraper.amazon 与 action: product 返回了 2,226,755 字节。解析的产品在 result 下为 4,608 字节,共有 63 个字段;剩余的 1,960,588 字节是列表的原始 html。将整个负载交给模型既昂贵又毫无意义。
在到达模型之前修剪响应
在工具节点之后直接放置一个代码节点。它运行 Python 3 或 JavaScript,将工具输出作为输入变量,并返回一个字典,后续节点通过键读取。选择那里字段没有费用,并保持模型的上下文小:
python
def main(response: dict) -> dict:
places = (response.get("local_results") or {}).get("places") or []
rows = []
for place in places:
contact = (place.get("phone") or "").strip()
digits = sum(character.isdigit() for character in contact)
rows.append({
"name": (place.get("title") or "").strip(),
"category": (place.get("type") or "").strip(),
"rating": place.get("rating"),
"reviews": place.get("reviews") or 0,
"phone": contact if digits >= 10 else None,
"note": None if digits >= 10 else contact,
"address": (place.get("address") or "").strip(),
})
return {"rows": rows, "count": len(rows)}
# Local check against a live response. Leave everything below out of the Code node.
if __name__ == "__main__":
import json, os, urllib.request
body = json.dumps({
"actor": "scraper.google.search",
"input": {"q": "plumbers in Austin, TX", "tbm": "lcl"},
}).encode()
call = urllib.request.Request(
"https://api.scrapeless.com/api/v1/scraper/request",
data=body,
headers={"Content-Type": "application/json",
"x-api-token": os.environ["SCRAPELESS_API_KEY"]},
)
with urllib.request.urlopen(call, timeout=180) as reply:
cleaned = main(json.load(reply))
print(cleaned["count"], "rows")
print(json.dumps(cleaned["rows"][0], ensure_ascii=False))
上述块还可以作为本地检查:在您的环境中使用密钥运行它,它将获取一个实时本地包,应用相同的函数,并打印出第一行清洁记录。请注意,直接调用将 input 作为对象发送——API 返回一个带有 400 {"message":"invalid input body"} 的字符串。Dify 在输出时为您转换字符串形式,这就是为什么相同的值在两个地方都能工作。
清理将 20 条原始记录转换为 20 条可用记录:名称和类别没有多余的空白,当字段包含实际数字时在 phone 中呈现,以及将营业时间文本移动到 note 而不是写入电话列。
对于亚马逊形状,同一个节点是单行代码—— return {"product": response["result"]} ——并且它丢弃了 99% 的有效负载。
将其附加到代理或工作流
两个界面使用相同的保存工具,选择是关于谁选择参数。
在 代理中,模型决定何时调用 scraperRequest 以及将什么放入 actor 和 input。当指令明确提到工具和数据条件时,这样的工作是有效的:
text
When a question depends on current web content, call scraperRequest with
actor "scraper.google.search" and input {"q": "<the search terms>"}, read the
organic_results, and answer from those. Do not answer from memory when the
question is about current prices, rankings, or availability.
在 工作流中,您将 actor 固定在工具节点上,并让上游节点只提供查询。由于 input 是一个字符串参数,可靠的模式是构建 JSON 文本的代码节点:
python
def main(query: str) -> dict:
import json
return {"payload": json.dumps({"q": query, "tbm": "lcl"})}
将 payload 接入工具节点的 input 字段。Dify 自己的 工具文档 更深入地涵盖了周围节点的布线。
如果您自托管 Dify
自托管实例通过专用的 ssrf_proxy 容器路由工具 HTTP,而不是让 API 容器直接访问互联网。当该服务未运行时,工具调用以 DNS 错误失败—— [Errno -3] Temporary failure in name resolution ——这读取起来像是一个损坏的 URL,而不是缺失的容器。启动完整的组合堆栈,而不仅仅是 api 和 web,同样的工具在功能上与云端一样。
本指南中的行为是在自托管的 1.16.1 实例上测量的:模式解析为一个工具,凭证测试返回 15,648 字节的 SERP JSON,前缀为 Custom,以及带有 401 的 @INLINECODE_101@@ 字符串,保存的提供者将 scraperRequest 列为可附加工具。
结论
市场插件覆盖一个查询字符串。自定义工具涵盖其背后的端点,这就是当引导流程开始读取本地包、对其进行分页以及在它们落地之前清理字段时所需的。
设置成本是一个 OpenAPI 文件和四个身份验证字段——其中两个是 Dify 默认填写错误的。将它们填对,每个家庭中的所有参与者都可以在工作区中的每个应用程序中使用,使用一个代码节点进行格式化,保持负载小和列清洁。
准备好接线了吗? 从免费的 Scrapeless 账户开始,获取您的 API 密钥,并将上述模式粘贴到您的工作区中。使用和计划限制在 Scrapeless 定价页面 列出。
常见问题
问:我应该使用 Deep SerpApi 插件还是自定义工具?
当您只需要简单的 Google 查询时,请使用插件——它公开一个具有单个 query 参数的工具,并且只需两次点击即可安装。当您需要本地包、页面偏移、亚马逊列表或任何其他参与者时,请使用自定义工具,因为这些参数无法通过那个单一字段访问。
问:为什么我的 Dify 自定义工具在同一密钥在 curl 中工作时返回 401?
两个 Dify 默认将密钥以 API 无法读取的形式发送。请求头的名称默认是 Authorization 而不是 x-api-token,请求头前缀默认是 Basic,这使得 Dify 发送 x-api-token: Basic <key>。将请求头名称设置为 x-api-token,将前缀设置为 Custom。
问:为什么 input 字段是字符串而不是对象?
Dify 在解析 OpenAPI 文档时会扁平化嵌套请求体属性,因此嵌套对象变成了字符串参数。Dify 接受任一形式并在请求发送之前进行规范化,因此发出 json.dumps(...) 的代码节点是在工作流中构建它的可靠方式。直接调用端点则更严格,需要一个对象。
问:这是否在 Dify Cloud 和自托管上都能工作?
是的。自定义工具是一个 OpenAPI 文档加上凭证,在这两者上都不需要安装。自托管实例有一个额外要求:必须运行 ssrf_proxy 容器,因为工具 HTTP 外发通过它路由。
问:一个请求返回多少个结果?
在本指南使用的捕获中,网络搜索返回了八个自然结果,结果数量因查询而异。本地包每个请求返回20个地点,start: 20获取下一页;一个查询的连续页面略有重叠,因此在写入时去重。
问:如何防止亚马逊的响应淹没模型的上下文?
在工具节点之后选择result的代码节点。产品调用返回了2,226,755字节,其中1,960,588为原始html字段,仅4,608为解析后的产品,因此返回{"product": response["result"]}保留了一切有用信息并丢弃其余部分。
问:一个自定义工具能覆盖多个参与者吗?
可以,这是设计的目的。端点接受actor加上input,因此单个scraperRequest操作可以触及您的账户所有可访问的参与者。在架构中将一个添加到enum使其在请求构建器中可见,而无需第二个工具。
问:API密钥应该放在哪里?
在工具提供者的凭证字段中,Dify将其作为密钥存储,并在调用时注入。将其保留在此,而不是在节点参数中,意味着导出的工作流或重复的应用程序不会携带密钥。
在Scrapeless,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。



