mcpField
S
scrape

scrape

github.com/madslorentzen生产力67.9k206 次安装3 天前更新
代码仓库

关于 scrape

Job Scraper 是一项通过已安装的职位门户搜索 CLI 查找岗位的技能,并可回退到 WebSearch,随后去重并快速评估新匹配职位。它面向跟踪求职机会的人,支持定向搜索、广泛搜索和门户健康检查。

主要功能

  1. 01通过已安装的门户 CLI 搜索
  2. 02支持 WebSearch 回退
  3. 03与已见职位及跟踪表去重
  4. 04快速评估新职位匹配度
  5. 05支持定向、广泛及健康检查模式

适用场景

  1. 01查找特定领域近期职位
  2. 02检查多个职位门户的新岗位
  3. 03避免重复查看已跟踪职位

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/madslorentzen/ai-job-search --skill scrape

安装后在 Claude Code 中输入/scrape确认已加载。

附带文件search-queries.md

详细介绍

职位抓取器


工作原理

本技能使用 .agents/skills/ 中已安装的门户搜索 CLI(外加 WebSearch 作为回退)搜索招聘门户,查询词来自你的个人资料。 它会针对此前见过的职位以及申请跟踪器进行去重,并展示新的匹配结果,同时给出快速匹配度评估。

调用方式

用户通过如下说法触发本技能:

  • “查找新职位”
  • “抓取职位”
  • “有新职位吗?”
  • “/scrape”

可选参数:

  • 聚焦领域,例如 “/scrape data science” 或 “/scrape geophysics”
  • “broad” 表示运行所有搜索类别,例如 “/scrape broad”
  • “health” 表示仅运行门户健康检查(步骤 4.75),不进行搜索、去重或展示职位——例如 “/scrape health”,或 “/scrape health jobnet” 用于探测单个门户,即使该门户已被禁用

执行步骤

步骤 0:加载状态
  1. 读取 job_scraper/seen_jobs.json(若不存在则创建——以 {"seen": {}} 开头)
  2. 读取 job_search_tracker.csv,提取已申请的公司 + 职位
  3. 读取 search-queries.md(本目录)以获取搜索策略
步骤 1:搜索

读取 search-queries.md(本目录)以获取搜索策略。默认运行优先级最高的 3 个查询类别。如果用户说了 “broad”,则运行所有类别。如果用户指定了聚焦领域(例如 “data science”),则优先使用该类别中的查询。

将已安装的 CLI 工具作为主要搜索机制。 仅对没有 CLI 技能的门户,或系统中 bun 不可用时,才回退到 WebSearch。

1a. 检查 bun 是否可用
bun --version

如果此操作失败(未安装 bun),则对所有门户跳到 1c(WebSearch 回退),并在步骤 5 的输出中注明使用了回退。

1b. 运行 CLI 工具(主要方式——尽可能并行运行这些工具)

