面经情报爬取 — Prompt 文档


一、系统概述

面经情报系统是一个基于 lingma(AI IDE)的自动化面经采集闭环。核心理念:让 AI 自己抓面经,多源并行、自动去重、前置过滤,全程不依赖外部服务。

当前数据规模

爬取技术栈


二、环境搭建(从零开始)

假设一台全新的 macOS + 全新的 lingma IDE,以下是让爬取能力跑起来的全部步骤。

2.1 基础环境

# Node.js >= 18(推荐 22.x)
# 如果没有 nvm:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash
nvm install 22
nvm use 22

2.2 项目依赖

# 在脚本目录安装 Node 依赖
cd .lingma/skills/interview-intel-crawler/scripts/
npm install
# 安装的包:
#   better-sqlite3 ^12.6.2  — SQLite 驱动(同步 API,WAL 模式)
#   playwright ^1.58.2      — 浏览器自动化(小红书/牛客/脉脉)
#   tesseract.js ^7.0.0     — OCR 备选方案(image MCP 不可用时降级)

# Playwright 需要额外安装浏览器二进制
npx playwright install chromium

2.3 目录结构初始化

# 在项目根目录下创建数据目录
mkdir -p interview-intel/raw
# DB 文件会在首次运行脚本时自动创建:interview-intel/interview-intel.db
# _config.json 需要手动放置到 interview-intel/_config.json(见第三节)

2.4 MCP Server 配置

lingma 需要配置两个 MCP Server,编辑 .lingma/settings/mcp.json

{
  "mcpServers": {
    "image-reader": {
      "command": "node",
      "args": ["<绝对路径>/image-reader-mcp/index.js"],
      "disabled": false,
      "autoApprove": ["read_image"]
    }
  }
}

image-reader-mcp 安装(自建的轻量 MCP Server,读取本地图片返回 base64):

mkdir image-reader-mcp && cd image-reader-mcp
npm init -y
npm install @modelcontextprotocol/sdk

创建 index.js

#!/usr/bin/env node
const { Server } = require("@modelcontextprotocol/sdk/server/index.js");
const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
const { CallToolRequestSchema, ListToolsRequestSchema } = require("@modelcontextprotocol/sdk/types.js");
const fs = require("fs");
const path = require("path");

const MIME = {
  ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
  ".gif": "image/gif", ".webp": "image/webp", ".bmp": "image/bmp", ".svg": "image/svg+xml",
};

const server = new Server({ name: "image-reader", version: "1.0.0" }, { capabilities: { tools: {} } });

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [{
    name: "read_image",
    description: "读取本地图片文件并返回图片内容,支持 png/jpg/jpeg/gif/webp/bmp/svg",
    inputSchema: {
      type: "object",
      properties: { path: { type: "string", description: "图片文件的绝对路径或相对路径" } },
      required: ["path"],
    },
  }],
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name !== "read_image") throw new Error(`Unknown tool: ${request.params.name}`);
  const filePath = path.resolve(request.params.arguments.path);
  if (!fs.existsSync(filePath)) return { content: [{ type: "text", text: `文件不存在: ${filePath}` }], isError: true };
  const ext = path.extname(filePath).toLowerCase();
  const mimeType = MIME[ext];
  if (!mimeType) return { content: [{ type: "text", text: `不支持的图片格式: ${ext}` }], isError: true };
  const data = fs.readFileSync(filePath);
  return { content: [{ type: "image", data: data.toString("base64"), mimeType }] };
});

async function main() { await server.connect(new StdioServerTransport()); }
main().catch(console.error);

Playwright MCP:lingma 内置。用于辅助抓取搜索结果页。 Fetch MCP:lingma 内置。用于直接抓取已知 URL。

2.5 小红书登录态准备

小红书需要 headed 模式 + 持久化登录态:

# 首次登录(会打开浏览器窗口,手动扫码/输密码登录)
node .lingma/skills/interview-intel-crawler/scripts/xhs-crawl-parallel.js --login
# 登录成功后 Ctrl+C 关闭

# 初始化多 Worker profiles(复制登录态)
node .lingma/skills/interview-intel-crawler/scripts/xhs-crawl-parallel.js --init-workers 3

登录态保存在 .browser-data/xiaohongshu/ 目录(Chromium persistent context)。Worker profiles 保存在 .browser-data/xiaohongshu-w1/xiaohongshu-w2/ 等。登录态失效时需重新 --login + --init-workers

2.6 GitHub Token(可选)

GitHub 数据源需要 Personal Access Token:

export GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx

无 Token 也能跑,但 API 速率限制很低(60 次/小时 vs 5000 次/小时)。

2.7 验证安装

# 验证脚本能正常加载 DB
node .lingma/skills/interview-intel-crawler/scripts/extract-raw.js --dry
# 应输出各公司的文件统计,无报错即可

# 验证小红书 Worker
node .lingma/skills/interview-intel-crawler/scripts/xhs-crawl-parallel.js
# 应输出帮助信息

三、Skill 定义

name: interview-intel-crawler
description: "面经抓取流水线:多源并行采集原始面经,保存为 raw markdown"

