Skip to content

知乎摸鱼 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 清洗。

知乎反爬的针对性处理

  1. networkidle2 超时兜底:知乎页面有大量长连接请求,waitForNetworkIdle 可能永远不会触发。改成 domcontentloaded + 5s 超时 waitForNetworkIdle,超时不报错继续处理。

  2. BEC Cookie 参数过滤:知乎首页的 BEC 参数记录了用户上次停留的 tab,如果值是"热榜",打开推荐页面会被重定向。设置 Cookie 时自动过滤掉 BEC 参数。

  3. navigator.webdriver 屏蔽:通过 page.evaluateOnNewDocument 注入脚本,把 navigator.webdriver 覆盖为 undefined,避免被知乎检测为自动化工具。

  4. "页面失效"错误页面拦截:知乎对已删除内容会跳转到一个 5 秒自动重定向的错误页。在 domcontentloaded 后立即检测 h1 是否包含"你似乎来到了没有知识存在的荒原",如果匹配则 clearTimeout 清除所有定时器阻止重定向,然后展示自定义错误页。

  5. 浏览器启动 5 次重试:启动失败等待 5 秒重试,最多 5 次。支持自定义 Chrome 路径和 Puppeteer 自带的 Chromium 双模式。

追问:"为什么用 Puppeteer 而不是直接调 API?"

知乎新版 API 有签名校验和频率限制,Puppeteer 模拟真人是更稳定的方案。而且需要执行 JS 才能渲染完整 DOM(如点击"显示全部"展开问题详情),纯 HTTP 请求拿不到。

Q2: 双层智能伪装系统

面试官可能问:"智能伪装是怎么做的?怎么让知乎页面看起来像代码编辑界面?"

回答

伪装系统分两层:详情页伪装 + 侧边栏伪装,通过 onDidChangeViewState 监听 WebView 焦点事件联动触发。

详情页伪装(DisguiseManager):

  • 焦点失去时 → 随机选择 32 种文件类型(.ts/.vue/.py/.java/.rs 等)→ 生成随机文件名 + 对应语言图标 → 替换 WebView 的 titleiconPath
  • 同时通过 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: nonehide-media 模式下 CSS 把媒体元素隐藏、占位符显示。
  • 动态图片源:优先使用 data-actualsrcdata-originalsrc,知乎的图片懒加载需要从自定义属性取真实 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 的核心处理

  1. 图片/GIF/视频处理:统一提取真实 URL(data-actualsrc > data-original > src),GIF 的 .jpg 替换为 .gif,视频添加 controls 属性,按模式设置缩放比例。

  2. 链接处理

    • 知乎重定向链接 link.zhihu.com/?target=xxx 提取真实目标 URL
    • 知乎内部链接自动添加"在 VSCode 中查看"的小图标按钮
    • zhida.zhihu.com 直答链接拦截,改为弹窗展示 AI 解释而不是跳浏览器
    • 相对协议 //xxx 补全为 https://xxx
  3. 广告/付费内容处理

    • 盐选付费内容检测(.KfeCollection-AnswerTopCard-Container),插入版权声明 + 免责声明
    • 移除咨询卡片、电商广告、MCN 推广等元素
  4. 数学公式:解析 script[type="math/tex"].ztext-math,提取 LaTeX 公式并包装为标准格式,前端 MathJax 渲染。

  5. 不适内容图片:知乎的模糊遮挡图片自动添加"点击查看"交互,替换遮挡层结构。

追问:"为什么不用 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 刷新对应列表。

面试官可能问:"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: 回答分批加载与滚动策略

面试官可能问:"如何加载问题下的几百个回答?怎么控制加载节奏?"

回答

知乎问题页的回答是滚动懒加载的,每次滚动到底部才会加载下一批。

分批加载机制

  1. 首次加载时解析当前页面的回答列表(约 20 条),记录 totalAnswerCount
  2. 用户浏览到倒数第 5 个回答时自动触发 loadMoreAnswers() → 模拟人类滚动(window.scrollTo + mouse.wheel 组合)→ 等待 1-1.5s 随机延迟 → 检查 scrollHeight 是否变化
  3. 如果 scrollHeight 没变 → 认为已到底,标记 loadComplete = true
  4. 如果变了 → parseAllAnswers() 增量解析新回答 → 去重(按 answerId)→ 追加到 answerList
  5. 判断 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 标签的工厂函数。

生成逻辑

  1. 根据文件扩展名确定语言(如 .ts → TypeScript Generator)
  2. 循环取模式生成 100 行代码——用 patternIndexes Map 记录每种语言当前取到第几个模式,循环往复
  3. 每个模式内部随机组合变量名、函数名、类名、参数等——比如 randomVariableName() 从 20 个常见变量名中随机选,randomClassName() 从 10 个类名中选
  4. 每个 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-惠东移动

前端开发工程师 · 面试准备