NowCoder 题目批量提取(空白交卷法)
牛客的答案与官方解析只在交卷后才渲染出来。本技能利用这一点:进入一套练习, 一题不答直接交卷,然后从结果页把整场题目连同答案、解析一并抓下来,解析成结构化数据, 再按知识点分类生成文档。
产出三类数据:原始页面文本 + 结构化题目 JSON + 分类总结 Markdown。
何时使用
- 用户给出牛客网考试 / 练习链接,要求"提取题目""抓答案和解析""整理成题库"。
- 用户要按知识点 / 分类(Linux、Java、计算机网络、操作系统…)汇总题目与解析。
- 用户希望数据落地,以便后续做统计或网页展示。
合规提醒
抓下来的是牛客的题目与官方解析。仅供本人学习备考使用,不要公开分发、二次售卖或 批量转载。如果用户提出要把产出发布出去(公众号、题库站、售卖资料包),请先提醒一句 版权与牛客用户协议的风险,再按用户的决定执行。抓取节奏也别太猛——一次跑几轮就够了, 不要写循环连续压几十轮。
Prerequisites(首次使用需准备)
- Node.js 18+,且
agent-browser命令可用(缺失就npm install -g agent-browser)。 - 安装 / 确认浏览器内核(基于 vercel-labs/agent-browser,驱动真实 Chromium):
⚠️ Chromium 缓存跨会话可能丢失,每次开跑前先跑一次确认。agent-browser install # 已装会秒回 already installed;否则后台下载约 500MB,需联网 - 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 不好使就放弃这条路。
脚本内部在做什么
了解这几步有助于牛客改版后定位问题,正常使用不需要手动执行。
- 开父页面 —— 普通模式每轮先
close --all再open(在已有会话上直接 open 有约一半概率停在about:blank);CDP 模式不 close,一关就断连丢登录态。 - 检查登录态 —— 必须在页面打开之后做,否则浏览器还在
about:blank, 页面文本为空,检查会永远判定"已登录",在最需要它的第一轮完全失效。 - 点分类 —— 进入一套新练习(URL 变成
exam/test/新TEST_ID,每轮 testId 不同, 题目不重复)。 - 交卷 → 确定 —— 不答题直接交。
- 轮询等
/submission—— 最多约 20s。 - 抓
document.body.innerText—— 结果页整页文本,每题含 正确答案 / 你的答案(未作答) / 官方解析 / 知识点 / 题友讨论。 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 <分类名>
数据持久化约定
- 原始数据:
raw/roundN_raw.txt(网页抓取原文)、raw/roundN_decoded.txt(解码后) - 结构化数据:
structured/questions_roundN.json(各轮)+questions.json(合并去重后) - 报告:
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. 启动 / 环境
-
已有会话上直接
open会留about:blank。 约一半概率复现,后续所有点击都找不到元素。 → 每轮开始前先close --all,等 2–3 秒再open(CDP 模式除外,见下)。 -
CDP 模式不能
close --all。 接管的是你本机已登录的 Chrome,一关就断连且丢登录态。 -
--profile路径写错会生成游离 Chrome 进程并卡死。 把/tmp/nk_work误打成/,nk_work时,agent-browser 会拿系统根目录当配置启动,命令卡住(exit 137 或超时), 还残留一个吃资源的 Chrome。 → 清理:pkill -9 -f nk_work,删SingletonLock|SingletonSocket|SingletonCookie锁文件,再用正确路径重开。 -
profile 路径含空格。 Windows 上
%TEMP%常在C:\Users\Some Name\...下。 脚本内部已用数组传参处理;如果你自己拼agent-browser命令,记得给路径加引号。 -
Chromium 跨会话可能丢失。
~/.cache/agent-browser不存在时先agent-browser install。 -
代理地址带结尾斜杠 → Chromium 报
ERR_NO_SUPPORTED_PROXIES。脚本会自动剥掉, 手动设HTTP_PROXY时注意别带/。
B. 交卷 / 抓取(Vue 受控组件,ref 与 click 都不可靠)
-
分类点击必须用 JS。
agent-browser click在 Vue 下不触发路由,点了 URL 不变, 后续报"找不到答题卡"。 → 定位getComputedStyle(el).cursor === 'pointer'且直接子文本精确等于分类名的div再.click()。精确匹配很关键,否则"Java"会命中"Javascript"。 -
「交卷」是 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(); -
确认弹窗里的「确定」是
<button>,但页面上可能同时存在多个隐藏弹窗的「确定」。 直接.find(b => b.textContent === '确定')可能选到不可见的那个。 → 先过滤可见性(getBoundingClientRect().width > 0且 computedStyle 不隐藏)再点。 -
点完「确定」不要立刻读 URL。 跳转途中
location.href可能短暂为空,只读一次会 误判失败。→ 轮询等/submission,最多约 20s。 -
CDP 模式下
get url完全失效(返回空 / EOF)。→ 统一用eval "location.href"。 -
「知页简历」广告覆盖层挡住确认框,
ESC关不掉。 → 用该套题的「考试详情」URL 直接重开(https://www.nowcoder.com/exam/test/新TEST_ID/detail?...), 进入干净答题页再交卷。 -
登录检测不能用
grep "登录"。 「退出登录」「登录后查看」「登录/注册」都含这两个字, 一个已登录的页面照样命中,CDP 模式下会直接误判成未登录并退出。 → 用正 / 负向证据分别判断(脚本里的LOGIN_PROBE_JS),拿不准就放行。
C. 解析 / 合并
-
parse.py第 4 个参数是"完整文件路径"不是目录。 -
没有「知识点」行的题会让解析吞掉页脚。 题块的边界是下一道题的「单选题/多选题」行, 所以整页最后一题的块一直延伸到页面底部;只按「知识点」切割时,一旦某题没有这一行, 「关于我们」「京ICP备…」就会被塞进
analysis和摘要里。 →parse.py已按「知识点 / 题友讨论( / 下一题题型 / 已知页脚词」多锚点截断, 并对异常偏长的解析打[WARN]。牛客改版时按references/innertext_format.md调整锚点。 -
去重要带上选项。 牛客存在题干相同、选项不同的单选/多选变体,只比题干会误合并。
-
分类的关键词规则和知识点映射表必须配对。 早期版本给未知分类清空了关键词规则, 却仍沿用 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 客户端)
微信扫一扫