首页
/ career-ops 本地解析器指南:让 scan.mjs 以 0 LLM Token 读取公司招聘页

career-ops 本地解析器指南:让 scan.mjs 以 0 LLM Token 读取公司招聘页

2026-09-04 16:47:33作者:宣聪麟

本文为 career-ops 的本地解析器(local parser)机制编写。读完你将掌握:如何在 portals.yml 中配置 scan_method: local_parser 指向自己编写的解析脚本、解析器 stdout 必须遵守的 JSON 契约、{careers_url} / {company} 占位符的安全展开规则、解析失败时的 ATS API 回退逻辑,以及 Agent 扫描模式下 local_parser_ok 如何阻断 Playwright/API 重复抓取以节省 Token。

scan.mjs 是 career-ops 的零 Token 扫描器:它纯靠 HTTP + JSON 抓取招聘职位,不消耗任何 LLM Token。本地解析器是其中的“Nivel 0”能力——当某公司的招聘页是 SSR 或静态 HTML、或有稳定的文档化端点时,与其让 Agent 用 Playwright 打开页面、把整页内容塞进模型上下文,不如写一个本地命令,让它把归一化后的职位 JSON 直接打印到 stdout。解析器以普通子进程运行,scanner 继续沿用同一套标题过滤、去重和 pipeline 输出流程。

何时使用本地解析器

满足以下条件时,适合为某公司写一个本地解析器:

  • 招聘页是 SSR 或静态 HTML,结构稳定,不需要浏览器渲染就能拿到职位列表;
  • 公司提供了文档化的 JSON/端点,本地请求比浏览器自动化更简单可靠;
  • 存在其他确定性数据源,用脚本解析比 Playwright 快照便宜且结果可复现。

两点前提需要注意:

  1. 解析器可以是 JavaScript、Python、shell、Go 或机器上可用的任何可执行文件(具体允许的解释器见下文安全边界一节)。
  2. career-ops 不捆绑任何公司专属解析脚本。仓库只提供执行机制,用户自带脚本,并让 portals.yml 指向它。

scan.mjs 的 Provider 架构中的位置

