资讯详情

油猴脚本实战:零服务器抓取知乎内容,从原理到导出全流程

📅 2026/10/17 23:14:07 | 华诺云谱 👁 阅读
油猴脚本实战:零服务器抓取知乎内容,从原理到导出全流程
提到“爬取知乎内容”很多人的第一反应是上爬虫框架、搞代理池、维护登录态整套东西下来没有两三天跑不通。但如果你只是想把自己某个问题下的回答、收藏夹里的内容、某个用户的高赞回答保存成本地资料其实有个轻量得多的路子——油猴脚本。油猴脚本跑在浏览器里天然拥有你登录后的身份不需要单独处理Cookie过期也不需要绕登录验证。页面加载什么脚本就能拿到什么数据存在哪里、结构长什么样直接打开开发工具看一眼就知道。这篇分享就围绕“用油猴脚本抓知乎内容”这个思路展开把从原理、实现到踩坑的完整过程整理出来适合有一定前端基础、想快速做内容备份的开发者参考。1. 为什么是用油猴脚本抓知乎而不是养一个爬虫服务1.1 登录态与数据来源的天然优势写爬虫最麻烦的不是解析页面而是身份问题。知乎的绝大多数内容必须登录后才能稳定访问尤其是搜索、收藏夹、关注页这类场景。传统方案需要维护登录Cookie、处理验证码、应对风控一套流程做下来精力全耗在对抗上真正的数据提取反而成了小头。油猴脚本是运行在浏览器扩展沙盒里的用户脚本它复用你当前浏览器窗口的登录态。知乎觉得“你就是那个正常登录的用户”请求从真实浏览器发出带完整的浏览器指纹、Cookie、UA脚本再去操作页面或调用页面内接口几乎不增加额外风控压力。我自己的体会是做个人数据备份、内容整理这类低频任务油猴脚本是性价比最高的形态。它不需要独立服务器不需要定时任务打开页面点一下脚本就能跑抓完导出一个文件收工。数据量级在几百条到几千条的范围内浏览器内存完全扛得住。1.2 适用场景与不该碰的场景油猴抓取天然适合三类场景一是备份自己的创作内容比如把某段时间发过的想法、文章按时间归档二是收藏夹整理把收藏的问题、回答导出成可检索的Markdown三是跟踪性抓取比如固定时间抓取某个热榜榜单做趋势记录。但它不适合做大规模数据采集。一方面浏览器标签页的内存有限持续滚动几万条内容页面渲染节点太多标签页会卡死另一方面脚本频繁调用页面接口短期内大量请求仍然会触发频控严重时影响当前账号的正常使用。这个边界要想清楚油猴是“个人备份工具”不是“数据采集平台”。2. 抓取前必须搞懂的底层机制渲染方式、数据位置与分页规则2.1 知乎内容页的装载方式服务端渲染与异步加载的混合体抓取前先做一次彻底的分析。我以前吃过亏拿到页面就开始写选择器结果滚动几下脚本就失灵重新排查才发现内容根本不是一直存在的DOM节点。打开一个知乎问题页面按F12看网络请求会看到两种明显不同的情况。第一屏的问题描述、部分回答是服务端渲染的结果直接存在于初始HTML里但继续往下滚动后面的回答是通过XMLHttpRequest或Fetch请求分页接口拿到的JSON数据再由前端JS渲染成DOM。换句话说你看到的页面一部分是“死”的一部分是“活”的。这种混合渲染机制决定了脚本不能只写一个页面加载完成的回调。必须监听滚动或者监听DOM变化等待异步内容渲染完成后再去提取新出现的节点否则只能抓到第一屏的内容。我建议的做法是区分“首屏抓取”和“动态抓取”两个阶段首屏直接解析已渲染DOM动态阶段通过MutationObserver监听新增回答节点这样结构最清晰也便于出错时定位到底哪一步没跑。2.2 DOM解析还是接口直取两条路线的取舍逻辑知乎页面的数据获取有两条技术路线解析渲染后的DOM节点或者直接调用页面内部的API接口。DOM解析的优点是稳定直接所见即所得。用户看到什么脚本抓到的就是什么。知乎前端的渲染逻辑就算变化只要最终DOM结构变化不大脚本调整的成本就很低。缺点是受页面结构影响大如果知乎某次改版调整了class名选择器就失效一大片。直接调接口的优点是数据结构干净拿到的JSON里有作者、时间、点赞数、内容全文不需要费劲剥离HTML标签。但接口不是随便调的知乎的接口有签名参数普通请求不带签名会被风控拦下来。油猴脚本的好处在于页面自身已经发起了这些接口请求脚本可以直接复用页面上下文中已有的请求参数或者从网络面板中提取请求格式。我最终采用的方式是混合路线页面里有完整数据的场景用DOM解析拿不到稳定DOM时再退回到接口直取。比如抓收藏夹内容时收藏夹页面DOM结构经常变化我就抓页面底部发起的列表接口解析JSON比调整DOM选择器快得多。2.3 分页规则的三种形态页码、游标与时间线知乎不同模块的分页逻辑各不相同写脚本前必须先确认你目标页面的分页类型。问题回答列表用的是深度分页加游标方式。接口返回的JSON里有一个“cursor”字段下一页请求把它原样带过去即可。评论区和想法流也类似都是基于游标。收藏夹列表则是比较简单的页码分页页面URL里直接有page参数或者接口体里有offset参数。如果你只抓前几页这个最简单循环请求即可。用户时间线是另一种情况它更像是“按时间截断”的流式接口请求参数里有before和after两个时间戳。初次请求拿最新20条下一次把当前列表最后一条的时间戳作为before参数继续往下翻。这个逻辑和cursor本质上很像但字段名不同容易搞混。3. 脚本实战从第一行代码到稳定跑完整个页面流3.1 构建脚本骨架注入时机与运行容器油猴脚本的头部元信息决定了脚本的运行环境。抓取知乎内容需要操作DOM、发请求、下载文件权限比普通脚本多一些。我常用的头部配置如下// UserScript // name 知乎内容抓取与导出 // namespace local.zhihu.export // version 0.2.0 // description 抓取当前页面的回答/文章内容支持翻页自动加载导出Markdown与JSON // match https://www.zhihu.com/* // grant GM_xmlhttpRequest // grant GM_download // grant GM_setValue // grant GM_getValue // run-at document-idle // noframes // /UserScript几个字段值得展开说一下。run-at document-idle表示页面加载完成后运行脚本这个时机能保证首屏DOM已经存在但又不会因为等待太久而丢失早期动态数据。noframes限制脚本只在主页面运行知乎页面上偶尔有内嵌iframe不限制的话脚本会在每个iframe里都跑一遍做弹窗逻辑时会异常。权限方面GM_download不是必需的如果脚本只用URL.createObjectURL加a[download]触发下载就不需要这个权限。但考虑到抓取内容量较大时可能超过浏览器单次下载限额我会用GM_download做兜底遇到大文件时更稳定。GM_setValue用于保存抓取进度防止页面刷新后从头再来。3.2 滚动加载监听别用最笨的定时器轮询监听知乎无限滚动时新手最容易写出的代码是setInterval每500毫秒检查一次“有没有新节点”这种做法的问题有两个一是频繁执行查询影响页面流畅度二是容易在知乎自身加载动画还没完成时抓到半截DOM提取出残缺数据。正确的方式是用MutationObserver监听列表容器的子节点变化同时配合IntersectionObserver做底部触发判断。简单说MutationObserver负责回答“是不是新增了内容”IntersectionObserver负责回答“是不是应该去加载下一页”。两者配合几乎不消耗多余性能。const listSelector .List-item; const observer new MutationObserver(() { collectNewItems(); }); observer.observe(document.querySelector(#root), { childList: true, subtree: true });注意监听范围不要太大。#root是知乎整个应用挂载节点监听它虽然能覆盖所有动态组件但每次知乎内部按钮状态变化都会触发回调回调里要做一层过滤判断当前Collection里是否真的增加了新的回答节点。数据量上去后建议缩小监听范围到回答列表的外层容器这样回调触发频率大幅下降页面也更流畅。3.3 回答解析与去噪只留下有价值的内容块从回答节点提取内容时目标是干净正文去掉推荐卡片、话题标签、无关链接。核心解析思路是定位内容容器再移除非正文子节点。以单条回答为例一个典型的List-item节点内部包含作者信息区、内容区、操作栏赞同、评论、分享按钮、推荐内容卡片等。内容区在class里常出现RichText这是知乎的富文本渲染容器。提取时我直接取RichText的innerText这个字段会保留段落结构又不会带进去HTML标签适合导出Markdown。需要注意标题和正文的区分。回答内容里如果含有用户自己插入的图片图片的alt文本有时会被拼进innerText导出时就会出现一段莫名奇妙重复的文字这部分要做一次清洗把连续的空格和换行压缩掉。作者、点赞数、评论数这些元数据在AuthorInfo组件和操作栏中提取按需保留不需要太多。function extractAnswer(item) { const authorNode item.querySelector(.AuthorInfo-name); const contentNode item.querySelector(.RichText); const likeNode item.querySelector(.VoteButtonContainer span); if (!contentNode) return null; return { author: authorNode ? authorNode.innerText.trim() : 匿名用户, content: contentNode.innerText.replace(/\n{3,}/g, \n\n).trim(), likes: likeNode ? parseInt(likeNode.innerText, 10) : 0, url: window.location.href.split(#)[0] }; }这段代码里的replace(/\n{3,}/g, \n\n)是我在实际使用中加上的知乎的富文本渲染会保留大量空行直接导出到Markdown里段落间距大得离谱这个正则能把多行空行压缩成单空行阅读体验好很多。3.4 翻页到底该如何驱动两种触发机制的组合策略处理知乎问题回答列表时需要你先滚动到页面底部触发加载再等新回答渲染再继续滚动如此反复。单纯设置一个滚动事件监听回调函数会高频触发加载还没完成就再次触发容易重复请求。推荐用一个“状态机”来控制抓取流程。定义三个状态IDLE空闲、LOADING加载中、FINISHED结束。滚动触底时只有IDLE状态下才允许触发加载切换到LOADING等待新节点渲染后再切回IDLE。这样可以天然避免重复触发。let state IDLE; async function handleScroll() { if (state ! IDLE) return; const nearestBottom document.documentElement.scrollHeight - window.scrollY - window.innerHeight; if (nearestBottom 600) { state LOADING; const prevCount itemCount; window.scrollTo(0, document.documentElement.scrollHeight); await waitForNewItems(prevCount, 8000); state IDLE; } }waitForNewItems的实现方式轮询当前已收集节点数如果8秒内没有增加判定为没有更多内容置为FINISHED。加上这个超时机制后脚本能正确识别“已经加载到末尾”的情况避免无限等待。4. 数据落盘序列化方案、暂存队列与文件导出4.1 三种导出格式的取舍Markdown、JSON与CSV抓取完成后数据导出格式直接影响后续使用效率。我保留了三种导出方式按场景选择。Markdown适合直接阅读和二次编辑。把回答内容按作者、点赞数、正文结构组织导出后用Typora或VS Code打开就是一份干净的阅读文档。缺点是Markdown对图片支持有限知乎图片链接是外链需要联网才能显示离线阅读时图片会裂掉。JSON适合程序化处理。保留原始数据字段导出后可以写个小脚本做统计、转格式、入库。CSV适合表格化整理导入Excel做筛选排序方便但CSV对长文本的兼容性不好回答正文里如果包含换行符和逗号需要做转义否则列会错位。我的建议是日常阅读场景导出Markdown后续要做处理就导出JSONCSV只用于轻量清单比如回答标题、作者、点赞数这种短字段不要塞正文。4.2 暂存机制页面被刷新的最后防线知乎这种单页应用用户滚动加载大量内容后标签页内存占用高随时可能崩溃。一旦页面崩溃脚本里的数组全部丢失之前的抓取就白干了。GM_setValue本来可以做持久化但知乎内容动辄几百条每抓一条就写入一次存储频繁同步会产生明显卡顿。折中方案是“暂存队列”每抓取20条数据批量写入一次GM_setValue同时维护内存里的完整数组用于最终导出。const BATCH_SIZE 20; let cache GM_getValue(zhihu_cache, []); function flushCache() { if (cache.length BATCH_SIZE) { GM_setValue(zhihu_cache, JSON.stringify(tempStore)); tempStore []; } }这个设计投入实用后很关键有一次我抓一个三百多回答的问题滚动到一半标签页崩溃了重启浏览器打开脚本数据恢复到了前不久的暂存点只是少抓了二十来条重新滚动补抓就完了没有从头再来。4.3 文件下载两种方式与大小限制浏览器端的下载方式有URL.createObjectURL和GM_download两种。前者是纯前端方式适合中小文件生成一个临时对象URL后触发a[download]点击即可。缺点是如果数据序列化成超大JSON字符串Blob存入内存会占用较多资源文件过大时浏览器可能阻止下载。后者是油猴脚本API把数据用Blob打包后交给浏览器的下载管理器处理适合更大的数据量而且这个API不受页面JS对a[download]跨域限制的影响。知乎页面的JS环境本身很复杂它对下载行为可能会有拦截GM_download绕过页面环境更稳。使用GM_download时注意MIME类型。导出Markdown文本用text/markdown导出JSON用application/json如果MIME传错部分浏览器会强制把文件改成.download后缀还要手动重命名体验不好。5. 上线后必须处理的五个坑一次真实的翻车记录5.1 重复数据无限滚动列表的顽疾第一个遇到的坑出现在滚动抓取第二页时。翻页后新渲染的回答节点里有一部分和之前已经抓过的相同。原因是知乎的列表渲染不是严格按offset追加的。在用户快速滚动时知乎前端可能会重复渲染一部分节点或者在网络抖动时重新拉取了之前页面的数据。解决思路是给每条回答建立一个唯一指纹。如果回答URL存在直接用URL去重如果抓的是收藏夹列表项没有唯一URL就用“作者加内容前50个字符”做哈希。哈希去重还有个额外好处如果一条回答被修改过指纹变化脚本会把它当新内容抓到这在备份场景下反而是优点。5.2 顺序错乱异步请求返回时间不一致导致的内容颠倒第二个坑比较隐蔽。抓到第50条之后我发现一旦打开“按时间排序”的回答列表内容顺序会出现跳变中间会漏几条。排查后发现原因是知乎回答列表的翻页不完全是顺序请求。前端预加载机制会在你滚动还没到末尾时就提前请求下一页多个接口并发返回渲染顺序和回答实际顺序不完全一致。我最初逐条保存存储顺序等于渲染顺序渲染顺序一旦错乱导出的内容顺序也就乱了。解决办法是提取节点时把节点自带的>
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