
OpenCLI Indeed 适配器实战用 Cookie 浏览器会话穿透 Cloudflare 抓取美国站职位搜索与详情【免费下载链接】OpenCLIMake Any Website into CLI Use your logged-in browser by AI agent.项目地址: https://gitcode.com/gh_mirrors/ope/OpenCLI本文以 OpenCLI 仓库中的 Indeed 适配器文档为蓝本完整讲解opencli indeed search与opencli indeed job两条命令的参数体系、URL 构造规则、DOM 提取策略与错误模型并结合 clis/indeed 下的源码与 单元测试 剖析为什么这类站点必须走真实浏览器会话。读完后你可以直接复现 Indeed 职位检索的完整工作流并理解 Cookie 策略Strategy.COOKIE在 OpenCLI 适配器架构中的落点。为什么 Indeed 必须驱动真实浏览器会话Indeed 站点位于 Cloudflare 防护之后对www.indeed.com的裸 HTTP 请求会被以403响应并附带cf-mitigated: challenge头因此 Indeed 适配器不走任何 API 直连路径而是驱动一个真实浏览器会话DOM 提取全部发生在页面渲染完成之后。这一点在 clis/indeed/search.js 的文件头注释中有明确说明注册时两条命令均声明strategy: Strategy.COOKIE且browser: true。从源码结构看Strategy.COOKIE是 OpenCLI 注册表的内置策略之一见 src/registry.ts 第 10 行的COOKIE cookie枚举定义且是browser: true命令的默认策略src/registry.ts 第 188 行const strategy cmd.strategy ?? (cmd.browser false ? Strategy.PUBLIC : Strategy.COOKIE)。src/execution.ts 的执行管线中专门包含Domain pre-navigation for cookie strategies一环即在执行前确保浏览器处于目标域上使已登录/已验证的 Cookie 能被页面复用——这正是 Indeed 适配器人工过一次 Cloudflare 验证、后续调用免验证的底层支撑。Indeed 的navigateBefore被显式设为false见 clis/indeed/search.js 第 31 行即不执行预导航而是由命令自身的page.goto(url)直达构造好的目标 URL。命令总览命令说明opencli indeed search query美国站关键词职位搜索opencli indeed job jk别名detail、view按jkjob key读取完整职位描述别名在 clis/indeed/job.js 第 21 行注册aliases: [detail, view]即以下三种写法等价opencli indeed job dccc07ac5a6a3683 opencli indeed detail dccc07ac5a6a3683 opencli indeed view dccc07ac5a6a3683两条命令均为只读access: read域名为www.indeed.com。用法示例# 快速上手 —— 首次运行会走浏览器会话 opencli indeed search rust developer --limit 10 # 限定地点、最近 7 天、按时间排序 opencli indeed search site reliability engineer \ --location Remote --fromage 7 --sort date # 使用 search 输出的 id 字段读取具体职位 opencli indeed job dccc07ac5a6a3683 # JSON 输出 opencli indeed search data engineer -f jsonsearch 命令参数体系与 URL 构造参数表参数类型默认值说明query位置参数必填string—职位 / 技能 / 公司名--locationstring无地点如Remote、New York, NY--fromagestring无时效过滤回溯天数1/3/7/14--sortstringrelevancerelevance或date--startint0分页偏移10 的倍数从 0 开始--limitint15最大返回行数1–25封顶一页参数校验全部集中在 clis/indeed/utils.js 中的纯函数里保证不依赖浏览器即可单元测试requireFromage仅接受白名单{1,3,7,14}其余值抛出ArgumentError如--fromage 30会得到indeed fromage must be one of 1/3/7/14 (days)requireSort归一化大小写后仅接受relevance/daterequireBoundedInt约束--limit为正整数且不超过 25上限即 Indeed 单页容量requireNonNegativeInt约束--start为非负整数。校验先于网络clis/indeed/indeed.test.js 中的 fails fast on invalid limit before opening the page 用例验证了非法limit会在page.goto被调用之前就抛出ArgumentError——坏参数不会浪费一次浏览器导航。URL 构造规则clis/indeed/utils.js 的buildSearchUrl第 97–105 行决定了最终访问的 URL 形态export function buildSearchUrl({ query, location, fromage, sort, start }) { const params new URLSearchParams(); params.set(q, query); if (location) params.set(l, location); if (fromage) params.set(fromage, fromage); if (sort sort ! relevance) params.set(sort, sort); if (start start 0) params.set(start, String(start)); return ${INDEED_ORIGIN}/jobs?${params.toString()}; }关键设计是只写入用户显式提供的参数绝不传空字符串Indeed 会把空参数回显进 URL反而污染 URL 的可回环性。具体省略规则由 单元测试 固化输入生成的 URLquery: software engineer其余为空/默认https://www.indeed.com/jobs?qsoftwareengineerquery: rust, location: remote, fromage: 7, sort: date, start: 20https://www.indeed.com/jobs?qrustlremotefromage7sortdatestart20sort: relevance默认值不输出sort参数start: 0不输出start参数也就是说sort与start采用与默认值相同即省略策略URL 保持最干净、可复现的形态。DOM 提取page.evaluate 与选择器轮询搜索结果并不来自 Indeed 的 JSON 数据岛——源码注释明确指出其 JSON 数据岛重度混淆且随 A/B 桶变化结构因此提取逻辑写在page.evaluate内对渲染后的 DOM 直接查询clis/indeed/search.js 第 55–91 行。核心机制就绪轮询以.job_seen_beacon存在有结果或[data-testidsearchCountPages]等空态标记存在无结果为就绪判据每 500ms 轮询一次最多 30 次约 15 秒卡片解析遍历每个.job_seen_beacon块从标题链接读取data-jk属性作为 job key并用Set按jk去重字段选择器公司名取[data-testidcompany-name]地点取[data-testidtext-location]薪资取.salary-snippet-container span标签取.metadataContainer li span全量文本Cloudflare 探测同时检测document.title是否含Just a moment、页面是否存在[id^cf-]元素作为拦截判定依据。job 命令按 jk 读取职位详情参数参数类型默认值说明id位置参数必填string—Job key来自search的 16 位小写十六进制 jkjk的格式由 clis/indeed/utils.js 第 16 行的正则/^[a-f0-9]{16}$/严格校验requireJobKey并会先 trim、再统一转小写——测试用例确认大写输入DCCC07AC5A6A3683也能被接受并归一化。详情页 URL 由buildJobUrl生成https://www.indeed.com/viewjob?jkjk。详情 DOM 提取与 not-found 判定clis/indeed/job.js 的page.evaluate块第 40–59 行以#jobDescriptionText、h1或[data-testiderror-page]三者之一出现为就绪判据同样做 500ms×30 次轮询并提取title←h1company←[data-testidinlineHeader-companyName]等回退选择器location←[data-testidjobsearch-JobInfoHeader-companyLocation] div/[data-testidinlineHeader-companyLocation]salary←[id*salaryInfoAndJobType] span/[data-testidjob-salary]job_type← 同区块内除薪资外的其余 span逗号连接description←#jobDescriptionText的innerText当页面出现[data-testiderror-page]或h1文本匹配Page Not Found|not found时命令抛出EmptyResultErrorNo Indeed job posting found for jk jk而不是把错误页当数据返回。输出列与 jk 回环约定命令列searchrank, id, title, company, location, salary, tags, urljobid, title, company, location, salary, job_type, description, urlid是 Indeed 的job keyjk——16 位小写十六进制标识符可直接管道进opencli indeed job jk深入读取完整职位描述。这一设计遵循 OpenCLI 的 listing↔detail ID 配对约定列表命令的每一行都携带一个可直接回环进详情命令位置参数的 id 形列使 Agent 无需按标题重搜或正则解析 URL 就能跟进某一行。该约定文档明确把jk列为 indeed 的域特定 id 形列之一。两列集合的差异也有测试保障clis/indeed/indeed.test.js 第 54–60 行search含rank/id/url而job含description且不含rank。行数据由 clis/indeed/utils.js 的searchCardToRow第 130–144 行归一化缺失字段一律落为空字符串而非误标tags列由dedupeTags处理——剥离与salary列重复的薪资文案、去空、去重再用·点分隔连接。测试用例展示了真实效果输入[$50 - $100 an hour, Contract, Hourly pay, Flexible schedule, Contract]且薪资为$50 - $100 an hour时输出为Contract · Hourly pay · Flexible schedule。另一个防御细节当某卡片解析不到jk时url列输出空串而不是拼出一个坏链接。rank的计算与分页对齐clis/indeed/search.js 第 108 行searchCardToRow(c, start i 1)使第 2 页--start 10首行 rank 为 11测试用例对此做了断言。错误模型三类错误的边界从 clis/indeed/search.js 与 clis/indeed/job.js 的实现及对应测试看适配器把失败严格区分为三类错误触发条件语义ArgumentError参数校验失败空 query、非法 fromage/sort、limit 越界、非法 jk调用方问题不打开浏览器页面CommandExecutionErrorCloudflare 拦截页、就绪标记 15s 内未出现选择器漂移/加载异常环境或站点侧问题可重试EmptyResultError页面就绪且确认真实无结果空态标记、not-found 页查询无数据非故障这个三分类在测试中被逐条钉死Cloudflare 拦截页映射为CommandExecutionError轮询超时/选择器漂移不能被误报为EmptyResultError空但就绪的页面才映射为EmptyResultError。对 Agent 而言这个边界很重要——它决定了遇到无结果时应停止还是重试。前置条件处理 Cloudflare 挑战页Indeed 使用 Cloudflare 防护站点。在全新浏览器会话中首次访问时可能先出现Just a moment…过渡页此时适配器抛出Indeed served a Cloudflare challenge page hint: Open https://www.indeed.com in the connected browser and clear the challenge, then retry.处理方式是打开 OpenCLI 连接的那个浏览器在indeed.com上完成一次人工验证此后验证产生的 Cookie 会随Strategy.COOKIE策略延续到后续所有适配器调用无需重复过验证。拦截判定本身在 DOM 内完成document.title含Just a moment或存在[id^cf-]元素见 clis/indeed/search.js 第 89 行。已知限制仅美国站www.indeed.com。Indeed 运营多区域子域uk.indeed.com、de.indeed.com等扩展到其他站点主要工作是替换 origin 并重新核对 DOM 选择器——origin 常量集中收敛在 clis/indeed/utils.js 的INDEED_ORIGIN但选择器硬编码在各page.evaluate块中改动面不止一处。薪资不做归一化salary列取的是页面可见文本原文不统一币种或区间格式需要结构化值时应自行接归一化层。tags 列是无固定分类法的·-连接自由文本工作类型、排班等Indeed 的元数据 pill 文案随 A/B 桶变化不要假设稳定枚举。仓库文件导航文件作用clis/indeed/search.jsindeed search命令实现URL 构造、DOM 提取、Cloudflare 拦截探测clis/indeed/job.jsindeed job命令实现详情页提取与 not-found 判定clis/indeed/utils.js参数校验、URL 构造器、行归一化searchCardToRow/dedupeTagsclis/indeed/indeed.test.js注册形状、校验器、URL 构造、错误分类的完整单元测试src/registry.tsStrategy.COOKIE策略定义与默认策略解析docs/conventions/listing-detail-id-pairing.md列表↔详情 ID 回环约定jk被列为 indeed 的域特定 id 列【免费下载链接】OpenCLIMake Any Website into CLI Use your logged-in browser by AI agent.项目地址: https://gitcode.com/gh_mirrors/ope/OpenCLI创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考