通过读取 .agents/skills/*/SKILL.md 下找到的每个 SKILL.md,发现所有已安装的门户 CLI 技能。每个文件都记录了该门户确切的 CLI 参数和用法示例。使用每个门户自己记录的接口——不要猜测参数。 这种方式会自动包含任何通过 /add-portal 新增的门户,而无需修改本文件。

遵守 enabled 开关。 除非某个门户的 SKILL.md frontmatter 设置了 enabled: false,否则该门户即为启用(缺少该键表示启用——这是默认值)。跳过每个已禁用的门户,并将其记录下来以供步骤 5 汇总。因此,fork 可以保留某个门户的安装状态但在某次运行中不参与,而无需删除其目录。

对于每个已启用的门户技能:

  1. 读取其 SKILL.md,找到正确的 bun run … 调用方式及支持的参数。
  2. 将 search-queries.md 中的查询词转换为该门户的参数格式(例如 --key、--search-string、--query、过滤代码——以该门户的 SKILL.md 所指定的为准)。
  3. 使用该门户支持的时效性过滤参数(--jobage、--since <YYYY-MM-DD> 等——以每个门户记录的方式为准)将范围限定为最近 14 天。没有时效性参数的门户(jobdanmark 不提供任何此类参数)同样要限定范围:每个门户的搜索结果都带有 date 字段,因此在客户端侧过滤——在调用返回后丢弃 date 早于 14 天的结果,且绝不要臆造该门户的 SKILL.md 未记录的参数(这些 CLI 会拒绝未知参数)。--order PublicationDate 是排序,而排序不是过滤——在没有任何更好选择的门户(jobnet)上,将其与 --limit 搭配是一种说得过去的近似做法,但同样要在其上应用客户端侧日期过滤。
  4. 使用门户的限制参数将每次调用的结果上限设为约 20 条。
  5. 使用 --format json 以获取机器可读的输出。

尽可能使用 Agent 工具并行运行所有门户 CLI 调用。将所有 results 数组收集到同一个池中供步骤 2 使用,并让每个结果都标记其来源门户技能(供步骤 2 的 detail 查询使用)。

如果某个 CLI 工具以非零代码退出,则记录错误信息并继续——不要中止整个搜索。

1c. WebSearch 回退

在以下情况下使用 WebSearch:

  • search-queries.md 中列出的门户,在 .agents/skills/ 下没有对应目录
  • 任一门户的 CLI 在运行时失败
  • bun 不可用(Step 1a 失败)

对这些门户,直接将 search-queries.md 中针对各网站的查询字符串用作 WebSearch 查询。

将每个回退结果标记为来源于 WebSearch;若回退是为了替代一个已安装但 CLI 运行失败的门户,还要保留该门户标签。Step 4 会将此信息保存为条目的 source,Step 5 会报告本次运行中哪些门户使用了回退。

Step 2:获取并解析

针对 Step 1 中每个有希望的结果:

来自 CLI 的结果: 搜索输出已包含职位名称、公司、地点、日期和 URL。对于值得深入了解的职位,使用该门户的 detail 命令获取完整详情(参见其 SKILL.md——不要猜测标志),以提取关键要求、申请截止日期和简短描述摘要。

检测来源处已关闭的职位: linkedin-search detail 也会返回 isActive。false 表示职位发布页面本身显示 LinkedIn 的“已不再接受申请”横幅——职位可能在被编入索引后、获取前已失效(过期的 LinkedIn URL 会重定向到类似的有效职位,因此搜索结果可能是幽灵职位)。标记此类职位,绝不要悄悄丢弃:在 Step 4 中将其条目写入 job_scraper/seen_jobs.json,并设置 "status": "expired",同时不将其纳入 Step 5 的展示——缺少条目看起来与从未见过该职位没有区别,而记录的状态能让之后的幽灵职位报告自行说明情况。isActive: true 仅表示没有该横幅,并不能证明职位仍在招聘;截止日期和失效 URL 仍由 /rank 处理。

来自 WebSearch 的结果: 对职位发布 URL 使用 WebFetch,手动提取相同字段。如果返回 HTTP 403,放弃前请按照 .claude/skills/job-application-assistant/09-web-research.md,通过 curl 使用浏览器标头重试——大多数银行和企业网站会拒绝 WebFetch 的用户代理,但能正常向浏览器提供内容。

保存实际能打开职位发布页面的 URL。 在职位列表页面 URL 后附加 #fragment(.../jobs/ciso/#ikerian)并不是职位发布页面:它能正常获取,却会返回无关的职位名称,导致之后每次 /rank 和 /apply 运行处理该条目时都失败。如果 WebSearch 只找到列表页面,请在雇主自己的招聘网站上搜索该职位,并保存对应 URL;或者放弃该候选职位,不要保存带片段的链接。

对每个候选职位:

  • 如果 URL 与 job_scraper/seen_jobs.json 中任一现有条目匹配,无论该条目使用什么键,都跳过。这样可以确保去重连续性:此前保存的职位使用的是辅助程序之前的键规则,而新条目使用 Step 4 中的规范键。
  • 否则,如果 job_scraper/seen_jobs.json 中已存在相同的公司+职位名称组合,则跳过
  • 如果 job_search_tracker.csv 中已出现相同的公司+职位,则跳过
Step 2.5:批量发布检测(在本次运行中)

这是一种值得向用户标记为提醒信号的分发模式,不是对雇主的指控——它描述的是职位列表的分发方式,并非对公司是否合法的判定。它本身不能证明存在问题(公司确实可能在多个城市招聘相同职位);请将其标记出来,让用户在决定是否投入时间时纳入考虑,不要因此降低匹配度,也不要悄悄排除该结果。

如果本次运行结果池中有两个或更多结果(来自同一家公司,或 URL 或职位名称中显示的 req/job ID 相同),其描述基本相同,只在城市/地点/职位名称上有所不同,则不要将它们作为单独的行展示。合并为一行,并注明分布范围,例如“在 6 个城市以相同内容发布(BR、MX、GT)”。

第 3 步:快速匹配度评估

对于每个新职位,快速检查匹配度(不是 04-job-evaluation.md 中的完整评估,只需快速判断):

  • 高度匹配:职位工作内容直接涉及你的核心技能
  • 中度匹配:职位与您的经验相近
  • 低度匹配:职位要求你缺少的重要技能

语言覆盖规则: 分配匹配等级之前,先根据 04-job-evaluation.md 的 Language Gate 检查职位信息(是否要求你完全未在 CLAUDE.md 的 Languages 表格中声明的语言)。如果职位要求的语言完全未声明,则无论技能匹配度如何,都应覆盖技能匹配结果,将职位标为 Low,并在要点中写明该语言,避免它被埋没在表面上看起来不错的匹配结果中。若你已声明该语言,但职位要求高于你声明的水平,这不构成覆盖规则——正常评估匹配度,但要在该职位的亮点(第 5 步)中添加一条红色警示要点,引用职位要求,并注明你声明的水平,让差距清晰可见,但不自动降低匹配等级。

第 4 步:去重并存储
  1. 使用辅助脚本生成每条记录的键,切勿临时将名称转换为 slug:
python3 tools/job_key.py --company "<company>" --title "<title>" --url "<url>"

它会输出一行:该职位信息的规范键。该键必须是职位信息的纯函数,因为两次运行若生成不同的 slug,就会把同一职位存储两次,违背本步骤去重的目的。辅助脚本还会限制过长标题的长度,并使用完整 slug 的哈希值消除截断歧义,因此截断后的标题在多次运行中保持稳定,两个不同的长标题也不会发生冲突。python3 tools/job_key.py --audit 会报告现有状态文件中不符合此规则的记录;它只会报告,不会重写键,因为重写后的键会破坏追踪器自身的公司与职位匹配逻辑。

  1. 将所有抓取到的职位(新增和跳过的)添加到 job_scraper/seen_jobs.json,结构如下:
{
  "seen": {
    "<key from tools/job_key.py>": {
      "title": "...",
      "company": "...",
      "url": "...",
      "first_seen": "YYYY-MM-DD",
      "posted_date": "YYYY-MM-DD" | null,
      "deadline": "YYYY-MM-DD" | null,
      "fit": "high/medium/low",
      "status": "new/skipped/ranked/expired/unreachable",
      "portal": "<source portal skill, e.g. jobindex-search>",
      "source": "cli/websearch"
    }
  }
}

portal 字段记录了生成该职位的 CLI 技能(第 1b 步的结果已按门户标记——请在此处保留该标记)。在此字段出现前写入的记录没有该字段;健康检查(第 4.75 步)会根据 URL 的域名与各门户的基础 URL 进行匹配来归属这些记录,因此不要回填。

source 字段记录了生成该记录的机制:第 1b 步门户 CLI 输出使用 cli,第 1c 步回退方案使用 websearch。这能确保运行摘要消失后,仍可诊断虚假职位报告:某条已存储记录的 URL 后续无法解析到任何内容(或解析到另一个职位),其含义会因来源于实时 CLI 输出还是可能已过时数周的搜索索引而大不相同;而一个已展示的职位在此处完全没有对应记录,则表明内容可能是捏造的,这违反规则 1。在此字段出现前写入的记录没有该字段;切勿回填,因为当时并未记录生成机制。

/rank 会以增量方式扩展此架构:经排名的记录还包含 rank_score(0–100 的综合评分)、rank_verdict(匹配档位,例如 "strong fit")、rank_date(排名日期,ISO 格式)、否决字段 location_verdict 和 language_gate(均为 PASS/FAIL/FLAG),以及解释非 PASS 结果的引用要求 language_note,还有 strengths/gaps(每项 1–3 条原文要点,摘自评分 Agent 的评估结果)。status 字段设为 "ranked"。重写记录时,不要删除这些字段。在 strengths/gaps 出现前完成排名的记录可能没有这些字段;读取程序可以容忍它们缺失,切勿猜测并回填。在判定字段重命名之前完成排名的记录,location 中可能带有旧版 PASS/FAIL/FLAG 字符串;若不存在 location_verdict,应将其视为判定结果。在新记录中,location 始终表示地点,而非判定结果。 deadline 是基础字段,而不是 /rank 扩展:Step 2 的详情抓取已经提取了申请截止日期,因此它在职位首次被发现时写入,并在 /rank Step 4 中当评分 Agent 返回不同值时刷新。null 表示招聘信息未注明截止日期;缺少该键表示该条目早于这个字段 - 绝不要据此推断截止日期,也绝不要靠猜测回填。

posted_date 是招聘信息自身的发布日期,取自 Step 2 的契约已在每个门户 CLI 的搜索输出中保证的 date 字段。Step 1b 使用该日期将本次运行限定在最近 14 天内,随后便丢弃它,因此下游任何环节都无法区分一条昨天发布的招聘信息和一条两年前发布的招聘信息 - first_seen 是这个爬虫首次看到该条目的时间,而不是雇主发布它的时间。持久化该字段使 Step 1b 的时间窗口在运行结束后可审计,并为 /rank 提供一个可供权衡的新鲜度信号,而不是重新发现该日期并把它记录在无人读取的文字中。这个缺口真实发生过:一条日期为 2024-05-13 的 freehire-search 招聘信息被抓取并评为 Strong Fit,在 133 条中排第 1,其自身的评分备注指出该职位 "may be long stale",却没有任何环节能据此采取行动。null 表示门户未为该结果返回日期(当招聘信息省略日期时,CLI 会输出 date: null);缺少该键表示该条目早于这个字段 - 绝不要据此推断发布日期,也绝不要靠猜测回填。

  1. 仅呈现尚未出现在已见列表(按 URL 或 公司+职位匹配)或 tracker 中的职位。
Step 4.5: 生成推荐联系人链接(仅限 High 与 Medium Fit)

对于本次运行中 fit 为 high 或 medium 的每个职位(跳过 low-fit 职位), 构建两个 LinkedIn 人员搜索 URL,以便用户找到招聘人员或团队成员, 寻求推荐或熟人引荐。这有意设计为链接生成步骤, 而非自动查询:不抓取、不使用第三方 API、无需任何运行时依赖或 凭据。

A. 招聘人员 / 人才招聘(推荐路径)

https://www.linkedin.com/search/results/people/?keywords=<url-encoded "<Company Name> recruiter">&origin=GLOBAL_SEARCH_HEADER

B. 职位/团队同行(信息性联络 / 熟人引荐路径)

https://www.linkedin.com/search/results/people/?keywords=<url-encoded "<Company Name> <role keyword>">&origin=GLOBAL_SEARCH_HEADER

从招聘信息的标题中取一个简短关键词作为 <role keyword> - 例如,一条标题为 "AI Program Manager" 的招聘信息会变成 "<Company Name> AI Program Manager"。

两个链接都供用户自行打开和浏览 - 绝不要以编程方式获取或抓取 LinkedIn 人员搜索结果页面。绝不要捏造联系人,也不要声称找到了某个具体的人;这些是搜索链接,不是结果。

步骤 4.75:门户健康检查

基于抓取的门户 CLI 可能会悄无声息地失效:门户更改标记语言后,解析器通常会以状态码 0 退出,但返回 0 条结果,或返回值为 null/乱码的字段;而步骤 1c 的回退机制不会触发,因为它只会在硬性失败时启动。此步骤根据本次运行已有的证据检测这类问题。

免费检查(不增加请求)。 对步骤 1b 中已运行的每个已启用门户:

  • **扫描降级情况:**检查本次运行返回的结果。以下情况均为异常:所有结果中的 company 都为 null 或空值、标题为空、标题中有未解码的实体(&amp;)或 HTML 片段,或 URL 指向的不是该门户。出现任何一种情况,都表示解析器仅部分正常,/scrape 正在悄悄收集垃圾数据。
  • **历史产出:**如果该门户在本次运行的所有查询中均未返回结果,请检查 job_scraper/seen_jobs.json 是否有该门户的过往条目(通过 portal 字段,或匹配该字段引入前条目的 URL 域名)。如果某门户之前的运行曾产出职位,而现在没有产出,则值得怀疑——相同的查询以前是有效的。

升级处理(仅在存疑时进行,次数有限)。 对存疑的门户执行一次哨兵探测:使用该门户自身 SKILL.md 中的示例查询(该查询在注册技能时已确认有效)运行其文档规定的 search,将门户的数量限制标志设为不超过 3,并使用 --format json。如果没有返回结果,再使用一个常见词重试一次。只有在此之后,才判定为故障。429 或拦截页面绝不能作为故障证据——将门户记录为无法确定(受到速率限制),稍后再试,不要重试。

判定结果。 健康的门户不作提示——不显示表格,也不显示单行信息。其他情况均在步骤 5 的摘要中显示一条健康状态信息。

仅探测模式(/scrape health)。 跳过步骤 1-4 以及此步骤的免费检查(没有新的运行结果可供扫描);改为直接探测每个已安装的门户——默认探测已启用的门户,只有明确指定时才探测已禁用的门户(例如 /scrape health jobnet)。对每个门户执行上述哨兵探测,并根据其返回内容应用降级判定标准;此外,由于用户明确要求诊断,还要对每个健康门户的首条结果执行一次 detail 获取(描述必须是可读的已解码文本;若失败,则将状态降为“降级”)。在此模式下报告所有状态,包括健康状态。请求量保持在有限范围内:每个门户进行一次搜索、至多一次重试,以及至多一次详情获取。

步骤 5:呈现结果

按匹配度从高到低,将新职位整理成表格。如果步骤 1b 跳过了招聘网站(enabled: false),请在下方添加 skipped (disabled): 行进行报告,让用户知道有网站被主动排除,而不是悄无声息地跳过;如果没有跳过任何网站,则省略该行。如果本次运行中,任何招聘网站的结果来自步骤 1c 的回退流程(bun 不可用,或其 CLI 在运行时失败),请添加 fallback (websearch): 行进行报告——回退结果来自可能过时的搜索索引,因此读者需要知道哪些行存在这一注意事项;如果所有招聘网站都运行了各自的 CLI,则省略该行。如果步骤 4.75 发现有招聘网站降级、故障或无法得出结论,请为每个可疑网站添加一行 health:(健康的网站无需添加);报告后,询问用户是否要将该网站设为 enabled: false,这样 /scrape 就会停止运行该网站(并通过步骤 1c 的回退流程覆盖它),直到问题修复为止——只有在用户确认后才能编辑此开关,且不得编辑技能中的任何其他内容。

## New Job Matches - YYYY-MM-DD

Found X new positions (Y high, Z medium, W low match).

skipped (disabled): <portal-name>, <portal-name>

fallback (websearch): <portal-name>, <portal-name>

health: <portal-name> - degraded (company null on all 12 results); parsing anchors in .agents/skills/<portal-name>/url-reference.md
health: <portal-name> - broken (0 results for the SKILL.md test query and a broader retry); parsing anchors in .agents/skills/<portal-name>/url-reference.md

| # | Fit | Title | Company | Location | Deadline | URL |
|---|-----|-------|---------|----------|----------|-----|
| 1 | High | ... | ... | ... | ... | [Link](...) |

If Step 2.5 flagged a mass-posting pattern, note it in the Title cell (e.g. "Frontend Developer (posted in 6 cities)") rather than burying it. Do the same for a declared-language-insufficient-level flag from the Language Gate (e.g. "Backend Engineer ⚠ fluent English required") - both are signals the user should see at a glance, not just in the detail highlights below.

### High-Match Highlights
For each high-match job, add 2-3 bullet points:
- Why it matches your profile
- Key requirements to check
- Any red flags (including mass-posting signals from Step 2.5)

### Contacts
For each high/medium-fit job from Step 4.5, add a short contacts block with the two
LinkedIn search links:
- Recruiters/TA search link, for the referral path
- Role/team-peer search link, for the warm-intro / informational-outreach path

呈现结果后,询问:

“要我详细评估其中哪些职位吗?只需告诉我编号。”

如果用户选择了编号,则调用 job-application-assistant 技能工作流(先评估匹配度,通过后再准备简历和求职信)。

如果本次运行发现了许多新职位(大约 8 个或更多),还应建议使用 /rank——它会根据完整的匹配度框架批量评分所有新职位,并返回排名后的候选名单,比逐一查看长表格更高效。(/rank 会在 job_scraper/seen_jobs.json 中设置 ranked、expired 和 unreachable 状态值;去重时应将这三种状态都视为已查看。)

步骤 6:更新跟踪表(可选)

如果用户决定申请某个职位,跟踪表中的相应行由 job-application-assistant 步骤 3b 写入,步骤 5 已会引导用户进入该步骤——此处不要再添加第二行。只有当用户表示自己通过该流程之外的方式申请了某个职位时,才按照 /outcome 步骤 1 中的表头和先匹配后更新规则添加一行。


重要规则

  1. 绝不编造招聘信息。 只展示来自实际 CLI 搜索/详情输出或 WebSearch/WebFetch 结果的职位。
  2. 遵守去重规则。 展示前务必同时检查 job_scraper/seen_jobs.json 和 job_search_tracker.csv。
  3. 聚焦已配置的地理区域。 跳过要求搬迁或明显超出通勤范围的职位。
  4. 只展示仍在招聘的职位。 跳过已过截止日期或标记为已关闭的招聘信息。
  5. 高效获取详情。 不要对每条搜索结果都运行 detail 或 WebFetch——先根据职位名称/摘要筛选,再获取有潜力的匹配项详情。
  6. 并行搜索。 并行运行招聘网站的 CLI 搜索;仅在 CLI 未覆盖的范围内使用 WebSearch。
  7. 禁止自动查找个人信息。 推荐联系人(步骤 4.5)只能通过 LinkedIn 搜索链接查找——绝不通过程序获取或抓取 LinkedIn 人员搜索结果页面。
  8. 健康检查应有限且如实。 步骤 4.75 对每个招聘网站最多进行一次探测、一次重试,以及(在 health 模式下)一次详情获取——这是诊断,而非全面抓取。限流绝不能作为服务故障的证据。健康状态结论只能基于实际观察到的 CLI 输出;无法测试的招聘网站应报告为无法得出结论,绝不可猜测。健康检查唯一可以编辑的内容是 enabled 开关,且必须先获得用户确认。
  9. 指出发布模式,不要指控。 大量发布信号(步骤 2.5)描述的是招聘信息的发布方式,而不是声称雇主是诈骗方。绝不要称某家公司欺诈或不可信——只陈述观察结果,由用户自行判断。

评论

支持 Markdown