兰 亭 墨 苑
期货 · 量化 · AI · 终身学习
首页
归档
编辑文章
标题 *
URL 别名 *
内容 *
(支持 Markdown 格式)
知乎是中文平台最高质量内容的讨论平台,内容质量相当高,值得花大力气进行适配抓取;微信是国内最大的私域知识分发平台,最常用的沟通工具,是减少知识收集摩擦的最好通道; # 从 URL 到知识资产:我的全自动内容抓取、转换与归档系统深度解析 > 本文详细拆解一套个人知识管理自动化体系的全链路设计——从手机一键提交链接,到浏览器插件安全抓取、AI 增强处理,最终沉淀至私有 NAS 形成结构化笔记。文章将逐一剖析 URL 提取、智能分流、高可靠性抓取、Markdown 转换、AI 增强、去重存储与跨组件同步等核心环节的工程考量与实现细节。 --- ## 1. 背景与动机 在信息过载的时代,我们每天都在接触大量值得保存的文章——知乎回答、微信公众号推文、博客长文。传统的收藏方式存在诸多痛点: - **平台绑定**:知乎收藏夹可能因账号异常或内容删除而丢失;微信收藏没有导出的出口。 - **手动存档成本高**:复制粘贴、整理格式、打标签、存到笔记软件,每一步都是摩擦。 - **跨设备壁垒**:手机上看到的文章,要转到电脑上才能整理,流程断裂。 - **封闭平台反爬**:知乎对非登录用户的限制极其严格,常规爬虫难以稳定抓取全文。 经过长期摸索和逐步迭代,我构建了一套以 **Chrome 扩展 + Cloudflare Worker + Knowly 守护进程** 为核心的个人知识管道。它能够: 1. **手机一键提交**:通过快捷指令将任意链接发送至中继 Worker。 2. **智能分流**:Worker 自动识别知乎链接与非知乎链接,放入不同队列。 3. **安全抓取**:Chrome 扩展利用浏览器的真实登录态抓取知乎正文;Knowly 守护进程负责微信公众号等通用网页。 4. **内容转换**:HTML 自动转换为 Markdown,保留图片、标题、加粗、链接等格式。 5. **AI 增强**:自动生成标签、摘要、质量评分,并整理内容结构。 6. **自动归档**:通过 SSH 同步至私有 NAS,按日期存储,三层去重保证无重复。 7. **离线可靠**:网络中断时暂存本地,恢复后自动重试,游标机制防止重复消费。 本文将深入剖析每个环节的技术方案、工程考量和优化历程,分享一条从 URL 到知识资产的完整自动化路径。 --- ## 2. 系统全景:三大组件与数据流向 ### 2.1 架构概览 系统由三个独立组件协作完成: - **Cloudflare Worker** (`knasync`):作为消息中继,提供队列服务。 - **Chrome 扩展** (`zhihu-assistant`):运行在用户浏览器侧,负责知乎文章的抓取。 - **Knowly 守护进程**:运行在 Mac 本地,负责剪贴板监听、通用网页抓取、AI 处理及 NAS 归档。 整体数据流可概括为: ``` 手机(快捷指令) → Worker(/submit) → 自动分流 → 知乎队列 / 通用队列 ↓ ↓ Chrome 扩展(pull) Knowly Relay(pull) ↓ ↓ 抓取知乎正文 抓取通用网页 ↓ ↓ 格式化为 Markdown 格式化为 Markdown ↓ ↓ push 结果至 Worker 的 results 队列 ↓ Knowly ResultPuller(pull results,游标增量) ↓ 去重检查 → AI 标签/摘要 → SSH 归档至 NAS ``` ### 2.2 组件职责边界 | 组件 | 核心职责 | 关键能力 | |------|---------|---------| | **Worker** | 消息队列、内容分流 | 双队列设计、自动识别知乎 URL、结果广播、游标分页 | | **Chrome 扩展** | 知乎内容抓取 | 复用浏览器 Cookie、支持 API 和页面解析两种方式、HTML→MD 转换 | | **Knowly** | 通用抓取、AI 增强、归档 | 多源抓取(HTTP/无头)、三层去重、SSH 同步、Outbox 离线队列 | 这种分工使得每个组件的逻辑高度内聚,且各自的优劣势得到互补:Chrome 扩展有登录态但无法处理非知乎链接;Knowly 可处理任何 URL 但受限于无状态环境。Worker 的中继作用完美解决了两者的竞争问题。 --- ## 3. URL 提取:从手机到队列 ### 3.1 手机端提交 手机端使用 iOS 快捷指令,支持从分享菜单直接发送链接。快捷指令构造一个 POST 请求: ```http POST /submit Host: ***.workers.dev X-Auth-Key: <secret> Content-Type: application/json {"content": "https://www.zhihu.com/question/12345678/answer/987654321"} ``` Worker 的 `/submit` 接口会先尝试解析 JSON,提取 `content` 或 `url` 字段;若解析失败,则直接当作纯文本处理。这种兼容性设计让终端不必关心格式细节。 ### 3.2 Worker 端自动分流 Worker 收到内容后,调用 `isZhihuUrl(content)` 进行判断: ```javascript function isZhihuUrl(content) { const trimmed = content.trim(); if (!/^https?:\/\//i.test(trimmed)) return false; if (!/^https?:\/\/([\w-]+\.)?zhihu\.com\//i.test(trimmed)) return false; const validPatterns = [ /zhihu\.com\/question\/\d+\/answer\/[\w-]+/, /zhihu\.com\/p\/\d+/, /zhuanlan\.zhihu\.com\/p\/\d+/, ]; return validPatterns.some(pattern => pattern.test(trimmed)); } ``` - 若是知乎链接 → 写入 KV 键 `queue_zhihu`(消费者:Chrome 扩展) - 否则 → 写入 `queue_general`(消费者:Knowly) 队列存储格式为 JSON 数组,每个元素包含 `{ t: timestamp, c: content }`。队列大小受 `MAX_QUEUE_SIZE = 50` 限制,并设置 `expirationTtl: 600`(10 分钟),防止异常堆积。 ### 3.3 消费者拉取 两个消费者分别以不同参数请求 `/pull`: - Chrome 扩展:`GET /pull?queue=queue_zhihu` - Knowly:`GET /pull?queue=queue_general` Worker 采用 **竞争消费模型**:每次拉取后立即删除队列(`await env.CLIPBOARD_STORE.delete(targetKey)`),确保每条消息只被一个消费者处理。这种方式牺牲了“可靠投递”的保证(若消费者在处理前崩溃,消息会丢失),但在个人场景下可接受,且避免了复杂的 ACK 机制。 --- ## 4. 知乎文章高可靠性抓取 ### 4.1 为什么扩展抓取知乎最可靠? 知乎对未登录用户的限制极其严格:未登录时只能查看少量内容,频繁请求会触发验证码。传统爬虫方案如 Selenium、Puppeteer 需要模拟登录,面临账号风控、Cookie 过期等问题。 而 Chrome 扩展运行在用户真实的浏览器环境中,可以通过 `chrome.cookies.getAll({ domain: '.zhihu.com' })` 直接获取当前登录态的所有 Cookie。这些 Cookie 是知乎服务器主动下发的,扩展只是“借用”了这一有效凭证,因此请求不会触发任何风控,成功率接近 100%。 ### 4.2 抓取流程:API 优先 + 页面回退 扩展的 `fetchZhihuContent` 函数实现了双通道抓取: **步骤一:API 优先** - 回答类:调用知乎内部 API `/api/v4/questions/{questionId}/answers/{answerId}` - 文章类:调用 `/api/v4/articles/{articleId}` 这些 API 返回 JSON 数据,包含完整的标题、正文 HTML、作者信息。使用 API 的优势是数据干净、无需解析 DOM。 **步骤二:页面回退** 如果 API 失败(例如未登录时某些 API 会拒绝访问),扩展会抓取完整 HTML 页面,并尝试从中提取内嵌的 `initialData` 或 `__INITIAL_STATE__`。知乎将页面初始状态以 JSON 形式嵌入在 `<script>` 标签中: ```javascript let match = html.match(/<script id="js-initialData"[^>]*>(.+?)<\/script>/); if (match) { const jsonStr = match[1] .replace(/</g, '<') .replace(/>/g, '>') .replace(/&/g, '&') .replace(/"/g, '"') .replace(/'/g, "'"); const data = JSON.parse(jsonStr); // 从 data.initialState.entities.answers 等路径中提取内容 } ``` 这种双重抓取策略让扩展即便在 Cookie 部分失效或 API 限流时仍能工作。 ### 4.3 HTML → Markdown 转换 知乎正文是 HTML 格式,扩展中实现了 `htmlToMarkdown` 函数进行转换,保留: - **图片**:优先取 `data-actualsrc` / `data-src` / `src`,转换为 `` - **标题**:`<h1>` ~ `<h6>` → `#` 标记 - **加粗/斜体**:`<strong>/<b>` → `**text**`,`<em>/<i>` → `*text*` - **链接**:`<a href="">` → `[text](href)` - **代码**:`<pre><code>` → 代码块,行内 `<code>` → `` `code` `` - **引用**:`<blockquote>` → `> text` - **列表**:`<li>` → `- item` 转换时还对 HTML 实体(` `, `<`, `—` 等)进行了解码,确保输出为纯文本 Markdown。 ### 4.4 结果推送 处理完成后,扩展将 Markdown 正文通过 `pushTaskResult` POST 到 Worker 的 `/push` 接口,存入 `results` 广播队列。请求体示例: ```json { "content": "# 文章标题\n\n> 作者:xxx\n> 来源:https://...\n\n正文 Markdown..." } ``` `results` 队列同样受 `MAX_QUEUE_SIZE` 限制,并设置 `expirationTtl: 3600`(1 小时自动过期)。 ### 4.5 推送频率控制 为避免触发 IMA 等下游 API 的频率限制,扩展在连续推送之间设置了可配置的间隔(默认 8 秒)。如果遇到 403 错误,还会实行指数退避重试(最多 3 次)。连续失败 2 篇后,自动暂停 2 小时,防止账号被风控。 --- ## 5. 通用网页抓取与 Knowly 的增强 ### 5.1 Knowly 的 Relay Puller Knowly 守护进程内部的 `relay.Puller` 定时从 `queue_general` 拉取内容。拉取器在初始化时指定了 `?queue=queue_general`,因此它只会获取非知乎链接(包括微信公众号文章、普通博客、纯文本等)。 ### 5.2 通用网页抓取:`fetcher.FetchPage` 对于 URL,Knowly 调用 `fetcher.FetchPage` 抓取页面标题和正文。该函数通过标准 HTTP 客户端发送请求,使用精心设置的一组请求头(`User-Agent`, `Accept`, `Accept-Language` 等)来模拟真实浏览器访问。针对微信文章,还实现了专门的解密逻辑(识别 `JsDecode` 中的 `\xNN` 转义内容)。 同时,Knowly 支持**知乎链接的后门入口**(尽管现已通过队列分流屏蔽):如果 Relaya 拉取到了知乎链接,它会尝试通过 **智谱 web_reader MCP 接口** 抓取,但该接口有额度限制且不稳定。这也是我们要将知乎完全移交给扩展的原因。 ### 5.3 剪贴板直通 除了从 Relaya 队列拉取,Knowly 的核心功能之一是**剪贴板监听**。当用户在电脑上复制任何文本或图片时,Knowly 会立即捕获并启动同步。对于剪贴板中的 URL,它会异步抓取网页标题,增强内容。 ### 5.4 URL 增强与内容格式化 无论是 Relaya 拉取还是剪贴板捕获,如果内容是 URL,Knowly 都会尝试抓取页面标题: ```go if fetcher.IsURL(content) { ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) info, err := fetcher.FetchPage(ctx, urlStr) if err == nil && info != nil { enhanced = content + "\n\n# " + info.Title + "\n\n" + info.Content } } ``` 这使最终保存到 NAS 的文件不再是孤零零的链接,而是附带了标题和正文摘要的 Markdown 文档。对于本次优化后的架构,**通用链接的正文抓取就由 Knowly 独立完成**。 ### 5.5 AI 增强 如果启用了 AI 模块,Knowly 会在同步前对文本进行结构化处理: - **标签**:生成 3~5 个关键词 - **摘要**:50 字以内的中文摘要 - **评分**:内容质量打分(0~10 分),系统日志等低质量内容自动识别 - **整理**:将原文重新组织为清晰的 Markdown 格式 AI 调用通过 OpenAI 兼容 API 进行,支持自定义提示词模板(通用、代码、学术、极简模式)。处理成功后,结果会嵌入到归档文件的 YAML Front Matter 中: ```markdown --- sync_time: 2026-04-27 08:36:05 source: clipboard content_hash: a1b2c3d4e5f6... tags: [AGI, 语言学, Transformer] summary: "从符号学角度论证中文更利于大模型权重收敛" score: 9 --- ``` 一个关键优化是:**去重检查前置于 AI 处理**。Knowly 在调用 AI 之前先通过 `ExistsByHash` 检查 NAS 上是否已有相同内容,若命中则直接跳过,避免浪费 AI Token。这在 Result puller 拉取大量历史结果时尤其重要。 --- ## 6. 转换细节:从 HTML 碎片到高质量 Markdown ### 6.1 知乎正文的 HTML 清洗 知乎正文 HTML 包含大量样式类、`<figure>` 容器、懒加载图片等。扩展的 `htmlToMarkdown` 转换过程需处理以下场景: 1. **图片懒加载**:知乎使用 `data-actualsrc` 存放真实图片 URL,`src` 往往是占位图。转换时优先匹配 `data-actualsrc`,其次是 `data-src`,最后才是 `src`。 2. **嵌套结构**:如 `<a><img></a>`,先提取图片再处理链接。 3. **代码块**:知乎的代码块通常是 `<pre><code>...</code></pre>`,但行内代码是 `<code>` 直接包裹。需要区分处理。 4. **引用块**:知乎的 `<blockquote>` 内可能包含多段落,转换时每行前加上 `> ` 标记。 ### 6.2 Knowly 的正文提取 对于通用网页,Knowly 的 `extractContent` 实现了逐级回退: 1. 微信正文提取(专用正则) 2. 语义化标签:`<article>` → `<main>` → `<div class="content">` 3. 回退到 `<body>` 4. 清除脚本、样式、注释,压缩空白 ### 6.3 Markdown 格式标准化 无论来自扩展还是 Knowly,最终入库的 Markdown 都遵循统一格式: ```markdown # 标题 > 作者:xxx > 来源:URL 正文内容... --- *自动同步于 2026-04-27 08:36:05* ``` Knowly 在写入 NAS 时还会添加 YAML Front Matter 元数据(同步时间、来源、哈希值、AI 标签/摘要/评分),便于未来使用静态站点生成器(如 Hugo)直接构建知识库。 --- ## 7. 保存与同步:三层去重与离线可靠性 ### 7.1 目标存储 所有内容最终通过 SSH 同步至 NAS(或任何 Linux 服务器),按日期组织目录结构: ``` ~/knowly_archive/ ├── 2026/ │ ├── 04/ │ │ ├── 27/ │ │ │ ├── 083558_0427_083444_INFO_RelayResul.md │ │ │ ├── 083142_自己发明的梗被广为流传是种怎样的体验.md │ │ │ └── ... ``` 文件名由时间戳(`HHMMSS`)+ 内容前缀构成,方便人类浏览。内容前缀提取时优先使用 Markdown 的一级标题,若无则取原文前 20 个字符并清理特殊符号。 ### 7.2 三层去重策略 为避免重复同步,Knowly 实现了纵深防御的三层去重: 1. **内存级**:Monitor 维护 `lastHash`,相同内容的连续复制直接过滤(读写锁保护)。 2. **持久化级**:进程重启后从 `status.json` 恢复 `lastHash`,防止重启后立即重复同步刚同步的内容。 3. **远程索引级**:每次写入前,检查 NAS 当天目录下的 `.knowly_hashes` 索引文件。该文件使用 `grep -qxF` 进行 O(1) 精确匹配,比遍历目录快几个数量级。**此检查现在被前置到 AI 处理之前**,从而避免了无意义的 API 消耗。 此外,Result puller 拉取结果时也通过游标避免重复消费(见第 8 节)。 ### 7.3 离线队列与重试 如果 SSH 连接中断导致同步失败,Knowly 会将内容存入本地 **Outbox**(`JSONL` 格式),并立即执行 `client.ForceReset()` 断开僵死连接。 守护进程启动后及每 5 分钟,会自动执行 `drainOutbox()`,尝试重新同步积压条目。成功则删除,失败则保留并记录错误。Outbox 中不仅存储原始内容,还保留 AI 元数据,确保重试成功后归档文件仍包含完整的 AI 信息。 ### 7.4 历史记录 每次成功同步(或去重跳过),Knowly 会将条目记录在本地 `history.jsonl` 中,包括 ID、预览、类型、NAS 路径和 AI 标签。该文件采用惰性计数与阈值压缩策略,保持文件大小可控(超过 2 倍上限时压缩至最新 1000 条),并通过逆序块读取优化查询性能。 --- ## 8. 同步策略:避免重复消费与信息丢失 ### 8.1 队列消费模式 Worker 的输入队列(`queue_zhihu` / `queue_general`)采用“读完即删”的竞争消费。这种模式天然保证了**一个 URL 只被一个消费者处理**,避免了两个 Knowly 实例或扩展与 Knowly 同时处理同一链接。 输出队列(`results`)则设计为**广播模式**:允许多个消费者(如手机查看结果、Knowly 归档)都可以通过游标增量拉取,数据不会因为第一个消费者拉走就消失。两种模式的结合恰到好处地平衡了资源竞争与结果共享的需求。 ### 8.2 游标持久化防止重启回溯 Knowly 的 ResultPuller 使用 `result_cursor.txt` 文件持久化最后一次拉取的游标(`results` 队列中条目的时间戳)。启动时从文件恢复游标,后续拉取只请求 `since` 参数大于此游标的新条目。 ```go rp.cursorFile = filepath.Join(config.GetConfigDir(), "result_cursor.txt") // 启动时加载游标 if data, err := os.ReadFile(rp.cursorFile); err == nil { fmt.Sscanf(string(data), "%d", &rp.cursor) } // 拉取后更新 if len(data.Items) > 0 { rp.cursor = data.Cursor os.WriteFile(rp.cursorFile, []byte(strconv.FormatInt(data.Cursor, 10)), 0644) } ``` 这一机制彻底解决了早期版本中每次重启 Knowly 都会拉取全部历史结果、重复 AI 处理和去重检查的问题。 ### 8.3 去重前置 在 `syncText` 函数中,**远程去重检查被提前到 AI 处理之前**。如果计算出的内容哈希在 NAS 当天目录的 `.knowly_hashes` 中已存在,则直接返回,整个 `syncText` 流程提前结束。这对于 Result puller 尤其重要,因为 `results` 队列中的很多内容可能之前已通过剪贴板同步过。 ### 8.4 背压保护 在 Monitor 剪贴板轮询中,如果同步速度跟不上复制速度(例如 SSH 网络拥塞),`itemChan` 缓冲区(默认大小 10)满时,新的 Payload 会被丢弃并记录警告。这确保了 Monitor 永远不会阻塞用户操作。 同样,Chrome 扩展在处理知乎收藏夹批量推送时,通过可配置的推送间隔(默认 8 秒/篇)和错误暂停机制(连续失败 2 篇暂停 2 小时)实现流量控制,避免触发 API 风控。 --- ## 9. 安全与隐私考量 ### 9.1 认证与授权 - Worker 的 `/submit`、`/pull`、`/push`、`/results` 接口均通过 `X-Auth-Key` 进行基于 `safeCompare` 的时序安全认证。 - Knowly 通过 SSH 密钥与 NAS 通信,私钥不离开本机。 - Chrome 扩展使用的 Cookie 仅限本机浏览器,不传输至任何第三方服务器。 ### 9.2 数据主权 所有抓取的内容直接存储于用户的私有 NAS,不经过任何云存储服务。AI 处理虽然调用外部 API,但可配置为本地模型(如 Ollama)或私有代理,确保数据不外泄。 ### 9.3 Shell 注入防护 Knowly 在通过 SSH 执行远程命令时,所有路径参数均经过严格的 Shell 转义(单引号包裹,内部单引号转义为 `'\''`),彻底杜绝了因文件名特殊字符导致的注入风险。 --- ## 10. 优化历程回顾:从简单复制到全自动管道 最初,Chrome 扩展的功能仅仅是**在知乎页面上添加一个复制按钮**,把文章内容复制到剪贴板。但很快发现: 1. **禁止转载限制**:部分文章无法直接选中复制。 2. **需手动操作**:每次都要记得点按钮,不够自动化。 3. **复制后仍需手动粘贴到笔记应用**,无法直接归档。 于是逐步演进: - **阅读模式**:通过侧边栏渲染正文,绕过禁止复制限制,同时提供舒适的阅读环境。 - **收藏夹监控**:定时检查收藏夹,自动推送新文章到 IMA。 - **任务 API 集成**:通过 Worker 队列与 Knowly 联动,实现跨设备跨平台的内容自动同步。 - **智能分流**:发现 Knowly 抓知乎不稳定后,引入双队列设计,让扩展主导知乎抓取。 - **游标持久化与去重前置**:解决重启回溯和 AI 浪费问题,进一步打磨性能。 每一次迭代都在解决实际痛点,而不是凭空设计,最终形成今天这套高度自动化的系统。 --- ## 11. 总结与展望 这篇文章详细拆解了一套个人知识自动归档系统的全链路实现,涵盖了从 URL 输入、智能分流、高可靠性抓取、内容转换增强,到去重存储和同步策略的完整技术细节。其核心价值在于: - **高可靠性**:利用浏览器原生认证状态抓取知乎,成功率堪比手动操作。 - **全自动化**:手机提交后,全程无需任何人工干预,直至内容沉淀为带 AI 摘要的 Markdown 笔记。 - **隐私可控**:数据完全私有,不依赖任何第三方存储。 - **可扩展性**:双队列分流架构可轻易扩展至更多平台(如 B 站、GitHub Issue 等),只需在 Worker 和扩展中添加对应的识别和处理逻辑。 对于个人开发者而言,这套方案的最大启发或许是:**充分利用你已有的基础设施**(浏览器、NAS、Cloudflare Worker),通过巧妙的消息队列分拆和客户端专用化,可以构建出比许多商业产品更可靠、更自主的个人知识管理系统。 --- *全文完*
配图 (可多选)
选择新图片文件或拖拽到此处
标签
更新文章
删除文章