知乎摸鱼 zhihu-fisher — 面试问答
项目时间:2025年4月 - 至今 | 安装量 7000+ | GitHub 160+ Star
代码仓库:https://github.com/crispyChicken999/zhihu-fisher-vscode
插件市场:https://marketplace.visualstudio.com/items?itemName=CrispyChicken.zhihu-fisher
Q1: Puppeteer 反爬数据采集引擎
面试官可能问:"说说怎么用 Puppeteer 爬取知乎内容的?反爬怎么处理?"
回答:
Puppeteer 在后台启动无头 Chrome,模拟真实用户浏览知乎。
核心流程:创建 Page → 设置 Cookie/UA → page.goto(url, waitUntil: 'domcontentloaded') → waitForNetworkIdle({ timeout: 5000 }) → page.evaluate() 提取 DOM 数据 → Cheerio 清洗。
知乎反爬的针对性处理:
networkidle2超时兜底:知乎页面有大量长连接请求,waitForNetworkIdle可能永远不会触发。改成domcontentloaded+ 5s 超时 waitForNetworkIdle,超时不报错继续处理。BEC Cookie 参数过滤:知乎首页的 BEC 参数记录了用户上次停留的 tab,如果值是"热榜",打开推荐页面会被重定向。设置 Cookie 时自动过滤掉 BEC 参数。
navigator.webdriver 屏蔽:通过
page.evaluateOnNewDocument注入脚本,把navigator.webdriver覆盖为undefined,避免被知乎检测为自动化工具。"页面失效"错误页面拦截:知乎对已删除内容会跳转到一个 5 秒自动重定向的错误页。在
domcontentloaded后立即检测 h1 是否包含"你似乎来到了没有知识存在的荒原",如果匹配则clearTimeout清除所有定时器阻止重定向,然后展示自定义错误页。浏览器启动 5 次重试:启动失败等待 5 秒重试,最多 5 次。支持自定义 Chrome 路径和 Puppeteer 自带的 Chromium 双模式。
追问:"为什么用 Puppeteer 而不是直接调 API?"
知乎新版 API 有签名校验和频率限制,Puppeteer 模拟真人是更稳定的方案。而且需要执行 JS 才能渲染完整 DOM(如点击"显示全部"展开问题详情),纯 HTTP 请求拿不到。
Q2: 双层智能伪装系统
面试官可能问:"智能伪装是怎么做的?怎么让知乎页面看起来像代码编辑界面?"
回答:
伪装系统分两层:详情页伪装 + 侧边栏伪装,通过 onDidChangeViewState 监听 WebView 焦点事件联动触发。
详情页伪装(DisguiseManager):
- 焦点失去时 → 随机选择 32 种文件类型(.ts/.vue/.py/.java/.rs 等)→ 生成随机文件名 + 对应语言图标 → 替换 WebView 的
title和iconPath - 同时通过
postMessage通知前端显示伪装代码界面:一个带行号 + VSCode 原生语法高亮的代码编辑器外观,完全覆盖知乎内容 - 焦点回来时 → 通过
postMessage隐藏伪装界面,恢复原标题/图标 - 快捷键
Space支持手动切换
侧边栏伪装(SidebarDisguiseManager):
- WebView 打开时 →
setContext('zhihu-fisher.sidebarDisguised', true)→ 利用 VSCode 的when条件,原本 5 个知乎 TreeView(推荐/热榜/关注/搜索/收藏)自动隐藏,替换为一个 FakeFileListDataProvider 生成的模拟项目文件列表(src/docs/config 等目录 + 各语言文件) - WebView 全部关闭时 → 自动恢复正常侧边栏
追问:"代码界面是真实代码还是假数据?"
是随机生成的代码片段,但结构是真实的——比如 .ts 文件会生成 interface、type、class、async/await 等真实的 TypeScript 语法结构,并且通过 span 标签标注了 VSCode 的 token 类型(mtk5=关键字, mtk9=变量, mtk11=字符串),前端用 VSCode 原生 CSS 变量渲染,效果和真实编辑器几乎一样。
Q3: 多模式自适应媒体控制系统
面试官可能问:"图片视频三种显示模式怎么实现的?隐藏模式下的占位符是怎么做的?"
回答:
三种模式通过 CSS class 切换:normal(默认)、mini(缩放 50%)、none(隐藏)。模式值存在 zhihu-fisher.mediaDisplayMode 配置中,onDidChangeConfiguration 监听变更自动刷新所有侧边栏和 WebView。
核心实现:
- 内存处理(Cheerio 端):解析 HTML 时,每个
<img>/<video>后面都插入一个<span class="media-placeholder">占位符,默认display: none。hide-media模式下 CSS 把媒体元素隐藏、占位符显示。 - 动态图片源:优先使用
data-actualsrc→data-original→src,知乎的图片懒加载需要从自定义属性取真实 URL。 - GIF 处理:知乎的 GIF 在页面中用
.jpg缩略图 + 播放按钮展示,插件自动把.jpg换成.gif并去掉播放按钮。 - 表情包识别:
width/height ≤ 36px或 class 包含emoji/sticker的图片,占位符显示为[表情]并保留 alt 文字,和普通[图片]区分。 - 悬浮预览:隐藏模式下鼠标 hover 占位符,通过
requestAnimationFrame精确计算位置动态弹出缩略图预览窗口,解决了完全隐藏媒体后体验差的问题。
追问:"为什么不直接在 VSCode 配置里设三个枚举值而是用快捷键切换?"
都支持。配置项 mediaDisplayMode 设默认值,侧边栏菜单可切换;快捷键 / 循环切换三种模式(normal→mini→none→normal),直接调 config.update 写配置,比纯 UI 操作快。
Q4: WebView 双向通信与局部 DOM 更新
面试官可能问:"WebView 和 Extension 主进程怎么通信的?早期全量 HTML 替换有什么问题?"
回答:
通信机制:Extension 主进程 → panel.webview.html = HtmlRenderer.getArticleHtml()(全量渲染)或 panel.webview.postMessage({ command, data })(增量更新)。前端 → vscode.postMessage({ command, data }) → 主进程 onDidReceiveMessage switch-case 分发。
消息类型有 40+ 种:内容加载、投票、评论、收藏、导航、伪装切换、样式设置、导出 Markdown 等。
从全量替换到局部更新的重构:
早期每次加载新回答都重新生成完整 HTML 并替换,问题很明显:滚动位置重置、子评论弹窗被关闭、用户操作状态丢失。
现在的策略是分层更新:
- 静态内容初次全量渲染:问题标题、回答内容、作者信息等用
HtmlRenderer.getArticleHtml()一次性生成 - 动态部分 postMessage 增量更新:导航信息(已加载数/总数)、评论列表、投票状态等通过
postMessage只更新对应的 DOM 节点 retainContextWhenHidden: true:切换标签页不销毁 WebView,保留所有 JS 状态
追问:"postMessage 消息多了不会乱吗?"
每个 WebView 有唯一 ID(webviewId),消息处理时从 Store.webviewMap.get(webviewId) 拿到对应实例,确保消息路由到正确的面板。面板关闭时 onDidDispose 清理 Map、关闭 Puppeteer 页面、清理伪装缓存。
Q5: Cheerio 内容解析引擎
面试官可能问:"Cheerio 在内容处理中做了什么?跟 Puppeteer 的分工是什么?"
回答:
Puppeteer 负责"拿数据"(执行 JS、获取 DOM),Cheerio 负责"洗数据"(解析、清洗、转换 HTML)。
ContentProcessor 的核心处理:
图片/GIF/视频处理:统一提取真实 URL(
data-actualsrc>data-original>src),GIF 的.jpg替换为.gif,视频添加controls属性,按模式设置缩放比例。链接处理:
- 知乎重定向链接
link.zhihu.com/?target=xxx提取真实目标 URL - 知乎内部链接自动添加"在 VSCode 中查看"的小图标按钮
zhida.zhihu.com直答链接拦截,改为弹窗展示 AI 解释而不是跳浏览器- 相对协议
//xxx补全为https://xxx
- 知乎重定向链接
广告/付费内容处理:
- 盐选付费内容检测(
.KfeCollection-AnswerTopCard-Container),插入版权声明 + 免责声明 - 移除咨询卡片、电商广告、MCN 推广等元素
- 盐选付费内容检测(
数学公式:解析
script[type="math/tex"]和.ztext-math,提取 LaTeX 公式并包装为标准格式,前端 MathJax 渲染。不适内容图片:知乎的模糊遮挡图片自动添加"点击查看"交互,替换遮挡层结构。
追问:"为什么不用 Puppeteer 直接提取文本?"
Puppeteer 返回的是原始 HTML 字符串,Cheerio 提供 jQuery 风格的 DOM 操作 API,清洗 HTML 比正则匹配可靠得多。而且 Cheerio 在 Node.js 端运行,不依赖浏览器,处理速度快。
Q6: VSCode Extension API 深度定制
面试官可能问:"TreeView 和 60+ 命令是怎么组织管理的?"
回答:
视图架构:自定义 viewsContainers 注册了一个 activitybar 容器 zhihuFisher,内含 5 个 TreeView(推荐/热榜/关注/搜索/收藏)+ 1 个伪装文件列表。每个 TreeView 对应一个 TreeDataProvider 实现类,独立管理自己的数据和刷新逻辑。
命令体系:60+ 个命令按功能模块拆分到 12 个文件:
commands/
hot.ts / recommend.ts / follow.ts / search.ts / collection.ts
webview.ts / webview-navigation.ts / media.ts
cookie.ts / browser.ts / general.ts / about.ts每个文件 export function registerXxxCommands(deps) 返回 vscode.Disposable[],registerAllCommands 统一收集并 push 到 context.subscriptions。
Context Menu 条件渲染:通过 when 条件精准控制菜单项出现场景:
view =~ /^zhihu(Hot|Recommend|Follow|Search)List$/+viewItem == TreeItem→ 显示"在浏览器中打开"view == zhihuCollectionsList && viewItem =~ /.*removable.*/→ 显示"取消收藏"view == fakeFileList时显示伪装相关的菜单,正常视图不显示
配置驱动:所有可配置项通过 contributes.configuration 声明,Extension 内用 vscode.workspace.getConfiguration('zhihu-fisher') 读写,自动持久化到 settings.json。onDidChangeConfiguration 监听变更,媒体模式/伪装开关等变更立即生效无需重启。
追问:"TreeView 和 WebView 数据怎么同步?"
TreeView 展示内容列表,点击某项 → WebviewManager.openWebview(item) → 创建新的 WebView Panel 加载详情。侧边栏数据变化时调 TreeDataProvider.refresh() 触发重绘。WebView 内的操作(如关注/投票)通过 postMessage → ZhihuApiService 调用知乎 API → 成功后通过 TreeDataProvider 刷新对应列表。
Q7: Cookie 管理与知乎 API 对接
面试官可能问:"Cookie 怎么管理的?知乎 API 做了什么?"
回答:
Cookie 管理(CookieManager):
- 通过
vscode.workspace.getConfiguration存储,password: true输入框保护隐私 - 兼容旧版 JSON 格式自动迁移到纯字符串格式
removeBECFromCookie()过滤掉导致重定向的 BEC 参数checkIfPageHasLoginElement()检测.SignFlow-submitButton判断 Cookie 是否过期- 过期时
isAlerting防抖避免重复弹窗提示
知乎 API(ZhihuApiService):
基于 Node.js 原生 fetch 封装的 REST 调用层,统一处理:
- 通用请求头(Cookie、UA、Referer、Origin 等模拟浏览器)
- 无 Cookie 时直接提示用户设置,不发起无效请求
- 支持的操作:赞同/不赞同回答、点赞评论、关注/取消关注用户、收藏到收藏夹、创建/删除收藏夹、不喜欢内容/屏蔽作者、对文章/想法投票
追问:"API 调用和 Puppeteer 爬取怎么分工?"
读操作用 Puppeteer(获取内容列表、文章详情),写操作用 API(投票、关注、收藏)。因为写操作需要知乎的 CSRF Token 校验,Puppeteer 模拟点击不稳定,直接调 API 更可靠。
Q8: 回答分批加载与滚动策略
面试官可能问:"如何加载问题下的几百个回答?怎么控制加载节奏?"
回答:
知乎问题页的回答是滚动懒加载的,每次滚动到底部才会加载下一批。
分批加载机制:
- 首次加载时解析当前页面的回答列表(约 20 条),记录
totalAnswerCount - 用户浏览到倒数第 5 个回答时自动触发
loadMoreAnswers()→ 模拟人类滚动(window.scrollTo+mouse.wheel组合)→ 等待 1-1.5s 随机延迟 → 检查scrollHeight是否变化 - 如果
scrollHeight没变 → 认为已到底,标记loadComplete = true - 如果变了 →
parseAllAnswers()增量解析新回答 → 去重(按 answerId)→ 追加到answerList - 判断
afterLoadCount - beforeLoadCount >= limitPerBatch→ 达到配置的每批数量则暂停,等用户继续滚动再触发
配置项 answersPerBatch(5-50):数值越大一次加载越多,但也越容易触发知乎反爬导致 Cookie 提前失效。
递归控制:
isLoadingBatch互斥锁防止重复触发- 每次递归前检查
Store.webviewMap.has(webviewId)确保页面未被关闭 - 超时错误(protocolTimeout)静默处理不报错
追问:"模拟人类滚动是怎么做的?"
不只是 scrollTo(0, bottom),而是三步组合:先滚到底 → 上移 200px 制造滚动空间 → 再次下滚触发加载。每次操作间加 1-1.5s 随机延迟,模拟真实用户的阅读停顿。
Q9: 代码生成器与多语言伪装
面试官可能问:"32 种编程语言的代码是怎么生成的?怎么保证看起来像真实代码?"
回答:
CodeGenerator 为每种语言定义了 15-25 个代码模式(pattern),每个模式是一个返回带语法高亮 span 标签的工厂函数。
生成逻辑:
- 根据文件扩展名确定语言(如
.ts→ TypeScript Generator) - 循环取模式生成 100 行代码——用
patternIndexesMap 记录每种语言当前取到第几个模式,循环往复 - 每个模式内部随机组合变量名、函数名、类名、参数等——比如
randomVariableName()从 20 个常见变量名中随机选,randomClassName()从 10 个类名中选 - 每个 token 用 VSCode 的 CSS class 标注(
mtk5=关键字,mtk9=变量,mtk11=字符串,mtk15=函数,mtk16=类型),前端用 VSCode 主题的原生 CSS 变量上色
语法结构的真实性:
- TypeScript 生成 interface、type、generic、enum、import type、async/await 等真实语法
- Python 生成 class、def、try/except、for/in、list comprehension
- Rust 生成 struct、impl、lifetime、Option/Result 模式
- SQL 生成 CREATE TABLE、SELECT/JOIN、INSERT、UPDATE 等
追问:"生成的代码能运行吗?"
不能,也没有必要。目标是外观像代码,不需要语法语义正确。用模式拼接的方式比模板字符串灵活——100 行代码有 25^4 种组合可能,几乎不会重复。
行为面试常见题快速答
"为什么做这个项目?"
自己上班想摸鱼刷知乎,网页版图片多太显眼,被老板看到就完蛋。VSCode 扩展市场搜了一圈,已有的知乎插件都是几年前的了,早就不能用了,索性自己写一个。写来自己用,顺手开源了。
"遇到的最大难点?"
知乎的反爬策略一直在变。早期用 networkidle2 等网络空闲,但知乎有长轮询请求导致永远等不到,页面卡死。后来改成 domcontentloaded + 5s 超时兜底的组合策略。还有滚动加载回答时,页面高度检测不稳定——有时滚动了但没新内容,有时新内容没渲染完就拿不到,调试了好几个版本才稳定。
"有什么遗憾?"
没有写自动化测试。每次改 Puppeteer 爬取逻辑都要手动跑一遍看能不能正常加载,效率低。如果重来,会搭 Mock Puppeteer Page 的测试环境,至少把 Cheerio 解析逻辑的测试覆盖率提上来。另外 HTML 渲染层没有用前端框架,全是模板字符串拼接,维护起来比较吃力。
"6700+ 安装量是怎么做到的?"
没刻意推广。插件名"知乎摸鱼"本身就自带传播属性——打工人看到会心一笑。加上 README 写得详细,GIF 动图展示核心功能,功能确实比网页版好用(没有广告、图片可控、伪装功能),用户自发推荐给同事。
"怎么处理用户反馈?"
GitHub Issues 是主要渠道,看到就回复。常见问题(浏览器配置、Cookie 设置、加载失败)写到了 README FAQ 里。插件里还集成了"故障排除指引"和"问题反馈"入口,降低用户求助门槛。
上一篇:02-智能回潮率检测仪系统 下一篇:04-惠东移动