触发短语:
  - "抓题目"、"爬面经"、"fetch interviews"
  - "抓 <公司> 面经"、"fetch <公司>"
  - "更新面经数据"

目录结构:
  脚本: .lingma/skills/interview-intel-crawler/scripts/
    - xhs-crawl-parallel.js  # 小红书并行调度器
    - xhs-crawl-worker.js    # 小红书单 Worker 执行器
    - nowcoder-worker.js     # 牛客抓取
    - github-worker.js       # GitHub API 拉取
    - web-crawl-worker.js    # Web 搜索抓取
  配置: interview-intel/_config.json
  原始数据: interview-intel/raw/<companyId>/
  浏览器数据: .browser-data/xiaohongshu[-wN]/

四、目标公司配置

17 家目标公司,每家配置 id / name / aliases / searchKeywords:

id名称别名示例
-----------------------------------------
alibaba阿里阿里巴巴、淘天、饿了么、阿里云、菜鸟、高德、闲鱼
ant蚂蚁蚂蚁集团、蚂蚁金服、支付宝、OceanBase
bytedance字节字节跳动、抖音、TikTok、飞书、火山引擎
meituan美团大众点评、美团优选
pdd拼多多PDD、Temu、多多买菜
tencent腾讯微信、WXG、TEG、腾讯云
baidu百度百度智能云、百度搜索
jd京东京东物流、京东健康、京东科技
xiaomi小米米家、小米汽车
netease网易网易云音乐、网易互娱、网易有道
didi滴滴滴滴出行
kuaishou快手快手电商
shopeeShopee虾皮、Sea、Garena
bilibiliB站哔哩哔哩
dewu得物毒、Poizon
ctrip携程Trip.com、去哪儿
xiaohongshu小红书RED

搜索关键词设计原则:


五、数据源矩阵

类型状态工具/脚本关键说明
----------------------------------------------------------------------------------------------------
牛客网Playwrightnowcoder-worker.jsheaded 模式,需登录态,搜索帖子类型
GitHubREST APIgithub-worker.jsCode Search API,需 GITHUB_TOKEN,直接拉 md
小红书Playwright + image MCPxhs-crawl-parallel.js两阶段流水线(截图→读图),风控最严
CSDNHTTP APIlingma 直接调用搜索 API 可用,部分文章需登录
Web 搜索HTTPweb-crawl-worker.js直接搜牛客讨论帖(放弃 Bing/百度/搜狗,均被反爬拦截)
脉脉Playwrightheaded + 扫码登录职言板块纯文本,噪音多需前置过滤
Playwright MCPMCPlingma 直接调用深度抓取搜索结果页
Fetch MCPMCPlingma 直接调用直接抓取已知 URL
掘金❌ 禁用搜索结果全是技术文章,文章详情 API 返回 null
博客园❌ 禁用reCAPTCHA 验证码拦截
力扣❌ 禁用SPA 渲染,headless 下链接提取为 0
SegmentFault❌ 禁用面经内容极少,大量 404

六、完整爬取流水线

Step 1:文本类来源(牛客 / CSDN / 脉脉)

lingma 通过 Playwright MCP 操作浏览器:

  1. 打开搜索页 → 输入关键词(从 _config.json 读取)
  2. 提取搜索结果链接列表
  3. 批量去重:查 DB crawled_urls 表,跳过已爬 URL
  4. 逐个打开详情页,获取正文文本
  5. 前置过滤(见第六节)→ 通过则保存为 raw markdown
  6. 记录 URL 到 crawled_urls

牛客特殊处理

脉脉特殊处理

Step 2:小红书(两阶段流水线)

为什么特殊:正文在图片里(不是 DOM 文本),风控严格(headless 秒封),a.title 的 href=null(必须 JS click)。

阶段一:Playwright 并行截图

# 首次登录(保存 cookie 到 persistent context)
node xhs-crawl-parallel.js --login

# 从主 profile 复制登录态到 N 个 worker
node xhs-crawl-parallel.js --init-workers 3

# 并行抓取
node xhs-crawl-parallel.js --companies alibaba,bytedance,meituan --workers 3 --limit 5

# 抓取所有公司
node xhs-crawl-parallel.js --all --workers 3 --limit 5

调度器设计xhs-crawl-parallel.js):

