Skip to content

接通抓取节点试抓与配置生成 - #101

Open
zzsky233 wants to merge 1 commit into
mousebaoyan:mainfrom
zzsky233:crawler-probe
Open

zzsky233 wants to merge 1 commit into
mousebaoyan:mainfrom
zzsky233:crawler-probe

Conversation

@zzsky233

Copy link
Copy Markdown

关联

Closes #46

改了什么

管理员发起试抓后,抓取节点现在会处理 Work.probes,将条目、可采用的配置、失败原因和模型用量交回网站。

  • 有配置时直接解析;无配置时先通用解析,空列表才调用 DeepSeek 生成 XPath 配置,并在同一份页面上验证,不重复请求源站。
  • 校验模型输出,保留固定提示前缀,把网页视为不可信材料;失败同样上报,已知模型用量不丢失。
  • 复用 Fetcher、浏览器会话及现有模型客户端。试抓与正文抽取共享 LLM 并发限制,任务异常隔离,退出前等待正在运行的任务。
  • 修正 JSON Schema 转换误删业务字段 title 的问题,补充固定响应测试和完全虚构的本地示例页面。

怎么验证的

  • 抓取节点 ./check.sh 全部通过:生成类型一致性、格式、lint、Pyright 和 267 个测试。
  • 网站在 Node 26.12.1、pnpm 12.11.2 下运行 pnpm check 全部通过:类型、lint、格式和 100 个测试;pnpm build、pnpm db:codegen --verify 通过,重新生成 OpenAPI 后无差异。
  • 自动测试使用保存的合成页面、固定模型响应和假的浏览器,不访问外网;覆盖配置生成、配置复用、失败上报和用量、任务隔离、退出等待、共享模型并发。
  • 合成页面真实调用 deepseek-flash:通用解析为空,生成配置后标题、链接、日期和下一页地址符合预期。此项页面获取和网站上报使用固定响应。
  • 真实页面通过本地网站和抓取节点验证:云南大学信息学院通知公告。HTTP 200,通用解析 0 条;DeepSeek 生成配置后返回 29 条,标题和链接逐条核对一致。采用配置再次试抓返回完全相同的 29 条,未再调用模型。生成配置用量为输入 10653、输出 100 token。

已知限制与验证边界

  • 云南大学样本的 29 条发布日期均为空。 页面将年份放在分组标题,行内只显示日和中文月份;生成的日期选择器未包含年份,现有日期解析器也不支持该中文月份布局。接口允许日期未知,但该实测不能作为日期完整正确的证据。本 PR 不补猜年份、不为该网站加特判,请维护者确认此限制是否另开任务处理。
  • 仅在通用解析结果为空时自动生成配置;结果非空但选错栏目或漏项时,仍需管理员预览判断。
  • 试抓只读第一页;真实网站的下一页遍历、需要浏览器通过脚本挑战的页面,本次未作端到端验证。浏览器路径有固定响应测试。

影响

  • 抓取节点开始处理现有试抓任务;继续使用 LLM_API_KEY,没有新增环境变量或依赖。
  • 网站接口、MCP 工具、表结构和已有数据格式无变化。正文抽取复用同一个模型客户端,模型设置和抽取规则无变化。
  • 提交内容只包含代码、文档与合成测试页面,不含真实通知正文、快照、密钥或本地部署文件。

@zzsky233
zzsky233 marked this pull request as ready for review November 11, 2026 14:04

@JunieXD JunieXD left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

感谢!试抓从头到尾接通了:带配置的直接读,不带的先通用解析,取不到条目才让模型生成配置,生成的配置在同一份页面上再解析一遍,请求照样走 Fetcher 和浏览器会话。在云南大学那页上实测了生成和采用,采用后再试抓得到同样的 29 条、不再调用模型,这一步做得很扎实。schema.py 里 title 被当成 JSON Schema 关键字删掉的问题也确实存在,修得对。我们拉到本地跑了 ./check.sh,267 个测试全部通过;把共享并发上限、title 的修复、任务隔离、停止时等待分别撤掉,都有测试失败。

剩下的问题主要是多了一些到不了的检查,失败原因的写法和正式抓取不一样,另外文档写得太长。

