← 返回 Skill 列表
extension
分类: 效率与办公API Key 暂未确认

nowcoder-exam-extractor

从牛客网(NowCoder)智能练习 / 专项练习页面批量提取题目、答案与官方解析。 当用户需要"空白交卷"后抓取题目(答案和解析仅在交卷后可见)、按知识点分类整理成文档、 或为刷题 / 面试备考沉淀可统计、可可视化题库时使用。This skill should be used when the user wants to harvest NowCoder exam questions with answers and analysis by submitting a blank paper and extracting the result page.

person作者: GenJamhubModelScope

NowCoder 题目批量提取(空白交卷法)

牛客的答案与官方解析只在交卷后才渲染出来。本技能利用这一点:进入一套练习, 一题不答直接交卷,然后从结果页把整场题目连同答案、解析一并抓下来,解析成结构化数据, 再按知识点分类生成文档。

产出三类数据:原始页面文本 + 结构化题目 JSON + 分类总结 Markdown。

何时使用

  • 用户给出牛客网考试 / 练习链接,要求"提取题目""抓答案和解析""整理成题库"。
  • 用户要按知识点 / 分类(Linux、Java、计算机网络、操作系统…)汇总题目与解析。
  • 用户希望数据落地,以便后续做统计或网页展示。

合规提醒

抓下来的是牛客的题目与官方解析。仅供本人学习备考使用,不要公开分发、二次售卖或 批量转载。如果用户提出要把产出发布出去(公众号、题库站、售卖资料包),请先提醒一句 版权与牛客用户协议的风险,再按用户的决定执行。抓取节奏也别太猛——一次跑几轮就够了, 不要写循环连续压几十轮。

Prerequisites(首次使用需准备)

  1. Node.js 18+,且 agent-browser 命令可用(缺失就 npm install -g agent-browser)。
  2. 安装 / 确认浏览器内核(基于 vercel-labs/agent-browser,驱动真实 Chromium):
    agent-browser install        # 已装会秒回 already installed;否则后台下载约 500MB,需联网
    
    ⚠️ Chromium 缓存跨会话可能丢失,每次开跑前先跑一次确认。
  3. Python 3.8+(脚本只用标准库,无需装包)。

快速开始

绝大多数情况直接用一键脚本,不要手动逐步操作——手动路径容易在按钮定位上翻车, 脚本里已经把踩过的坑都固化进去了。

# macOS / Linux
./scripts/run_nk_extract.sh -u "父页面URL" -c "分类名" -n 轮数 -o 输出目录

# Windows(必须走 CDP 接管本机已登录 Chrome,原因见下)
bash scripts/run_nk_cdp.sh -u "父页面URL" -c Java -n 2 -o ./nowcoder_Java

示例(Linux 专项,跑 3 轮):

./scripts/run_nk_extract.sh \
  -u "https://www.nowcoder.com/exam/intelligent?questionJobId=10&subTabName=intelligent_page&tagId=21002" \
  -c Linux -n 3 -o ./nowcoder_linux

参数:

| 参数 | 含义 | 默认 | |------|------|------| | -u | 牛客专项练习父页面 URL(含 tagId=) | 必填 | | -c | 分类名,须与牛客页面上的文字完全一致(Linux / Java / 操作系统 / 计算机网络…) | 必填 | | -n | 轮数,每轮抓一整套 | 4 | | -p | 每轮题数,仅用于日志提示,不影响结果 | 20 | | -o | 输出目录 | ./nowcoder_<分类> | | -s | 起始轮次编号(分段续跑用) | 0 | | --max N | 去重后保留的题目上限,0 = 不截断(默认) | 0 | | --cdp <port> | 接管本机已登录 Chrome 的调试端口(Windows 必用) | 无 | | --proxy <url> | 代理地址(结尾斜杠会被自动剥掉) | 无 |

每套题数由牛客决定(Java 约 30,Linux / 操作系统 / 网络约 20),脚本不做预设。 想要 N 题就把 -n 调大多跑几轮,去重后取到多少就是多少。只有明确要限量时才用 --max,它截断时会打印一行 [WARN] 告诉你丢了几题。

判断这一轮是否真的成功

这一节很重要:"文件生成了"不等于"抓到题了"。页面没真正进结果页、牛客改版导致 innerText 结构变化,都会得到一份格式完全正确但内容为空的产物。所以判据只有一个—— 脚本最后那行:

=== 完成:共 40 题(3 轮)===

汇报给用户时以这个数字为准。脚本的退出码也已经和它对齐了:任何一轮抓到 0 题都会 判定该轮失败并重试,三次都失败就非零退出,不会再出现"打印完成但报告是空的"。

排查时的信号:

  • [ERROR] 未解析出任何题目 —— 多半没进 /submission,或牛客改版。先打开 raw/roundN_raw.txt 看一眼实际抓到了什么。
  • [WARN] 解析异常偏长的题号: [...] —— 解析里可能混进了页脚,说明截断锚点没命中, 需要按 references/innertext_format.md 调 parse.py 的正则。
  • [FATAL] 登录态问题 —— 重试没用,先把登录做好。

