# 面经情报爬取 — Prompt 文档
# 一、系统概述
面经情报系统是一个基于 lingma(AI IDE)的自动化面经采集闭环。核心理念:让 AI 自己抓面经,多源并行、自动去重、前置过滤,全程不依赖外部服务。
# 当前数据规模
- 430+ 篇面经原始文件
- 18 家目标公司
- 6 个启用数据源(牛客/GitHub/小红书/CSDN/Web搜索/脉脉)
# 爬取技术栈
- lingma(AI IDE):核心调度
- Playwright:小红书并行截图 + 牛客/脉脉 headed 模式
- image MCP(
mcpimagereaderreadimage):小红书图片内容识别 - Fetch MCP / Playwright MCP:辅助抓取
- SQLite
crawled_urls表:URL 级去重
# 二、环境搭建(从零开始)
假设一台全新的 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 | 快手 | 快手电商 |
| shopee | Shopee | 虾皮、Sea、Garena |
| bilibili | B站 | 哔哩哔哩 |
| dewu | 得物 | 毒、Poizon |
| ctrip | 携程 | Trip.com、去哪儿 |
| xiaohongshu | 小红书 | RED |
搜索关键词设计原则:
- 每家公司 10-25 个关键词,覆盖主品牌 + 子品牌
- 格式:
<公司/子品牌> Java/Go/后端/服务端 社招 面经/面试 - 字节特殊:同时覆盖 Java 和 Go 后端
# 五、数据源矩阵
| 源 | 类型 | 状态 | 工具/脚本 | 关键说明 |
|---|---|---|---|---|
| -------------- | ---------------------- | ---- | ----------------------- | ------------------------------------- |
| 牛客网 | Playwright | ✅ | nowcoder-worker.js | headed 模式,需登录态,搜索帖子类型 |
| GitHub | REST API | ✅ | github-worker.js | Code Search API,需 GITHUB_TOKEN,直接拉 md |
| 小红书 | Playwright + image MCP | ✅ | xhs-crawl-parallel.js | 两阶段流水线(截图→读图),风控最严 |
| CSDN | HTTP API | ✅ | lingma 直接调用 | 搜索 API 可用,部分文章需登录 |
| Web 搜索 | HTTP | ✅ | web-crawl-worker.js | 直接搜牛客讨论帖(放弃 Bing/百度/搜狗,均被反爬拦截) |
| 脉脉 | Playwright | ✅ | headed + 扫码登录 | 职言板块纯文本,噪音多需前置过滤 |
| Playwright MCP | MCP | ✅ | lingma 直接调用 | 深度抓取搜索结果页 |
| Fetch MCP | MCP | ✅ | lingma 直接调用 | 直接抓取已知 URL |
| 掘金 | — | ❌ 禁用 | — | 搜索结果全是技术文章,文章详情 API 返回 null |
| 博客园 | — | ❌ 禁用 | — | reCAPTCHA 验证码拦截 |
| 力扣 | — | ❌ 禁用 | — | SPA 渲染,headless 下链接提取为 0 |
| SegmentFault | — | ❌ 禁用 | — | 面经内容极少,大量 404 |
# 六、完整爬取流水线
# Step 1:文本类来源(牛客 / CSDN / 脉脉)
lingma 通过 Playwright MCP 操作浏览器:
- 打开搜索页 → 输入关键词(从
_config.json读取) - 提取搜索结果链接列表
- 批量去重:查 DB
crawled_urls表,跳过已爬 URL - 逐个打开详情页,获取正文文本
- 前置过滤(见第六节)→ 通过则保存为 raw markdown
- 记录 URL 到
crawled_urls表
牛客特殊处理:
- 搜索 URL:
https://www.nowcoder.com/search?type=post&query=<keyword> - 需点击"面经"标签 + "最新"排序
- 正文选择器:
[class='rich-text'], [class='post-content'], article
脉脉特殊处理:
- 搜索 URL:
https://maimai.cn/web/search_center?type=gossip&query=<keyword>&highlight=true - 需扫码登录,headed 模式
- 噪音多(求面经帖/内推/引流/校招重复帖),需前置过滤
- 按时间排序抓取
# 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):
- N 个 Worker 并行,每个独立浏览器 profile(
xiaohongshu-wN/) - 公司列表 Round-Robin 分配到 Worker
- Worker 间 10-30s 随机延迟启动(避免风控)
- 每个 Worker 内串行执行(同一浏览器实例)
- Profile 初始化:只复制认证文件(Cookies/Login Data/Preferences),不复制 Cache
Worker 执行流程(xhs-crawl-worker.js):
- 从首页
https://www.xiaohongshu.com进入(不能直接访问搜索 URL) - 找到搜索框
[placeholder*="搜索"]→ 填入关键词 → 回车 - 等待搜索结果加载(3s)
- 遍历
a.title元素: - 标题级前置过滤:命中非社招/非后端/题库/广告 → 直接跳过,不进详情页
- 滚动到元素 → JS
.click()进入详情页(不能用 href 导航) - 检测导航方式:overlay(URL 不变)vs 新页面(URL 变了)
- 提取 noteId → 查 DB 去重 → 已爬过则关闭/返回
- 翻页截图:
- 翻页按钮:
.arrow-controller.right - 页码格式:
.xhs-slider-container中的 "N/M" - 每页截图保存为
<companyId>-<序号>-<页码>.png - 翻页后验证页码是否变化,失败重试 3 次
- 提取文本元素(标题
.note-content .title+ 描述#detail-desc) - 记录到
_result.json映射文件 - 写入 DB
crawled_urls表 - 关闭详情页 / 返回搜索结果
- 滚动加载更多结果(
scrollForMore,检测 "THE END" / "没有更多了") - 任务间 5-15s 随机间隔
反风控措施:
- headed 模式(非 headless)
--disable-blink-features=AutomationControlled- persistent context 保持登录态
- 所有操作间加随机延时(0.5-3s)
- Worker 间错开启动
- 滚动模拟人类行为(smooth scroll + 抖动重试)
# 阶段二:lingma 读图整理
按 _result.json 映射逐条处理:
mcpimagereaderreadimage读取每张截图- 优先关注图片左侧主内容区(右侧是推荐栏噪音)
- 前置过滤:非社招?非后端?题库?广告?→ 标记 skip
- 去 UI 噪音(点赞数/评论/头像/水印)→ 合并多页 → 修正 OCR 错误
- 保存为 raw markdown,文件头含原始 URL:
> URL:https://www.xiaohongshu.com/explore/<noteId> - 处理完毕后清理
xhs-tmp-*临时目录
# Step 3:GitHub(API 拉取)
node github-worker.js --company alibaba --limit 10
- 用 GitHub Code Search API 搜索面经仓库中的 md 文件
- 需要
GITHUB_TOKEN环境变量 - 无需浏览器,直接 HTTP 请求
- 自动去重(URL 级)
# Step 4:Web 搜索
node web-crawl-worker.js --company alibaba --limit 10
- 直接搜索牛客网讨论帖(放弃搜索引擎中间层)
- Bing / 百度 / 搜狗均被反爬拦截,已确认不可用
# 七、过滤规则(全源统一)
# 必须删除(不保存)
| # | 规则 | 判断依据 |
|---|---|---|
| --- | ----- | ----------------------------------------------------------------- |
| 1 | 非社招 | 实习/暑期实习/intern/校招/秋招/春招/应届/campus/22-26届/提前批/转正/补录 |
| 2 | 时间过早 | 发布时间在 2020 年之前 |
| 3 | 非后端岗位 | 前端/Android/iOS/客户端/测试/测开/QA/产品/运营/算法岗/数据分析/ML/NLP/CV/C++游戏/嵌入式/硬件 |
| 4 | 题库/合集 | 题库/面试题合集/高频题整理/八股文合集/面试宝典/必背/必刷/N道题/汇总整理/速记/背诵版 |
| 5 | 广告/引流 | 卖课/引流/加群/公众号/领取资料/免费领/优惠/报名/训练营/付费/内推码/推广/返现 |
| 6 | 重复内容 | 同一 URL 已在 crawled_urls 表中 |
| 7 | 非面经内容 | 新闻/技术文章/招聘帖/活动帖/鸡汤文 |
# 例外(白名单)
- 字节 Go 后端:保留(
/字节|bytedance|抖音/+/Go\s*(后端|开发|工程师)/) - Java + 大数据混合:保留
- 个人多公司面经:一律保留,提取时按公司拆分
# 判断优先级
非社招 → 时间 → 岗位 → 内容性质
# 标题级前置过滤(小红书 Worker 内置)
在搜索结果页就执行,命中则不进详情页,节省截图开销:
- 白名单优先放行(字节Go、Java+大数据)
- 然后依次检查:非社招 → 非后端 → 题库/合集 → 广告/引流
# 八、永动机模式(自动化运维)
# 架构
agentStop hook 触发 → 检查状态 → 抓取 → 提取 → 统计 → 结束
↓
agentStop 再次触发
每次 lingma agent 结束后自动检查是否有活干,形成无限循环。
# 安全阀
- 连续 3 轮无新数据 → 自动休眠(
consecutiveEmptyRounds >= 3) - 两轮间隔 ≥ 5 分钟(冷却)
- 每次有新数据 → 重置空轮计数为 0
# 优先级策略
| 公司数据量 | 每轮 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
# 九、关键约束(铁律)
- 小红书用 Playwright 脚本,不走 agent-browser(风控原因)
- image MCP 优先于 OCR:读取截图优先用
mcpimagereaderreadimage - 抓取完成后关闭浏览器 + 清理临时目录
- 禁止
node -e内联执行:必须写临时文件再执行 - 数据保留 6 个月,超期自动降低权重
- 从首页进入小红书,不能直接访问搜索 URL
- 所有操作加随机延时,模拟人类行为
- URL 级去重:写入 DB
crawled_urls表,跨会话持久化
# 十、Raw 文件保存格式
路径:interview-intel/raw/<companyId>/YYYY-MM-DD-NNN-<标题摘要>.md
文件头:
> URL:<原始链接>
> 来源:<数据源名称>
> 发布时间:<原文发布时间>
---
<正文内容>
去重机制:
- 写入前查 DB
crawled_urls表 - 小红书按 noteId 去重(URL 格式
/explore/<noteId>) - 其他源按完整 URL 去重
# 十一、设计决策记录
# 为什么小红书要两阶段?
- 正文在图片里,不是 DOM 文本
- 风控严格,不能用 headless 模式
- 需要保持登录态(persistent context)
- 并行截图 + 后续读图,效率最高
# 为什么放弃搜索引擎中间层?
- Bing / 百度 / 搜狗均被反爬拦截
- 直接搜牛客讨论帖更稳定
- Fetch MCP 对已知 URL 直接抓取更可靠
# 为什么用 headed 模式而不是 headless?
- 小红书 / 牛客 / 脉脉都有反自动化检测
- headed 模式 +
--disable-blink-features=AutomationControlled绕过检测 - persistent context 保持登录态,避免每次重新登录
# 为什么多 Worker 并行?
- 单浏览器串行太慢(每条笔记 5-10s)
- 多 profile 并行,每个 Worker 独立登录态
- 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 表操作 |
# 关键设计
- 所有写操作用事务包裹(
db.transaction()) - FTS5 同步:每次 insertInterview 自动同步到 questions_fts
- Schema 升级:ALTER TABLE + try/catch 忽略已存在列
- 前置依赖关系表:硬编码 cas→aqs、volatile→jmm 等映射
# 附录 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 层过滤:
- Java 相关性:必须含
java|jvm|spring|redis|kafka|mysql|并发|线程池|分布式|后端等 - 面经信号词:必须含
面经|面试|一面|二面|offer|八股|凉经等 - 实习/校招(正文级):
实习面|实习offer|校招面|秋招面|提前批面等 - 非后端岗位(正文前200字):
前端面|Android面|测试面|QA面等 - 题库/合集(正文):
以下是.面试题|[0-9]{2,}道.题|题目汇总等 - 广告/引流(≥3次才判定):
卖课|引流|加群|公众号|扫码|加微信等 - 时间过早(2020年之前):
201[0-9]年.*面试等
# 公司匹配(matchesCompany)
- 标题包含公司名/别名 → 直接通过
- 标题不包含 → 正文前200字必须包含
# 保存 raw md(saveRawMd)
文件命名:{YYYY-MM-DD}-{序号}-{标题前60字}.md
文件头格式:
# {标题}
> 来源:{source}
> URL:{url}
> 采集时间:{ISO}
> 发布时间:{publishedAt}
---
{正文}
同时写入:
_manifest.json(本地去重)- DB
crawled_urls表(全局去重)
# 附录 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"] }
}
}