打造专属面试题库:一次知识抽取与数据清洗实践

13 min

前言

研一刚开始时,我很喜欢逛牛客网、脉脉、小红书,搜集各种面试经验贴。看到那些热门面试题和“八股文”,就忍不住把自己代入进去,想如果是我会怎么回答。

但是,问题也随之而来:题目太多,新题也一直在冒出来,只靠收藏和记忆很难长期维护。

后来我就想,能不能自己整理一个专属面试题库,把这些经验贴里的问题抽取出来,再统一成结构化数据。

比较巧的是,刚把数据集搜集完,实习机会来了,这个项目就被暂时搁置了。

虽然项目当时没有完全落地,但从获取数据到清洗数据的过程,让我对数据处理和结构化有了更具体的理解。所以还是把这段经验整理出来,留作记录。

1. 数据从哪儿来:网页数据的获取

获取数据是搭建任何数据集的第一步。想拿到网页上的数据,通常有以下两种方法:

  1. 模拟网络请求:Chrome开发者工具Charles 这类抓包工具,找到页面背后的 API 接口。然后在合规范围内带上必要的 Cookie 或 Session,就能拿到相对干净、原始的数据。 写代码时也非常简单,JavaScript 中有 axiosfetch,Python 中有 requests 库,几行代码就能搞定。

  2. 无头浏览器: 有时候网站的反爬虫机制比较强,直接请求 API 不太稳定。这时可以使用无头浏览器,模拟用户在页面上的真实操作,再从渲染结果里提取内容。 常用工具有 PlaywrightSelenium。实际使用时也要注意访问频率、请求头和间隔,尽量减少对站点的影响。

2. 先把文本清出来:数据预处理

辛辛苦苦爬下来的原始数据,打开后经常是这样的(以下为脱敏数据示例):

<section data-v-5ea596c4="" data-v-405cdcf2=""><!----> <div data-v-5ea596c4="" class="tw-flex"><h1 data-v-5ea596c4="" class="tw-mb-5 tw-font-medium tw-text-size-title-lg-pure tw-text-gray-800">宇宙厂一面</h1> <!----></div> <!----> <!----> <div data-v-5ea596c4="" class="feed-content-text tw-text-gray-800 tw-mb-4 tw-break-all">base北京&nbsp;&nbsp;2024年4月1日<br>上来三道算法题<br>1.最大公共前缀<br>2.最大子序列<br>3.重复字符串<br><br>开始聊项目细节,看到我的项目涉及数据存储相关,问我存储的内容;看到有智能代理相关项目,询问具体工作内容...<br>后面开始从项目延伸出考察知识点:<br>流式输出;<br>大文件上传<br>场景题:如果每次向智能程序询问的问题都是一样,你会怎么减少性能开销。<br>询问对前端框架的熟悉情况,我说对vue框架了解更多,面试官询问了该框架的相关内容,我阐述了一些基础响应式原理相关内容后,未被要求继续展开。<br>询问学习前端的时长和学习方式。<br>反问环节<br><br>全程1个小时9分钟,前面算法题用时接近40分钟(理论预计30分钟完成)。整体感觉还行,面试官没有深入追问更多细节。<br><br><a href="/creation/subject/3871fa77df8f40faa40474d054dbe5e3" target="_blank" class="subject font-green" nc-subject-type="0" nc-subject-tag-id="0">#面试问题记录#</a> <a href="/creation/subject/e7e06396e28b43b2928970fed37fc9ea" target="_blank" class="subject font-green" nc-subject-type="0" nc-subject-tag-id="0">#面试被问“你的缺点是什么?”怎么答#</a> </div> <!----> <!----> <!----> <!----> <!----> <!----> <!----> <div data-v-5ea596c4="" class="tw-my-3"><!----> <!----></div></section>

这样的内容读起来很费劲,也没法直接用于后面的处理。

这时候就需要做预处理。但不能简单粗暴地把所有标签都删掉,那样会破坏文本原有的段落结构和语义。更稳妥的做法是,把 HTML 标签替换成空格或者空行,这样既能去掉无关内容,也能尽量保留原来的语义结构。

处理之后,大致会变成这样:

宇宙厂一面
base北京  2024年4月1日
上来三道算法题
1.最大公共前缀
2.最大子序列
3.重复字符串

开始聊项目细节,看到我的项目涉及数据存储相关,问我存储的内容;看到有智能代理相关项目,询问具体工作内容...

后面开始从项目延伸出考察知识点:
流式输出;
大文件上传;
场景题:如果每次向智能程序询问的问题都是一样,你会怎么减少性能开销。

询问对前端框架的熟悉情况,我说对vue框架了解更多,面试官询问了该框架的相关内容,我阐述了一些基础响应式原理相关内容后,未被要求继续展开。

询问学习前端的时长和学习方式。

反问环节
全程1个小时9分钟,前面算法题用时接近40分钟(理论预计30分钟完成)。整体感觉还行,面试官没有深入追问更多细节。

#面试问题记录#

#面试被问“你的缺点是什么?”怎么答#

3. 把文本整理成结构化数据

经过初步清洗,我们拿到了一批纯文本。但这些文本里,公司、时间、问题、难度等信息仍然混在一起,机器并不能直接按字段读取。

这一步要做的,就是把非结构化文本转换成结构化 JSON 数据。