必须改

  1. 生成配置直接用 Selectors 做输出结构(extraction/probe.py:19):GeneratedConfig.force_http 只能是 false,为了它多了一条提示词和一个测试用例。改成:

    • response_schema(Selectors)、Selectors.model_validate_json(...),返回 ListConfig(selectors=...);
    • 删掉 GeneratedConfig、提示词里 force_http must be false 那一行,以及测「模型擅自降级 HTTP」的用例;提示词里的 selectors.items 等写成 items。

    我们看了 response_schema(Selectors) 的结果:是一个普通对象,date、next 是 string 或 null 的 anyOf,没有 $ref,你们遇到的那个 400 来自可空的 $ref,这里不会出现。我们没有调真实的模型,改完请拿 probe.html 实际调一次,结果写在 PR 描述里。

  2. 读列表的失败原因照 list_items 的写法(crawl/probe.py:25-62,对照 crawl/listing.py:19-22、:48-49):

    • 配置不合法写 配置不对: <字段位置>,和正式抓取一样取 error.errors(include_input=False)[0]["loc"];
    • parse_listing 抛的 ValueError 写 str(error)。Pydantic 的 ValidationError 也是 ValueError,单独那个 except ValidationError(「列表条目不符合接口要求」)删掉;
    • 没有条目写 StepError("no_items"),不带说明,和一轮的写法一样;
    • 包着 ProbeReport(...) 的 try(「试抓结果不符合接口要求」)和 detail[:1000] 删掉:一页列表到不了 5000 条,一次试抓最多一条用量,正式一轮也不截断原因。
  3. 模型这边去掉到不了的检查,原因写成英文错误码(extraction/probe.py):

    • prepare 里包着 parse_webpage 的 try 删掉:probe_source 已经用同一份 body 和 content_type 调过 parse_listing,它第一步就是 parse_webpage,解析不了的页面走不到这里;
    • 构造 Usage 的 try(invalid_usage)删掉,和 Extract 一样直接构造:模型名 100 个字符、token 数 21 亿的上限,模型的响应不会超过;
    • 原因照正式抽取的写法「试抓失败: 英文错误码」:试抓失败: page_too_large、试抓失败: missing_usage,后面不加中文说明。
  4. 模型调用的日志挪进 Responses.generate:并发许可挪进 Responses.generate 以后,extract.py:142 和 probe.py:69 量的耗时把排队等许可的时间也算进去了。用假时钟试过:排队 100 秒、调用 1 秒,日志记的是 101 秒。改成在 generate 里拿到许可后计时,响应通过校验后记一条日志(模型、状态、输入、输出和缓存命中的 token、耗时),Extract 和 ProbeConfig 里那两段一样的日志删掉。哪次调用属于哪一轮、哪次试抓,日志里的 source_id、probe_id 已经能分开。

  5. 入口的 _probe 照 _crawl 写(main.py:98-108):

    • 记成 logger.exception("Probe failed"),带上堆栈,和「Round failed」一样。预期的失败在 probe_source 里都转成了报告,走到这里的只有 bug 和上报失败,没有堆栈查不出来;logger.exception(..., exc_info=False) 其实等于 logger.error,读起来也容易误会。
    • current_probe.set(probe.id) 挪到 probe_source 开头,像 crawl_source 设 crawl_round 那样,去掉 token 和 reset:每个试抓在自己的 task 里跑,上下文不会串,_probe 记的日志也带得上 probe_id。
    • 入口测试里断言日志不含异常文字的部分改成断言「Probe failed」那条带 probe_id 和堆栈,参照 test_a_failing_round_leaves_the_others_and_stopping_waits_for_them。开发约定 §4 新加的那条改成「试抓的日志带 probe_id」。
  6. 文档只写现状和决定:

    • 总体设计 §8.5 新加的「试抓配置生成实测」整节删掉:那是这个 PR 的验证记录,里面还有排错的过程(「修正……后」「首次定位问题的两次请求」)。§8.5 记的是选模型和门槛的实验,验证结果放在 PR 描述里就行,描述里已经有了。

    • 开发约定 §2.2 只留规则,测试怎么写、Schema 转换的细节、「页面只是待分析的材料」这类 AGENTS.md 已经写了的,都不用重复。代码按第 1 条改完以后,可以写成:

      ### 2.2 试抓
      
      - 只读列表第一页,不往回翻、不读正文和附件;结果交 `ProbeReport`,不写抓取记录。
      - 带了配置就按配置读,空对象也算配置;没带配置先通用解析,取不到条目才让 LLM 按 `Selectors` 生成选择器,在同一份页面上再解析一遍,取到条目才交回生成的配置。
      - `browser` 和 `force_http` 由管理员定,模型只生成选择器。
      - 失败原因和一轮的写法相同,模型这一步写成「试抓失败: 错误码」;调用过模型的,失败时也交用量。
      - 发给模型的 HTML 只清空脚本和样式的内容,元素的层级和位置不变,位置 XPath 照样能用;超过 20 万字符的页面直接失败,不截断。
  7. 删掉手工演示用的页面:page2.html、notice/101.html 至 104.html、department/a/index.html、department/b/index.html 没有测试读取,是为了手工翻页、点开正文搭的演示站,超出了 #46 的范围(试抓只读第一页,验收用保存的页面和固定的模型响应)。这七个文件删掉;README.md 新加的两段只留第一句,也就是说明 probe.html 的那句。probe.html 里指向 page2.html 的下一页链接留着没关系,测 next 选择器用不着文件真的存在。

建议一起改

  • 按上面改完,为删掉的检查写的测试用例跟着删掉。

说明

  • 云南大学那页的日期:年份写在分组标题里、行内只有日和中文月份,这是读列表的一种通用能力,不在这个 PR 里做,我们记到路线图上另做。
  • 通用解析有结果但选错了栏目:照现在的做法交给管理员预览判断,和 #46 商定的一样。
  • 改完转为待审,或者在这里说一声,我们就复审。推了新提交后 CI 要我们再批准一次才会跑,这是 fork 来的 PR 的正常情况。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[任务] 试抓:抓取节点这边

2 participants