scan.mjs 启动时自动加载 providers/*.mjs 目录下的所有 provider(_ 前缀的文件视为共享工具,不会被当作 provider,如 providers/ADDING_A_PROVIDER.md 所述)。每个 provider 导出 id、可选的 detect(entry) 和必选的 fetch(entry, ctx)

Provider 的解析顺序在 providers/_registry.mjsresolveProvider 中明确定义,共三级:

  1. 条目显式声明 provider: {id} 时直接命中,跳过 detect()
  2. local-parser 优先于所有 API provider:只要条目配置了 parser.command + script 且能安全解析,就命中它(providers/local-parser.mjsdetect() 会先校验整个调用是否安全,校验不过返回 null 而不是报错);
  3. 其余 provider 的 detect() 按文件加载顺序(字母序)依次尝试,首个命中者胜出。

scan.mjs 在运行摘要中会把本地解析器单独计数,例如 Scanning 5 companies; 2 local parser; 1 skipped — no provider matched(见 scan.mjs)。

Portal 配置:把 parser 指向你的脚本

大多数本地解析器是公司专属的:脚本本身已经知道源 URL、选择器、端点特性、分页和归一化规则。这种常见情况下,scanner 只需要知道要执行哪条命令。templates/portals.example.yml 中的注释块给出的标准写法如下:

tracked_companies:
  - name: Example Company
    careers_url: https://example.com/careers
    scan_method: local_parser
    parser:
      command: node
      script: scripts/parsers/example-company-jobs.js
      format: jobs-json-v1
    enabled: true

同一注释块还给出了 Python 版本:

  - name: Example Python Company
    careers_url: https://example.org/jobs
    scan_method: local_parser
    parser:
      command: python3
      script: scripts/parsers/example_python_company_jobs.py
      format: jobs-json-v1
    enabled: true

参数与占位符展开

args 是可选的,用法完全由解析器作者自定义:让一个脚本复用到多家公司(传 {careers_url}{company})、开调试开关、存 JSON 快照、控制任何脚本专属行为都可以。

执行时 scan.mjs 不经过 shell 展开,直接以参数数组方式调用(execFile,无 shell 插值);{careers_url}{company} 两个占位符会在每个 parser.scriptparser.args 元素中先做替换再执行。展开逻辑与安全校验都实现在 providers/local-parser.mjsexpandParserArg / buildParserArgs 中。

此外还有两个可配置的执行限制(providers/local-parser.mjs):

配置项 默认值 说明
parser.timeout_ms 20000(20 秒) 子进程超时时间
parser.max_buffer_bytes 2000000(2 MB) stdout 缓冲区上限,超出即失败

子进程的 cwd 被固定到 career-ops 项目根目录(providers/local-parser.mjs),这样相对路径的 script 参数与 detect() 阶段校验的是同一个文件,与调用方所在目录无关。

Stdout 契约:三种 JSON 形状

解析器必须向 stdout 打印以下三种 JSON 形状之一(其余输出会被判定为非法 JSON 而失败):

[
  { "title": "Senior AI Engineer", "url": "https://example.com/jobs/123", "location": "Remote" }
]
{
  "jobs": [
    { "title": "Senior AI Engineer", "url": "https://example.com/jobs/123", "location": "Remote" }
  ]
}
{
  "results": [
    { "title": "Senior AI Engineer", "url": "https://example.com/jobs/123", "location": "Remote" }
  ]
}

契约细节(由 providers/local-parser.mjsnormalizeParserJob 实现印证):

  • titleurl 必填;任一缺失的职位行会被静默丢弃(测试用例 tests/providers/local-parser.test.mjs 验证了这一点:fixture 脚本输出的 3 行中 2 行缺字段,最终只保留 1 行);
  • company 可选,省略时 scanner 使用 tracked_companies 条目的 name
  • 相对 URL 会针对 careers_url 解析为绝对 URLnormalizeJobUrl)——绝对 URL 是去重键,这一点在 providers/ADDING_A_PROVIDER.md 的 Job 契约中同样强调;
  • 字段名有一定容错:title 也接受 nameurl 也接受 jobUrl / job_url / applyUrl / apply_urllocation 若是数组会拼成 "Remote, NY" 形式,若是对象则取 nametext 字段(providers/local-parser.mjs)。

仓库自带的 fixture 脚本 tests/providers/_fixture-local-parser.mjs 是一个可以直接参考的最小解析器样例:它根据 process.argv 分支输出标准数组、{jobs: [...]} 信封或非法 JSON,恰好覆盖了契约的三种正例和一种反例。

Token 节省:为什么值得写本地解析器

scan.mjs 的职位发现阶段消耗 0 个 LLM Token:解析器在本地运行,只有归一化后的职位行进入 pipeline。

对比之下,在 Agent 扫描模式(/career-ops scan)下,Playwright 快照和 API 响应会把大体积的页面/JSON 负载送进模型上下文。文档为此定义了一条硬性规则(modes/scan.md):

  • Agent 在内存中维护一个 local_parser_ok 集合,存放 Nivel 0(本地解析器)成功完成的公司名。成功标准:parser.command + parser.script 存在且脚本无致命错误、stdout 是合法 JSON、没有超时或进程崩溃;
  • 对集合中的公司:跳过 Nivel 1(Playwright)——不 browser_navigatecareers_url跳过 Nivel 2(API)——不 WebFetch 其 api: 字段;Nivel 3(WebSearch)只运行通用查询,命中该同公司的结果直接丢弃;
  • 通用门户查询(site:jobs.ashbyhq.com、职位关键词等)照常运行,用于发现 tracked_companies 之外的新雇主;
  • 解析器失败的公司加入 local_parser_ok,Nivel 1/2 照常适用。

推荐的执行顺序(modes/scan.md)是:

  1. Level 0:Local Parser → 有 parser: 配置的公司;构建 local_parser_ok
  2. Level 1:Playwright → 有 careers_url 且不在 local_parser_ok 中的公司;
  3. Level 2:API → 有 api: 且不在 local_parser_ok 中的公司;
  4. Level 3:WebSearch → 所有启用的 search_queries,过滤掉 local_parser_ok 公司的命中。

文档还提到,Cohere + Mobileye fixture 上的实测基准(tiktoken cl100k_base,Playwright vs parser vs API 三档对比表)存放在 feature/local-parser-integration-tests 分支,可用 npm run test:scan-tokens 复跑——具体数值以该分支文档副本为准,当前主线不内嵌这些测量结果。

失败处理:解析器失败不等于公司被丢弃

本地解析器在 ATS API 检测之前运行。如果解析器失败,而该公司的 careers_url(或 api: 字段)能识别出 Greenhouse、Ashby、Lever 等 API 源,scan.mjs 会记录解析器失败,并回退到该公司的 API 路径,而不是把它从本次扫描中丢掉。

这段回退逻辑在 scan.mjs 中:fetch 抛错且 provider 是 local-parser 时,调用 resolveProvider(company, providers, { skipIds: ['local-parser'] }) 重新解析——此时显式跳过 local-parser,让其余 API provider 的 detect() 重新竞争。回退成功则用 api 源抓取,并向运行摘要的 errors 列表写入 local parser failed, used API fallback: <原因>;回退也不可用(skipIds 后没有 provider 命中)才把原始解析器错误抛出。Agent 侧的规则与此一致:解析器失败的公司不进入 local_parser_ok,走 Playwright/API 兜底(modes/scan.md)。

产物存储约定

Scanner 只需要 stdout。如果解析器额外写出完整 JSON 快照(用于调试或审计),约定存放在 data/parser-output/{company}/ 下。这些生成的 JSON 产物必须保持在 git 之外.gitkeep 占位文件是唯一允许提交的例外,用于保留目录结构(modes/scan.md 与 cookbook 的 “Artifact Storage” 一节一致)。

安全边界:career-ops 如何防住不可信配置

parser.command / parser.script 来自 portals.yml,而共享或模板化的配置不能视为完全可信。providers/local-parser.mjs 的注释明确说明防御目标:命令绝不能是 rmcurl 之类的任意二进制。实现上的规则包括:

  • 解释器白名单python3pythonnodedenobunshbashALLOWED_INTERPRETERS);不在白名单内的 command 必须是位于项目根目录内的文件(resolveInsideRootrealpathSync 解析后校验不逃逸出项目树);
  • 脚本必须在仓库内:白名单解释器的第一个参数必须是一个仓库内脚本,且 args[0] 恰好等于它——这是为了堵死 node --eval / python -c 这类内联代码执行;
  • 占位符注入防护{careers_url} 展开前必须能解析为合法 URL 且协议为 http:/https:{company} 展开后不允许以 - 开头(防止被解析为 CLI flag,即 argument injection)。由于 execFile 原样传参(无 shell),这是仅剩的注入面,见 safeCareersUrl / safeCompany
  • detect 即校验detect() 内部调用 resolveInvocation,任何无法安全解析的调用(未知命令、仓库外脚本、内联代码 flag)都直接返回 null 跳过,而不是带着风险去执行。

这些规则有专门的测试覆盖:拒绝无 script 的解释器命令、拒绝 ../escaped 路径、拒绝以连字符开头的公司名、拒绝非 http(s) 的 careers_url,全部在 tests/providers/local-parser.test.mjs 中。

动手验证

不用写真实爬虫,仓库自带 fixture 就能验证整套链路(配置解析 → 子进程执行 → 占位符展开 → JSON 解析 → 归一化 → 丢弃非法行):

  1. 查看 fixture 脚本:tests/providers/_fixture-local-parser.mjs,它按 echo / envelope-jobs / invalid 参数分支输出三种载荷;
  2. 运行 provider 测试:node tests/providers/local-parser.test.mjs,通过 pass/fail 断言逐条验证 detect() 的正反例、{company}/{careers_url} 插值(echo Company: Acme Corp URL: https://example.com/acme)、相对 URL 解析(/job2https://example.com/job2)和非法 JSON 的报错;
  3. 对照 docs/local-parser-cookbook.md 复核 stdout 契约,再参考 providers/ADDING_A_PROVIDER.md 了解若你想把某个 JSON 端点直接做成内置 provider(而非本地脚本)时 Job 契约的完整要求。

写解析脚本时的最小清单:command 用白名单解释器;script 用相对仓库根的路径且在根内;只向 stdout 打印 JSON(日志走 stderr 或文件);每行保证 title + 绝对或可解析的相对 url;需要多家公司复用时通过 args 里的 {careers_url} / {company} 传参,而不是在脚本里硬编码。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
docsdocs
暂无描述
Markdown
889
5.78 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.17 K
341