此时可以使用大语言模型(LLM),配合”提示工程”(Prompt Engineering)技巧来完成。例如,使用经典的 COSTAR 框架组织提示词,并指定LLM输出JSON格式。以下是提示词示例:

# CONTEXT(上下文)
你将处理一段来自面试经验分享的非结构化文本。这些文本通常包含面试公司信息、面试时间、面试内容、技术问题、面试难度等多种元素。文本已经过初步清洗,但仍需进一步结构化处理。

# OBJECTIVE(目标)
将非结构化的面试文本转换为结构化的JSON数据,提取关键信息。

# STYLE(风格)
- 精确:确保提取的信息准确无误
- 完整:尽可能提取所有相关信息
- 结构化:遵循严格的JSON结构
- 简明:去除冗余信息,保留核心内容

# TONE(语调)
专业、中立、客观,不添加个人评价或判断。

# AUDIENCE(受众)
数据处理者和开发人员,他们将使用这些结构化数据构建面试题库。

# RESPONSE(响应)
请返回一个JSON数组,包含以下字段(如果信息不存在则使用null):
[{
  "company": "面试公司名称",
  "level": "面试轮次(如一面、二面等)",
  "tag": "问题类别(如数据结构与算法、数据库、计算机网络等)",
  "difficulty": "难度等级(1-5的整数)",
  "question": "具体的面试问题内容(每条数据仅包含一个问题)",
  "time": "面试日期(YYYY-MM-DD格式)"
},...]

处理大量数据时,逐条请求 LLM 效率较低,并发请求又可能产生较高成本。这类场景可以考虑使用批量 API(Batch API)。把所有请求打包成一个 .jsonl 文件(每行一个 JSON 请求),一次性提交给模型,处理完成后再统一拿结果。这样更适合离线任务,也更容易控制成本。

处理后,就能得到类似下面这样的 JSON 数据(这里只展示一条原始文本提取出的多条面试题数据):

[
  {
    "company": "宇宙厂",
    "level": "一面",
    "tag": "数据结构与算法",
    "difficulty": 3,
    "question": "最大公共前缀",
    "time": "2024-04-01"
  },
  {
    "company": "宇宙厂",
    "level": "一面",
    "tag": "项目经历",
    "difficulty": 2,
    "question": "聊项目细节",
    "time": "2024-04-01"
  },
  {
    "company": "宇宙厂",
    "level": "一面",
    "tag": "数据库",
    "difficulty": 2,
    "question": "项目涉及数据存储相关,存储的内容是什么?",
    "time": "2024-04-01"
  },
  {
    "company": "宇宙厂",
    "level": "一面",
    "tag": "计算机网络",
    "difficulty": 3,
    "question": "流式输出的实现或原理",
    "time": "2024-04-01"
  },
  {
    "company": "宇宙厂",
    "level": "一面",
    "tag": "技术知识",
    "difficulty": 4,
    "question": "大文件上传的实现或方案",
    "time": "2024-04-01"
  },
  {
    "company": "宇宙厂",
    "level": "一面",
    "tag": "性能优化",
    "difficulty": 4,
    "question": "如果每次向智能程序询问的问题都是一样,你会怎么减少性能开销?",
    "time": "2024-04-01"
  }
]

4. 统一口径:二次清洗

拿到 LLM 处理过的数据,并不代表工作就结束了。

仔细看一遍,可能会发现标签、公司名、面试轮次还是很不统一。比如,“宇宙厂”、“抖Y”、“字J”、“bytedance”其实说的都是一家公司。

这时候就需要手动检查,或者用脚本、Excel 再做一轮规范化。

比如,把“宇宙厂”、“抖音”、“字节”、“bytedance”这些都统一成“字节跳动”;面试轮次也规范成“一面”、“二面”、“三面”这种标准格式。

清洗完毕,数据会规整很多:

{
  "id": "3w5s8a",
  "company": "字节跳动",
  "level": "一面",
  "tag": "计算机网络",
  "difficulty": 3,
  "question": "流式输出的实现或原理",
  "time": "2024-04-01"
}

5. 数据体积也可以顺手优化

当数据要在网络上传输时,体积大小也需要考虑。仔细观察 JSON 数据,你会发现很多 key(比如 "company", "question")都是重复的,占了不少空间。

这时可以给 key 做一层压缩:

比如,把完整的key缩写成单个字母:

{
  "i": "3w5s8a",
  "c": "字节跳动",
  "l": "一面",
  "t": "计算机网络",
  "d": 3,
  "q": "流式输出的实现或原理",
  "m": "2024-04-01"
}

更狠一点,直接省略key,用数组形式(当然,这需要前后端约定好顺序):

["3w5s8a", "字节跳动", "一面", "计算机网络", 3, "流式输出的实现或原理", "2024-04-01"]

这样可以减少一部分数据体积。具体收益取决于字段数量和数据规模,在我的场景里大概能减少近 30%。

尾声:项目和数据

基于上面折腾出来的这套数据集,我顺手搭了个前端面试刷题小站:FE Rusher (没错,就是为了督促自己刷题,哈哈!)。

代码也放在 GitHub 上:ni00/FERusher,数据集在仓库的 ./data/fe.json 路径下,有兴趣可以参考。

如果想要创建后端版、算法版,甚至产品经理版的刷题网站,也可以直接在 data 文件夹下放入 JSON 数据,再按需要修改代码运行。