Worker 执行流程xhs-crawl-worker.js):

  1. 从首页 https://www.xiaohongshu.com 进入(不能直接访问搜索 URL
  2. 找到搜索框 [placeholder*="搜索"] → 填入关键词 → 回车
  3. 等待搜索结果加载(3s)
  4. 遍历 a.title 元素:
  5. 标题级前置过滤:命中非社招/非后端/题库/广告 → 直接跳过,不进详情页
  6. 滚动到元素 → JS .click() 进入详情页(不能用 href 导航)
  7. 检测导航方式:overlay(URL 不变)vs 新页面(URL 变了)
  8. 提取 noteId → 查 DB 去重 → 已爬过则关闭/返回
  9. 翻页截图
  10. 翻页按钮:.arrow-controller.right
  11. 页码格式:.xhs-slider-container 中的 "N/M"
  12. 每页截图保存为 <companyId>-<序号>-<页码>.png
  13. 翻页后验证页码是否变化,失败重试 3 次
  14. 提取文本元素(标题 .note-content .title + 描述 #detail-desc
  15. 记录到 _result.json 映射文件
  16. 写入 DB crawled_urls
  17. 关闭详情页 / 返回搜索结果
  18. 滚动加载更多结果(scrollForMore,检测 "THE END" / "没有更多了")
  19. 任务间 5-15s 随机间隔

反风控措施

阶段二:lingma 读图整理

_result.json 映射逐条处理:

  1. mcpimagereaderreadimage 读取每张截图
  2. 优先关注图片左侧主内容区(右侧是推荐栏噪音)
  3. 前置过滤:非社招?非后端?题库?广告?→ 标记 skip
  4. 去 UI 噪音(点赞数/评论/头像/水印)→ 合并多页 → 修正 OCR 错误
  5. 保存为 raw markdown,文件头含原始 URL:> URL:https://www.xiaohongshu.com/explore/<noteId>
  6. 处理完毕后清理 xhs-tmp-* 临时目录

Step 3:GitHub(API 拉取)

node github-worker.js --company alibaba --limit 10

Step 4:Web 搜索

node web-crawl-worker.js --company alibaba --limit 10

七、过滤规则(全源统一)

必须删除(不保存)

#规则判断依据
-------------------------------------------------------------------------
1非社招实习/暑期实习/intern/校招/秋招/春招/应届/campus/22-26届/提前批/转正/补录
2时间过早发布时间在 2020 年之前
3非后端岗位前端/Android/iOS/客户端/测试/测开/QA/产品/运营/算法岗/数据分析/ML/NLP/CV/C++游戏/嵌入式/硬件
4题库/合集题库/面试题合集/高频题整理/八股文合集/面试宝典/必背/必刷/N道题/汇总整理/速记/背诵版
5广告/引流卖课/引流/加群/公众号/领取资料/免费领/优惠/报名/训练营/付费/内推码/推广/返现
6重复内容同一 URL 已在 crawled_urls 表中
7非面经内容新闻/技术文章/招聘帖/活动帖/鸡汤文

例外(白名单)

判断优先级

非社招 → 时间 → 岗位 → 内容性质

标题级前置过滤(小红书 Worker 内置)

在搜索结果页就执行,命中则不进详情页,节省截图开销:


八、永动机模式(自动化运维)

架构

agentStop hook 触发 → 检查状态 → 抓取 → 提取 → 统计 → 结束
                                                        ↓
                                              agentStop 再次触发

每次 lingma agent 结束后自动检查是否有活干,形成无限循环。

安全阀

优先级策略

公司数据量每轮 limit说明
--------------------------
< 50 题15严重不足,优先补充
50-99 题10偏少
100-199 题7一般
≥ 200 题5充足,维护性抓取

状态持久化

pipeline-state.json

{
  "pipeline": { "status": "idle|crawling|extracting|error", "consecutiveEmptyRounds": 0, "lastRun": "ISO" },
  "crawl": { "roundNumber": 1 },
  "extract": { "status": "idle|running", "pendingFiles": 0 },
  "schedule": { "cooldownMinutes": 5 }
}

操作命令

# 查看状态
node query.js --stats

# 停止:编辑 hook 文件设置 enabled: false
# 重置休眠:编辑 pipeline-state.json,consecutiveEmptyRounds 设为 0

九、关键约束(铁律)

  1. 小红书用 Playwright 脚本,不走 agent-browser(风控原因)
  2. image MCP 优先于 OCR:读取截图优先用 mcpimagereaderreadimage
  3. 抓取完成后关闭浏览器 + 清理临时目录
  4. 禁止 node -e 内联执行:必须写临时文件再执行
  5. 数据保留 6 个月,超期自动降低权重
  6. 从首页进入小红书,不能直接访问搜索 URL
  7. 所有操作加随机延时,模拟人类行为
  8. URL 级去重:写入 DB crawled_urls 表,跨会话持久化

十、Raw 文件保存格式

路径:interview-intel/raw/<companyId>/YYYY-MM-DD-NNN-<标题摘要>.md

文件头:
> URL:<原始链接>
> 来源:<数据源名称>
> 发布时间:<原文发布时间>

---

<正文内容>

去重机制:


十一、设计决策记录

为什么小红书要两阶段?

为什么放弃搜索引擎中间层?

为什么用 headed 模式而不是 headless?

为什么多 Worker 并行?


本文档仅覆盖"爬取"部分。结构化存储 / 查询 CLI / 学习系统集成 → Day 3 输出。


附录 D:Steering — 提取流水线规范

路径:.lingma/steering/extraction-pipeline.md

---
inclusion: manual
---

# 面经结构化提取流水线

> raw markdown → SQLite 结构化数据的提取执行规范。

## 提取流程

1. 获取待处理列表:`node extract-raw.js --list [company]`
2. 逐篇提取:readFile → lingma 分析 → heredoc 管道写入
3. 收尾:`node extract-raw.js --hot-topics`

## 提取 JSON 格式

{
  "company": "string",
  "companyId": "string",
  "level": "P5|P6|P7",
  "department": "string|null",
  "rounds": 1,
  "result": "pass|fail|unknown",
  "experienceYears": "string|null",
  "questions": [
    {
      "module": "concurrent|kafka|redis|mysql|...",
      "topic": "归一化主题",
      "type": "八股|场景设计|代码题|系统设计|追问链|project-deep-dive",
      "questionStyle": "concept|principle|source-code|...(24种)",
      "depthLevel": "surface|mechanism|source|design",
      "difficulty": "P5|P6|P7",
      "content": "归一化题目描述",
      "rawContent": "面试官原话",
      "answerHint": "参考答案要点",
      "round": 1,
      "sortOrder": 0,
      "followUps": [{"content": "追问内容", "parentIndex": null, "depth": 0}],
      "knowledgePoints": ["kebab-case-id"],
      "relatedTopics": ["kebab-case-id"]
    }
  ],
  "jdHighlights": []
}

## 原子拆分规则

| 场景 | 处理 |
|------|------|
| 一个编号多个问号/不同知识点 | 拆为多条,保留追问关系 |
| 项目深挖连续追问 | 合并为 1 条,type=project-deep-dive |
| 算法题 + 追问优化 | 1 条,type=代码题 |
| 系统设计 + 追问细节 | 1 条,type=系统设计 |

## 批量执行策略

- 按公司逐个处理,每篇提取后立即 --save
- 进度文件实时更新,支持断点续传
- 禁止调用外部模型,lingma 自己分析

附录 E:Steering — 永动机运维规范

路径:.lingma/steering/interview-pipeline.md 触发:用户说"抓面经"、"永动机"时加载

---
inclusion: manual
---

# 面经永动机运维规范 v3

## 架构概览

agentStop hook 自动触发,每次 agent 结束后检查是否有活干。

多源抓取层(牛客/GitHub/小红书/Web搜索/Playwright MCP/Fetch MCP)
  → 提取层(lingma 自己分析,不调外部模型)
  → 统计层(脚本刷新 topic_stats)
  → agentStop → 自动下一轮

安全阀: consecutiveEmptyRounds >= 3 → 休眠
冷却: 两轮间隔 >= 5 分钟
状态: pipeline-state.json (断点续传)

## 数据源矩阵

| 源 | 类型 | 脚本/工具 | 说明 |
|----|------|----------|------|
| 牛客 | Playwright 脚本 | nowcoder-worker.js | headed 模式,需登录态 |
| GitHub | API | github-worker.js | 需 GITHUB_TOKEN |
| 小红书 | Playwright 脚本 | xhs-crawl-parallel.js | headed 模式,图文为主 |
| Web搜索 | HTTP | web-crawl-worker.js | 牛客 HTTP 搜索+翻页 |
| Playwright MCP | MCP | lingma 直接调用 | 深度抓取搜索结果页 |
| Fetch MCP | MCP | lingma 直接调用 | 直接抓取已知 URL |

## 优先级策略

| 公司数据量 | 每轮 limit | 说明 |
|-----------|-----------|------|
| < 50 题 | 15 | 严重不足,优先补充 |
| 50-99 题 | 10 | 偏少 |
| 100-199 题 | 7 | 一般 |
| ≥ 200 题 | 5 | 充足,维护性抓取 |

## pipeline-state.json

{
  "pipeline": { "status": "idle|crawling|extracting|crawl-done|error", "consecutiveEmptyRounds": 0, "lastRun": "ISO" },
  "crawl": { "roundNumber": N },
  "extract": { "status": "idle|running", "pendingFiles": N },
  "schedule": { "cooldownMinutes": 5 },
  "history": [...]
}

附录 F:数据库 ER 图

路径:interview-intel/schema.puml

@startuml interview-intel-schema !theme plain

entity "companies" { id : TEXT <<PK>>; name; aliases(JSON); searchkeywords(JSON); totalinterviews } entity "interviews" { id : TEXT <<PK>>; companyid <<FK>>; source; sourceurl; title; rawfile; publishedat; level; rounds; result; experienceyears; education; tags(JSON) } entity "questions" { id : INTEGER <<PK>>; interviewid <<FK>>; companyid <<FK>>; module; topic; type; difficulty; content; rawcontent; answerhint; questionstyle; depthlevel; round; sortorder; publishedat } entity "questionknowledgepoints" { questionid <<FK>>; knowledgepoint } entity "questionfollowups" { questionid <<FK>>; followup; sortorder; parentid; depth } entity "questionrelations" { questionida <<FK>>; questionidb <<FK>>; relationtype; confidence; note } entity "topicstats" { module; topic; frequency; companycount; companies(JSON); difficultydist(JSON); trend; styledist(JSON); timeline(JSON) } entity "companymoduleprofile" { companyid <<FK>>; module; questioncount; toptopics(JSON); difficultydist(JSON) } entity "crawledurls" { url UNIQUE; companyid; source; title; status } entity "extractionlog" { *companyid+filename UNIQUE; status(done/skipped); questionsextracted; interviewid } entity "questionsfts" { FTS5虚拟表: topic, content, answer_hint }

companies ||--o{ interviews interviews ||--o{ questions questions ||--o{ questionknowledgepoints questions ||--o{ questionfollowups questions ||--o{ question_relations @enduml


附录 G:公司配置

路径:interview-intel/_config.json 18 家目标公司,每家含别名 + 搜索关键词

核心结构:

{
  "targetCompanies": [
    {
      "id": "alibaba",
      "name": "阿里",
      "aliases": ["阿里巴巴", "淘天", "淘宝", "天猫", "阿里云", "菜鸟", "饿了么", "高德", "钉钉", "闲鱼"],
      "searchKeywords": ["阿里 Java 社招 面经", "淘天 Java 社招 面经", "饿了么 后端 社招 面经", ...]
    }
  ],
  "targetLevel": ["P6", "P7"],
  "sources": [
    { "id": "nowcoder", "enabled": true },
    { "id": "github", "enabled": true },
    { "id": "xiaohongshu", "enabled": true, "note": "需headed模式+用户登录" },
    { "id": "csdn", "enabled": true },
    { "id": "web-search", "enabled": true, "note": "直接搜索牛客网讨论帖" },
    { "id": "playwright-mcp", "enabled": true },
    { "id": "fetch-mcp", "enabled": true },
    { "id": "maimai", "enabled": true, "note": "职言板块,需headed+扫码登录" }
  ],
  "extractionModel": "lingma",
  "maxInterviewsPerFetch": 10
}

完整公司列表:alibaba / ant / bytedance / meituan / pdd / tencent / baidu / jd / xiaomi / netease / didi / kuaishou / shopee / bilibili / dewu / ctrip / xiaohongshu


附录 H:db.js 接口概要

路径:.lingma/skills/interview-intel-crawler/scripts/db.js(1610 行) SQLite 存储层,基于 better-sqlite3,WAL 模式

核心导出接口

类别函数说明
-------------------------------------------------------------------------------------------
初始化getDb()单例获取 DB 连接,自动建表+升级
公司upsertCompany(company)插入/更新公司信息
面试insertInterview(interview)事务写入面试+题目+知识点+追问链+JD亮点
面试generateInterviewId(companyId)生成 {companyId}-{序号} 格式 ID
去重getProcessedUrls(companyId)返回已处理 URL Set
去重isCrawledUrl(url) / addCrawledUrl(...)crawled_urls 表操作
查询queryQuestions(filters)通用查询,支持 module/company/difficulty/kp/fts 等
查询ftsSearch(query, limit)FTS5 全文搜索,降级到 LIKE
热点refreshTopicStats()刷新 topic_stats 物化视图
热点getHotTopics(module, company)按模块/公司获取热点
画像refreshCompanyProfiles()刷新公司×模块画像
画像getCompanyProfile(companyId, module)获取公司面试风格
溯源getQuestionTrace(questionId)单题完整溯源(含关联题)
分析getFrequencyRank(module)知识点频次排名
分析getFollowUpPatterns(kp)追问链模式分析
分析getComboPatterns(kp, limit)组合拳模式(A 之后问什么)
分析getTrendTimeline(kp, granularity)时间趋势(季度/月)
分析getRoundAnalysis(module, company)轮次分析(一面/二面考什么)
分析getExperienceAnalysis(module)经验年限分析
统计getStats()全局数据概览
覆盖getCoverageAnalysis(module)面经覆盖度 vs 学习知识点
关联buildRelations(options)题目关联分析(知识点交集+前置依赖)
校验validateQuestions(options)数据质量校验(--fix 自动修正)
提取日志logExtraction(...) / isFileProcessed(...)extraction_log 表操作

关键设计


附录 I:提取脚本 extract-raw.js — 核心 Prompt 模板

路径:.lingma/skills/interview-intel-crawler/scripts/extract-raw.js 职责:raw markdown → 结构化 JSON,lingma 自己分析,不调外部模型

提取系统 Prompt(EXTRACTIONSYSTEMPROMPT)

你是一个技术面试分析专家。从面经文本中提取结构化信息,严格按 JSON 格式返回。

## 提取规则

1. company:识别公司名(阿里/字节/美团/拼多多/携程),支持别名(淘天=阿里,抖音=字节)
2. companyId:公司标识(alibaba/bytedance/meituan/pdd/ctrip/xiaomi/kuaishou/shopee/baidu/netease/jd/didi/other)
3. level:识别职级(P5/P6/P7),无明确信息则根据题目难度推断
4. module:每个问题归类到模块(kafka/redis/mysql/concurrent/jvm/java-basic/spring/microservice/system-design/network/os/mq/distributed)
5. knowledgePoints:用短横线命名风格(如 acks-mechanism, isr-mechanism)
6. difficulty:根据追问深度判断(单层概念=P5,源码级=P6,架构设计=P7)
7. 如果文本中包含多轮面试,合并所有轮次的题目,每道题标注 round
8. 如果文本不是面经(如新闻、广告、技术文章),返回 {"skip": true, "reason": "非面经内容"}

## 原子拆分规则

一个编号下如果包含多个独立知识点(多个问号、"以及"、"还有"连接的不同问题),必须拆分为多条记录:
- "HashMap 的底层结构?扩容机制?" → 拆为 2 条
- "Redis 的持久化方式有哪些?RDB 和 AOF 的区别?" → 拆为 2 条
- "说说 synchronized 的原理" → 1 条(单一知识点)

例外(不拆分):
- 项目深挖类:围绕同一个项目的连续追问,合并为 1 条,type 标为 "project-deep-dive"
- 算法题:题目描述 + 追问优化,合并为 1 条
- 系统设计题:一个完整的设计题 + 追问细节,合并为 1 条

## 新字段说明

- questionStyle(24 种):concept / principle / source-code / comparison / scenario / troubleshoot / coding / system-design / best-practice / trade-off / anti-pattern / experience / cross-domain / evolution / project-deep-dive / implementation / optimization / boundary / why-not / workflow / config-tuning / monitoring / reliability / data-consistency
- depthLevel(4 级):surface(表层概念)/ mechanism(机制原理)/ source(源码级)/ design(架构设计级)
- rawContent:保留面试官的原话,不做归一化处理
- content:归一化后的题目描述
- answerHint:参考答案要点(简要关键点)
- followUps:追问链,对象数组 [{content, parentIndex, depth}]
- relatedTopics:相关知识点标识列表

只返回 JSON,不要任何解释文字。

输出 JSON 格式

{
  "company": "string",
  "companyId": "string",
  "level": "P5|P6|P7",
  "department": "string|null",
  "rounds": 1,
  "result": "pass|fail|unknown",
  "experienceYears": "string|null",
  "questions": [{
    "module": "string",
    "topic": "string",
    "type": "简答|场景设计|代码题|系统设计|八股|追问链|project-deep-dive",
    "questionStyle": "concept|principle|...",
    "depthLevel": "surface|mechanism|source|design",
    "difficulty": "P5|P6|P7",
    "content": "归一化题目描述",
    "rawContent": "面试官原话",
    "answerHint": "参考答案要点",
    "round": 1,
    "followUps": [{"content": "string", "parentIndex": null, "depth": 0}],
    "knowledgePoints": ["string"],
    "relatedTopics": ["string"]
  }],
  "jdHighlights": ["string"]
}

执行流程

# 1. 列出待处理文件(返回 JSON,含 systemPrompt + userPrompt)
node extract-raw.js --list [company]

# 2. lingma 读取 raw md → 分析 → 写入临时 JSON → 保存
node extract-raw.js --save <company> <file> --file <jsonFile>

# 3. 跳过非面经文件
node extract-raw.js --skip <company> <file> [reason]

# 4. 刷新热点统计
node extract-raw.js --hot-topics

# 5. 查看提取进度
node extract-raw.js --dry
node extract-raw.js --extraction-stats

附录 J:公共过滤模块 crawl-common.js

路径:.lingma/skills/interview-intel-crawler/scripts/crawl-common.js 所有爬虫 worker 共享的过滤、去重、保存逻辑

标题级前置过滤(shouldSkipByTitle)

按优先级依次检测,命中即跳过:

优先级规则正则示例
-----------------------------------
白名单字节 Go 后端保留`/字节.Go.(后端
白名单Java + 大数据混合保留/Java/i && /大数据/
1非社招(实习)`/实习
2非社招(校招)`/校招
3非社招(届)/2[2-6]届/
4非后端岗位`/前端
5题库/合集`/题库
6广告/引流`/卖课

正文级过滤(shouldSkipByContent)

标题+正文前500字联合检测,7 层过滤:

  1. Java 相关性:必须含 java|jvm|spring|redis|kafka|mysql|并发|线程池|分布式|后端
  2. 面经信号词:必须含 面经|面试|一面|二面|offer|八股|凉经
  3. 实习/校招(正文级):实习面|实习offer|校招面|秋招面|提前批面
  4. 非后端岗位(正文前200字):前端面|Android面|测试面|QA面
  5. 题库/合集(正文):以下是.面试题|[0-9]{2,}道.题|题目汇总
  6. 广告/引流(≥3次才判定):卖课|引流|加群|公众号|扫码|加微信
  7. 时间过早(2020年之前):201[0-9]年.*面试

公司匹配(matchesCompany)

保存 raw md(saveRawMd)

文件命名:{YYYY-MM-DD}-{序号}-{标题前60字}.md

文件头格式:

# {标题}

> 来源:{source}
> URL:{url}
> 采集时间:{ISO}
> 发布时间:{publishedAt}

---

{正文}

同时写入:


附录 K:小红书并行爬虫

路径:xhs-crawl-parallel.js(调度器)+ xhs-crawl-worker.js(执行器) 特点:多浏览器 profile 并行,图文笔记截图 + 文字提取

调度器 Prompt(给 lingma 的指令)

你需要并行抓取小红书面经。执行步骤:

1. 首次使用前初始化 worker profiles(复制主 profile 的登录态):
   node xhs-crawl-parallel.js --init-workers 3

2. 如果登录态失效,先手动登录:
   node xhs-crawl-parallel.js --login
   (在弹出的浏览器中登录小红书,登录后 Ctrl+C)

3. 并行抓取:
   node xhs-crawl-parallel.js --all --workers 3 --limit 5
   或指定公司:
   node xhs-crawl-parallel.js --companies alibaba,bytedance --workers 2 --limit 10

调度器会:
- 将公司均匀分配到 N 个 worker
- 每个 worker 使用独立浏览器 profile(避免 cookie 冲突)
- worker 间错开 10-30 秒启动(避免风控)
- 每个 worker 内串行执行分配到的公司任务

Worker 核心逻辑

单个任务执行流程(crawlOneTask):
1. 打开小红书首页 → 搜索框输入关键词 → 回车
2. 遍历搜索结果中的 a.title 元素:
   a. shouldSkipByTitle 前置过滤(非社招/非后端/题库/广告)
   b. 点击标题进入详情页(可能是覆盖层或新页面)
   c. 提取 noteId 做去重(DB crawled_urls 表)
   d. 截图所有图片页(翻页箭头 .arrow-controller.right)
   e. 提取文字内容(.note-content .title + #detail-desc)
   f. 写入 _result.json(增量保存)
   g. 返回搜索结果页(关闭覆盖层或 goBack)
3. 滚动加载更多结果(scrollForMore,最多重试 8 次无新内容则停止)
4. 达到 limit 或搜索结果到底时结束

反风控措施:
- headed 模式(非 headless)
- --disable-blink-features=AutomationControlled
- 任务间随机间隔 5-15 秒
- 滚动时加入抖动(先上滚300px再下滚)
- 翻页失败重试 3 次

去重机制

CrawledTracker 类:
- 内存 Set + DB crawled_urls 表双重去重
- has(noteId):先查内存,再查 DB(完整 URL 格式)
- add(noteId, title):写内存 + 写 DB
- setCompanyId(companyId):设置当前公司(供 DB 写入时使用)

附录 L:牛客爬虫 nowcoder-worker.js

路径:.lingma/skills/interview-intel-crawler/scripts/nowcoder-worker.js 特点:Playwright headed 模式,传统分页翻页,需登录态

Prompt(给 lingma 的指令)

你需要从牛客网抓取面经。执行步骤:

1. 需要一个已登录牛客的浏览器 profile(默认 ~/.agent-browser-profile)
2. 调度方式:由 text-crawl-parallel.js 统一调度,传入 --tasks JSON
3. 单独测试:
   node nowcoder-worker.js --worker 0 --tasks '[{"companyId":"alibaba","keywords":["阿里 Java 社招 面经"],"limit":10}]'

抓取流程:
1. 打开牛客搜索页 → 输入关键词 → 点击"面经"标签 → 点击"最新"排序
2. collectSearchLinks:翻页收集所有帖子链接(最多25页)
   - 牛客分页 DOM:.search-agination 内最后一个 button 是"下一页"
   - 下一页 disabled 或连续无新链接 → 停止
3. 逐个访问帖子详情页:
   a. shouldSkipByTitle 标题过滤
   b. 提取正文(多个选择器降级:.nc-post-content → .post-content → .discuss-main → article)
   c. shouldSkipByContent 正文过滤
   d. matchesCompany 公司匹配
   e. saveRawMd 保存为 raw markdown
4. 单公司超时 4 分钟

关键 DOM 选择器:
- 搜索结果链接:a[href*="/discuss/"], a[href*="/feed/main/detail/"]
- 标题:.discuss-title, h1.post-title, .post-detail h1
- 正文:.nc-post-content, .post-content, [class*="rich-text"], .discuss-main
- 发布时间:time, .post-time, .discuss-time
- 分页容器:.search-agination, .el-pagination

附录 M:GitHub 爬虫 github-worker.js

路径:.lingma/skills/interview-intel-crawler/scripts/github-worker.js 特点:纯 API,无需浏览器,Code Search + Contents API

Prompt(给 lingma 的指令)

你需要从 GitHub 抓取面经。执行步骤:

1. 设置环境变量:export GITHUB_TOKEN=<your_token>(Search API 必须认证)
2. 调度方式:由 text-crawl-parallel.js 统一调度
3. 单独测试:
   node github-worker.js --worker 0 --tasks '[{"companyId":"pdd","keywords":["拼多多"],"limit":15}]'

双策略搜索:
- 策略1:Code Search API — "{公司名} 面经 language:markdown"
- 策略2:Code Search API — "{公司名} 社招 后端 language:markdown"
- 每个公司用 name + 前2个别名分别搜索

文件处理流程:
1. searchCode → 获取文件列表(name, path, repo, htmlUrl)
2. 文件名预过滤(shouldSkipByTitle)
3. 跳过 >200KB 的文件(大概率是知识点汇总)
4. downloadViaContentsApi → 通过 Contents API 下载(避免 raw.githubusercontent.com 被墙)
5. 提取标题(h1 或文件名)→ shouldSkipByTitle → shouldSkipByContent → matchesCompany
6. saveRawMd 保存

API 限流:
- Search API:10次/min,每次搜索间隔 2.5 秒
- Contents API:认证后 5000次/h
- 单公司超时 240 秒

附录 N:Web 搜索爬虫 web-crawl-worker.js

路径:.lingma/skills/interview-intel-crawler/scripts/web-crawl-worker.js 特点:纯 HTTP,无需浏览器,搜索牛客网讨论帖 + 翻页 + 抓正文

Prompt(给 lingma 的指令)

你需要通过 HTTP 搜索抓取面经(不依赖浏览器)。执行步骤:

1. 抓取指定公司:
   node web-crawl-worker.js --company bilibili --limit 10
2. 抓取所有公司:
   node web-crawl-worker.js --all --limit 5
3. 预览模式(只搜索不抓取):
   node web-crawl-worker.js --dry

搜索策略:
- 直接 HTTP 请求牛客网搜索页(放弃 Bing/百度/搜狗,均被反爬拦截)
- 关键词来源:_config.json 中的 searchKeywords + 补充变体(凉经/offer/一面二面)
- 每个关键词最多翻 3-5 页(limit<=20→3页,否则5页)
- 从 HTML 中正则提取 /discuss/\d+ 链接

抓取流程:
1. searchNowcoder(keywords, maxPages) → 收集所有帖子链接
   - 连续3个关键词0结果 → 判定限流,跳过剩余关键词
   - 关键词间延迟 3-5 秒,页间延迟 3-5 秒
2. 逐个 HTTP 抓取帖子正文:
   a. isUrlGloballyCrawled 全局去重
   b. fetchArticle → httpGet + htmlToMarkdown
   c. shouldSkipByTitle → shouldSkipByContent → matchesCompany
   d. saveRawMd 保存
3. 限流退避:指数退避,最多 2 分钟

htmlToMarkdown 转换:
- 移除 script/style/nav/footer/header/aside
- 保留段落结构(p→\n\n, br→\n, li→"- ")
- 保留格式(strong→**, em→*, code→`)
- 解码 HTML entities

附录 O:MCP 爬取 Prompt 指南

无独立脚本,由 lingma 直接调用 Playwright MCP / Fetch MCP 工具

Playwright MCP 深度抓取

适用场景:需要浏览器渲染的页面(SPA、需登录、动态加载)

Prompt 模板(给 lingma 自己的指令):

你需要用 Playwright MCP 深度抓取面经。步骤:

1. 用 browser_navigate 打开搜索页面:
   - 牛客:https://www.nowcoder.com/search?type=post&query={关键词}
   - 掘金:https://juejin.cn/search?query={关键词}
   - 知乎:https://www.zhihu.com/search?type=content&q={关键词}

2. 用 browser_snapshot 获取页面结构,识别搜索结果列表

3. 逐个点击结果(browser_click),进入详情页:
   a. browser_snapshot 获取正文内容
   b. 判断是否为面经(含面试/面经/一面/二面等信号词)
   c. 判断是否为目标公司(标题或正文前200字含公司名/别名)
   d. 判断是否为社招后端(排除实习/校招/前端/测试等)
   e. 通过过滤 → 提取标题+正文+URL → 保存为 raw md
   f. browser_navigate_back 返回搜索结果

4. 保存格式同 crawl-common.js 的 saveRawMd:
   # {标题}
   > 来源:Playwright MCP
   > URL:{url}
   > 采集时间:{ISO}
   ---
   {正文}

5. 写入 interview-intel/raw/{companyId}/ 目录
6. 调用 db.addCrawledUrl 记录去重

Fetch MCP 直接抓取

适用场景:已知 URL,页面不需要 JS 渲染

Prompt 模板(给 lingma 自己的指令):

你需要用 Fetch MCP 抓取已知面经 URL。步骤:

1. 用 mcp_fetch_fetch 获取页面内容:
   url: "https://www.nowcoder.com/discuss/12345"
   max_length: 50000

2. 从返回的 markdown 中提取:
   - 标题(第一个 # 标题或 <title>)
   - 正文(去掉导航/侧边栏/评论区)
   - 发布时间

3. 过滤判断(同上述规则):
   - 是否为面经?
   - 是否为目标公司?
   - 是否为社招后端?

4. 通过 → 保存为 raw md 到 interview-intel/raw/{companyId}/

适合批量处理已知 URL 列表的场景,比如:
- 从搜索引擎收集到的 URL 列表
- 从其他面经帖子中提取的引用链接
- 用户手动提供的面经 URL

MCP 配置要求

// .lingma/settings/mcp.json 中需要启用:
{
  "mcpServers": {
    "playwright": { "command": "npx", "args": ["@anthropic/mcp-playwright"] },
    "fetch": { "command": "uvx", "args": ["mcp-fetch"] }
  }
}