返回博客

Dify + Scrapeless:通过自定义工具为您的代理提供实时网页数据

Emily Chen
Emily Chen

Advanced Data Extraction Specialist

20-Aug-2026

TL;DR:

  • Dify 市场中的 Deep SerpApi 插件暴露了一个工具,只有一个参数 query,因此任何需要结果垂直、页面偏移或不同网站的请求必须来自其他地方。
  • 自定义工具是一个 OpenAPI 文件。Dify 将其解析为一个操作 scraperRequest,通过一个端点访问整个 Scrapeless scraper.* 演员系列。
  • 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 放在顶层,旁边是 metadatapaginationsearch_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 Copy
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,并且它接受两个参数: actorinput。这三个命名示例出现在请求构建器中,这样就可以避免手动输入亚马逊 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 Copy
# 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 Copy
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 Copy
{
  "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 被注册为 字符串 参数而不是对象——解析的架构将 actorinput 都报告为 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, positionsnippet_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.amazonaction: product 返回了 2,226,755 字节。解析的产品在 result 下为 4,608 字节,共有 63 个字段;剩余的 1,960,588 字节是列表的原始 html。将整个负载交给模型既昂贵又毫无意义。

在到达模型之前修剪响应

在工具节点之后直接放置一个代码节点。它运行 Python 3 或 JavaScript,将工具输出作为输入变量,并返回一个字典,后续节点通过键读取。选择那里字段没有费用,并保持模型的上下文小:

python Copy
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 以及将什么放入 actorinput。当指令明确提到工具和数据条件时,这样的工作是有效的:

text Copy
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 Copy
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,而不是缺失的容器。启动完整的组合堆栈,而不仅仅是 apiweb,同样的工具在功能上与云端一样。

本指南中的行为是在自托管的 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,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。

最受欢迎的文章

目录