登录态(唯一需要人介入的环节)

牛客页面必须登录,而且方案分平台,详见 references/browser_auth.md。

macOS / Linux —— 有头窗口 + 独立 profile:

PROFILE=/tmp/nk_work
agent-browser --headed --profile "$PROFILE" open "练习页URL"

弹出的 Chrome 窗口里扫码 / 登录一次,之后所有命令带 --profile "$PROFILE" 复用同一登录态。 脚本会自动做这件事,未登录时会暂停等你操作。

Windows —— 必须 CDP 接管本机 Chrome: agent-browser --headed 在 Windows 上开的窗口对用户不可见(进程不在交互桌面), 没法扫码,所以唯一稳的路是接管你自己那个已登录的 Chrome:

REM 1) 关掉所有 Chrome 窗口(托盘也退出),然后带调试端口重启并登录牛客,保持窗口开着
"C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222
# 2) 用 --cdp 接管(run_nk_cdp.sh 默认就是 --cdp 9222)
bash scripts/run_nk_cdp.sh -u "<父页面URL>" -c Java -n 2 -o ./nowcoder_Java

⚠️ 与 macOS 结论相反:Windows 上 CDP 直连能稳定复用登录态,不要因为 macOS 上 CDP 不好使就放弃这条路。

脚本内部在做什么

了解这几步有助于牛客改版后定位问题,正常使用不需要手动执行。

  1. 开父页面 —— 普通模式每轮先 close --all 再 open(在已有会话上直接 open 有约一半概率停在 about:blank);CDP 模式不 close,一关就断连丢登录态。
  2. 检查登录态 —— 必须在页面打开之后做,否则浏览器还在 about:blank, 页面文本为空,检查会永远判定"已登录",在最需要它的第一轮完全失效。
  3. 点分类 —— 进入一套新练习(URL 变成 exam/test/新TEST_ID,每轮 testId 不同, 题目不重复)。
  4. 交卷 → 确定 —— 不答题直接交。
  5. 轮询等 /submission —— 最多约 20s。
  6. 抓 document.body.innerText —— 结果页整页文本,每题含 正确答案 / 你的答案(未作答) / 官方解析 / 知识点 / 题友讨论。
  7. parse.py 解析成 JSON,多轮跑完后 merge_report.py 合并去重出报告。

⚠️ 分类 / 交卷 / 确定一律用页面内 JS el.click(),不要用 agent-browser click。 牛客前端是 Vue 受控组件,程序化 click 不触发框架的事件系统和路由——点了 URL 不变, 后面全盘失败。具体选择器见「常见失败」第 4、5 条。

空白交卷不需要先跳末题。 进练习页直接交卷,结果页就含整场全部题目。早期版本 有个"先点末题触发渲染"的步骤,实测多余,而且是 Windows 上最容易出错的一环,已删。 那套基于 snapshot + grep ref 的老流程留在 references/legacy_ref_method.md, 仅在牛客大改版、JS 选择器全失效时才需要翻出来参考。

单步运行(排查时用)

# 解析单轮原始文本
python scripts/parse.py <raw文件> "<结果页URL>" <输出.json> <解码后原文.txt>
#   注意第 4 个参数是**具体文件路径**,不是目录

# 合并去重 + 生成报告(末尾 0 = 不截断)
python scripts/merge_report.py <structured目录> <输出目录> 0 <分类名>

数据持久化约定

  1. 原始数据:raw/roundN_raw.txt(网页抓取原文)、raw/roundN_decoded.txt(解码后)
  2. 结构化数据:structured/questions_roundN.json(各轮)+ questions.json(合并去重后)
  3. 报告:report/<分类>_questions.md(总览表 + 按知识点分组的逐题详解, 含 ✅ 正确选项标记、简洁摘要、完整官方解析、知识点、讨论数、来源链接)

适配其他分类

  • 换知识点:去牛客对应专项练习页复制带 tagId= 的 URL,换掉 -u 和 -c 即可。
  • 分类规则内置在 scripts/topic_rules.py,按 -c 分类名自动选用 (Linux / 操作系统 / 计算机网络 / Java 已收录)。
  • 未收录的分类(数据库 / C++ / 前端…)自动兼容:关键词规则和知识点映射表会 一起清空,每题直接按自带的 knowledge_points 归类,不会被 Linux 规则误判。
  • 要长期支持某个新分类:在 topic_rules.py 的 TOPIC_RULES_MAP 加一项; 临时用可设环境变量 NK_TOPIC_RULES。两种写法见 references/topic_rules.md。

常见失败与对策

按发生阶段归类,全部是实测踩过的坑。

A. 启动 / 环境

  1. 已有会话上直接 open 会留 about:blank。 约一半概率复现,后续所有点击都找不到元素。 → 每轮开始前先 close --all,等 2–3 秒再 open(CDP 模式除外,见下)。

  2. CDP 模式不能 close --all。 接管的是你本机已登录的 Chrome,一关就断连且丢登录态。

  3. --profile 路径写错会生成游离 Chrome 进程并卡死。 把 /tmp/nk_work 误打成 /,nk_work 时,agent-browser 会拿系统根目录当配置启动,命令卡住(exit 137 或超时), 还残留一个吃资源的 Chrome。 → 清理:pkill -9 -f nk_work,删 SingletonLock|SingletonSocket|SingletonCookie 锁文件,再用正确路径重开。

  4. profile 路径含空格。 Windows 上 %TEMP% 常在 C:\Users\Some Name\... 下。 脚本内部已用数组传参处理;如果你自己拼 agent-browser 命令,记得给路径加引号。

  5. Chromium 跨会话可能丢失。 ~/.cache/agent-browser 不存在时先 agent-browser install。

  6. 代理地址带结尾斜杠 → Chromium 报 ERR_NO_SUPPORTED_PROXIES。脚本会自动剥掉, 手动设 HTTP_PROXY 时注意别带 /。

B. 交卷 / 抓取(Vue 受控组件,ref 与 click 都不可靠)

  1. 分类点击必须用 JS。 agent-browser click 在 Vue 下不触发路由,点了 URL 不变, 后续报"找不到答题卡"。 → 定位 getComputedStyle(el).cursor === 'pointer' 且直接子文本精确等于分类名的 div 再 .click()。精确匹配很关键,否则 "Java" 会命中 "Javascript"。

  2. 「交卷」是 DIV 叶子节点,不是 <button>。 右上角真正可点的是 <div class="tw-text-xl-pure ...">交卷</div>,用 querySelectorAll('button') 找不到, JS 会报 Cannot read properties of undefined。

    [...document.querySelectorAll('*')]
      .find(el => el.children.length === 0 && el.textContent.trim() === '交卷').click();
    
  3. 确认弹窗里的「确定」是 <button>,但页面上可能同时存在多个隐藏弹窗的「确定」。 直接 .find(b => b.textContent === '确定') 可能选到不可见的那个。 → 先过滤可见性(getBoundingClientRect().width > 0 且 computedStyle 不隐藏)再点。

  4. 点完「确定」不要立刻读 URL。 跳转途中 location.href 可能短暂为空,只读一次会 误判失败。→ 轮询等 /submission,最多约 20s。

  5. CDP 模式下 get url 完全失效(返回空 / EOF)。→ 统一用 eval "location.href"。

  6. 「知页简历」广告覆盖层挡住确认框,ESC 关不掉。 → 用该套题的「考试详情」URL 直接重开(https://www.nowcoder.com/exam/test/新TEST_ID/detail?...), 进入干净答题页再交卷。

  7. 登录检测不能用 grep "登录"。 「退出登录」「登录后查看」「登录/注册」都含这两个字, 一个已登录的页面照样命中,CDP 模式下会直接误判成未登录并退出。 → 用正 / 负向证据分别判断(脚本里的 LOGIN_PROBE_JS),拿不准就放行。

C. 解析 / 合并

  1. parse.py 第 4 个参数是"完整文件路径"不是目录。

  2. 没有「知识点」行的题会让解析吞掉页脚。 题块的边界是下一道题的「单选题/多选题」行, 所以整页最后一题的块一直延伸到页面底部;只按「知识点」切割时,一旦某题没有这一行, 「关于我们」「京ICP备…」就会被塞进 analysis 和摘要里。 → parse.py 已按「知识点 / 题友讨论( / 下一题题型 / 已知页脚词」多锚点截断, 并对异常偏长的解析打 [WARN]。牛客改版时按 references/innertext_format.md 调整锚点。

  3. 去重要带上选项。 牛客存在题干相同、选项不同的单选/多选变体,只比题干会误合并。

  4. 分类的关键词规则和知识点映射表必须配对。 早期版本给未知分类清空了关键词规则, 却仍沿用 Linux 的知识点映射表,结果一道知识点为「权限管理」的数据库题被归成「文件权限」。 → get_rules() 现在对未知分类把两者一起清空。加新分类时同理,别让 KP_MAP 指向 不属于该分类的主题名。

D. 速记

  • 点击三原则:用页面内 JS、精确匹配文本、可见性过滤。
  • 路径三原则:profile 路径合法、参数传具体文件、URL 里别混进空格(曾因 exam/ intelligent 多了空格而 404)。
  • 中断恢复:卡死就 pkill -9 -f nk_work + 清锁文件 + 确认 Chromium 已装, 再带正确 --profile 重开;用 -s <已完成轮次> 续跑。

参考文件

  • references/browser_auth.md —— 登录态获取,分平台的实测成功 / 失败方案
  • references/innertext_format.md —— 结果页文本结构(parse.py 的解析依据)
  • references/topic_rules.md —— 各分类归类规则与扩展方法
  • references/legacy_ref_method.md —— 已废弃的 snapshot+ref 操作法,仅供大改版时参考
  • scripts/README.md —— 面向终端用户的独立使用说明(不依赖任何 AI 客户端)

OceanBase 数据库大赛队伍验证行

<!-- OB-S:RBP8IOE5syMdf_aMfSSHVQ -->