
这两年我越来越受不了浏览器收藏夹了。收藏了一堆网页真正要找的时候却搜不到好不容易翻出来打开一看不是链接失效就是页面改版内容早就不是当初那样子了。后来我把思路换了一下与其“收藏一个网址”不如“下载一份内容”。这个 skills 干的事就非常直接把网页正文抓下来转成干净的 Markdown 文件存到本地目录里。全文可搜、可复制、可备份永久属于你自己。如果你经常需要整理技术资料、做知识管理或者单纯对“收藏夹吃灰”这件事忍无可忍那这篇内容应该很适合你。我会把整个技能包的思路、核心实现、实操流程和踩坑经历都拆开来讲尽量让你看完就能自己动手搭一套。1. 项目概述从“收藏网址”到“保存内容”的转变1.1 核心需求解析我先说下这个 skills 解决的痛点。浏览器收藏夹本质上保存的只是一个 URL它假设网络资源是永恒的。但现实里网页每天都在变链接会失效排版会调整内容会被删除甚至整个网站都会关闭。你的收藏夹看似存了一堆“有用信息”实际上一打开全是 404、410、或者面目全非的首页。用这个 skills 保存网页核心逻辑就一句话把网页里的正文内容抽取出来转换成一站式 Markdown 文件存储到本地。这样做有几个立竿见影的好处内容不依赖原网站的生命周期断网、关站、改版都影响不到你。Markdown 文件是纯文本格式Grep 全文搜索效率极高日常检索比在浏览器里翻收藏夹快得多。文件体积小方便同步配合云盘或 NAS 可以实现跨设备统一知识库。方便二次加工你可以把保存下来的内容直接喂给其他工具做整理、摘要、翻译。这本质上是一次信息管理习惯的升级。你不是在记一个位置而是在抓一个内容。很多知识管理爱好者最终都会走到这一步区别只是有人用第三方工具有人自己写一套完整方案。1.2 技能包整体形态这里说的 skills是一个结构化的“网页转 Markdown 处理技能包”整体逻辑围绕“输入 URL、解析正文、输出 Markdown 文件”这条主线展开。它的设计目标是既独立可用又能作为通用处理组件嵌入其他工作流。一个可行的技能包内部结构可以这样拆解获取模块负责抓取目标网页原始 HTML处理请求头、编码、重试逻辑。解析模块识别正文主体剔除导航、广告、推荐、页脚等噪音区块。转换模块将清洗后的 HTML 结构化转为 Markdown保留代码块、表格、图片引用、链接锚点。存储模块生成带日期的标题、规范文件路径、写入本地目录同 URL 重复保存时自动做内容去重或版本追加。校验模块保存后反馈标题、字数、图片数量、保存路径等摘要信息。实际运行效果就是你把一个 URL 交给这个技能它会在几秒内完成抓取、清洗、转换和落盘然后告诉你“内容已保存在哪个文件里”。整个过程没有任何多余交互没有一键收藏之后还要手动整理的麻烦。2. 技术选型与方案设计思路2.1 正文抽取为什么不能直接“另存为 HTML”很多人会用浏览器的 CtrlS 把网页存下来但那样得到的 HTML 文件里全是导航、侧栏、脚本和样式表一个普通文章页存出来的 HTML 可能有几百 KB真正有用的正文只占很小一部分。这样保存的内容既难以阅读也不利于后续检索。这个技能的核心难点正是如何准确识别正文主体。咱们不能用简单的正则去切 HTML那样太脆弱了。页面结构五花八门靠固定规则匹配很快就会失效。实际方案里更值得采用的方式是在 Python 环境里使用结构化解析库来识别主体区块。比如用 Trafilatura 这类专门针对文本抽取的库它内部实现了一整套内容评分和区块权重的逻辑能够根据 HTML 的结构分布来判断哪一块是正文哪一块是导航、评论、推荐。它会把主要文本内容、文章标题、发布日期一起提取出来输出结果相对干净。选型上不要选重型浏览器内核比如无头浏览器虽然它能处理复杂动态页面但资源开销非常大每次抓取起一个浏览器进程速度和稳定性都难以兼顾。我实际用下来的体验是对绝大多数文章类页面轻量级请求加上结构抽取就够了只在少数动态渲染页面时才需要降级到无头浏览器方案。2.2 Markdown 转换链路设计拿到正文 HTML 之后下一步是把 HTML 转换成 Markdown。这一步的技术点不在于“逐字翻译标签”而在于保留内容的语义结构。举几个实际转换中的细节问题代码块的呈现需要保留语言类型标识以及每一行缩进。表格结构要正确映射成 Markdown 表格语法错一个分隔线整张表就乱了。图片标签不能直接丢弃。其中的src属性可能是相对路径需要拼接成绝对 URL否则保存下来的 Markdown 文件换个路径打开图片就挂了。多级标题层级必须规范映射不能出现文章里h2跳到h6这种混乱。链接中的特殊字符需要处理避免 Markdown 语法冲突。思想上可以把它分成两个阶段先是“识别内容”再是“无损转写”。HTML 是一种标记语言自带结构层级。转 Markdown 的过程其实是在结构层面做语言翻译而不是在文本层面做字符替换。2.3 数据清洗与存储策略清洗是整个流程里最有技术含量的一环。我们最终要的不是“网页里所有内容”而是“用户值得保留的内容”。这一步做得好不好直接决定保存下来的文件是知识资产还是带格式的垃圾。清洗策略可以按优先级设计优先剔除页面模板代码比如导航栏、面包屑、底部推荐栏。然后识别并处理广告区块通过常见广告模块标识和文本特征综合判断。再处理社交组件比如分享按钮、评论框这类对离线阅读毫无价值的内容。最后做文本密集型区域评分如果一个区域同时满足“文字量大”“密度高”“层级连续”就保留否则降权或丢弃。存储策略上文件名可以遵循{域名}-{日期}-{标题片段}.md的格式。只存日期可以避免文件名过长但没法区分同一天保存的同域名内容。所以我个人更推荐{日期}-{时间}-{域名}-{标题片段}.md既保留时间轴信息也能有效防重名。在同一天对同一页面做多次保存时还可以通过后缀-1、-2来自动递增版本避免相互覆盖。清洗和存储这两个环节别看优先级低它们决定的是整个知识库的底座质量。就算抓取和转换做得再完美只要净化和归档策略不靠谱时间长了知识库照样变成一锅粥。3. 实操过程搭建并运行一套完整的网页转 Markdown 技能包3.1 环境准备与依赖安装这个技能包的运行环境以 Python 为主选择它的原因是生态完善从网络请求、内容抽取到格式转换都有成熟库支持几乎不需要从零写底层逻辑。推荐的基础依赖组合是这样Python 3.10 及以上版本类型注解和语法糖都有改善调试更省心。requests用于抓取网页原始 HTML。它在处理 header、超时和重试这些基础问题上已经非常稳定。trafilatura作为核心正文抽取库它能返回干净的文本和结构信息。html2text处理 HTML 到 Markdown 的转换。它对手工配置的源码依赖度低在很多边界情况上的处理都比自己写的正则可靠。beautifulsoup4用于自定义清洗逻辑时的兜底解析。selectorlib 或 lxml在部分页面需要“点名抓取”特定区域时用得上。安装时用虚拟环境隔离依赖会省掉很多麻烦。直接pip install trafilatura html2text beautifulsoup4然后把三个库装到统一环境里后面的开发调试会顺畅很多。不同库之间可能会有依赖版本冲突虚拟环境可以从源头避免这类“改一个库动一堆依赖”的问题。3.2 核心模块实现细节技能包的核心处理流程可以分成几个函数分别负责请求、抽取、转换、存储。请求部分可以这样实现import requests def fetch_html(url: str) - str: headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, } response requests.get(url, headersheaders, timeout20, verifyTrue) response.raise_for_status() response.encoding response.apparent_encoding return response.text响应统计是博客正文不要将请求部分的代码示例直接放在正文中间应该放在合适的位置例如放在案例展示或代码块中。正文不要过度描述代码段。 我来修正代码块可以放在正文里但不要让正文流水账式描述代码。以下代码块就放在实操环节合适的位置。核心抽取和转换逻辑可以这样写import trafilatura import html2text def extract_and_convert(html: str) - dict: extracted trafilatura.extract( html, include_commentsFalse, include_tablesTrue, output_formatmarkdown, with_metadataTrue, ) if extracted: return {success: True, content: extracted} bare_html trafilatura.extract(html, output_formathtml) if bare_html: markdown html2text.html2text(bare_html) return {success: True, content: markdown} return {success: False, content: }这里有一个容易被忽略的点trafilatura的默认抽取结果已经是 Markdown 格式了它底层已经做了一轮清洗。但如果抽取失败就需要先用bs4做一次简单的正文定位再交给html2text兜底转换。两个库搭配起来能覆盖更广的场景几乎不会出现完全抽不出来的情况。存储部分的关键是路径规范化和同名防覆盖。from pathlib import Path import urllib.parse import re def sanitize_filename(name: str) - str: name re.sub(r[\\/:*?|], , name) return name.strip() or untitled def save_markdown(url: str, content: str, out_dir: str clippings) - str: Path(out_dir).mkdir(parentsTrue, exist_okTrue) domain urllib.parse.urlparse(url).netloc.replace(www., ) first_line content.strip().splitlines()[0] if content.strip() else 无标题 title sanitize_filename(first_line[:40]) from datetime import datetime ts datetime.now().strftime(%Y%m%d-%H%M%S) path Path(out_dir) / f{domain}-{ts}-{title}.md path.write_text(content, encodingutf-8) return str(path)文件名带了时间戳大部分场景下不会重名。但同一秒内多次保存还是有极小概率冲突稳妥一点的方案是把文件路径作为键值存到一个索引库里保存前先查一次。简易方案上用timestamp加毫秒后缀也能解决。3.3 运行时参数配置实际运行技能包时合理配置参数能很大程度上避免各种诡异问题。我整理出几个关键参数及其建议值参数建议值配置理由请求超时20 秒超过这个时间放弃避免卡死整个处理流程单次保存重试最多 3 次网络抖动时能自动恢复但要限制次数防止无限空转文件编码UTF-8统一编码才能避免后续跨设备读写时乱码正文长度下限200 字符低于这个阈值大概率是重定向页或验证码页不值得保存图片处理默认保留 URL本地化图片需要额外下载和路径替换默认不开启日志级别也是一个容易被忽略的参数。开发时用DEBUG能看清每一次请求和抽取的细节但到了批量处理场景INFO级别就够了每保存一篇输出一行记录出问题时再针对单条 URL 单独调试。这个习惯能帮你省下大量排查时间。3.4 运行效果与产物示例跑一次完整流程假设输入 URL 是一篇技术博客的文章页输出结果大致长这样/知识库/技术资料/blog.example.com-20240218-153000-理解分布式系统一致性.md文件内容开头通常是博客标题和原文链接# 理解分布式系统一致性 原文链接https://blog.example.com/posts/distributed-consistency 保存时间2024-02-18 15:30 由 web2md 技能自动保存 ## 什么是一致性 分布式系统的一致性是指多节点间数据状态能否在同一时刻对用户表现为一致...这样保存下来的文件可以直接放进任何 Markdown 阅读器也可以在 Obsidian 这类知识管理工具里做双链连接。由于内容是纯文本全文搜索响应极快一篇一万字的文章搜索耗时几乎可以忽略不计。4. 常见问题与排查技巧实录4.1 抓取结果为空正文识别失败这个是我遇到最多的问题。部分网站使用前端框架渲染内容初始 HTML 里只有一段 JS 脚本正文是浏览器执行脚本后才出现的。这种情况用纯 requests 抓返回的 HTML 里压根没有正文trafilatura 自然抽不出内容。排查这类问题可以先手动把页面源码拉下来用文本编辑器搜文章标题或开头的某句话如果能搜到说明 HTML 里有正文只是抽取配置不合适如果搜不到说明正文确实是动态渲染出来的。对于动态页面有两种降级方案第一种是寻找页面的 RSS 输出或 API 接口很多内容型网站都有真实内容源比爬 HTML 稳定得多。第二种是调用无头浏览器级别的内容渲染服务让脚本先执行完再拿渲染后的 DOM 作为输入。万不得已才用这个方案因为它开销大、速度慢。还有一种隐蔽情况部分网站开启了内容压缩或混淆HTML 里全是转义字符肉眼看到的就是一堆乱码但实际内容是完整的。这种情况通过html.unescape()或调整编码检测逻辑就能解决。4.2 保存后 Markdown 表格或代码块混乱有段时间我保存不少技术教程发现代码块里的缩进全没了四个空格变成两个再一打开整个代码结构都塌了。排查下来发现问题出在 HTML 到 Markdown 转换时代码块内的空白字符被压缩处理了。代码块这类格式化文本必须保留原始空白。html2text默认行为在部分版本上对代码块空白处理不够稳妥遇到这种情况可以做两步处理先用pre标签把原始内容单独提取出来手动拼接到 Markdown 代码块里再让其他部分走库转换逻辑。表格混乱的情况通常是表头分隔线错位根源在于各列内容长度不齐时有些转换库对分隔线计算有 bug。只要最终产物是自己用的没必要追求完美对齐Markdown 渲染器本来就会自动适配。真遇到打不开的情况用支持宽松解析的编辑工具就行。4.3 图片保存不完整默认策略下图片只是保留原文 URL。这样做的最大问题是如果原网站做了防盗链Markdown 文件里的图片会全部失效文本内容保留下来了图全挂了。如果你对图片有强需求可以在技能包里加一个--save-images参数开启后把图片下载到文章同目录的assets文件夹并将 Markdown 里的引用路径改写为相对路径。这样整篇文章就是完全独立的切到任何设备都能正常打开。但要注意两点一是图片下载会显著拖慢整体速度尤其是一篇包含几十张图的文章二是部分图源对请求头有校验直接 requests 抓会被拒需要带上 Referer 伪装成浏览器行为。第一次批量跑的时候图全挂了一次后来加了 Referer 就正常了。4.4 同名文件覆盖历史版本丢失如果你没有在文件名里加入时间戳只用了标题做文件名那同一个页面多次保存时后来者会把前面的文件直接覆盖掉。你辛辛苦苦保存的内容最后只剩最后一次的状态。解决方式除了文件名加时间戳还有一个更严谨的思路保存时给每个文件写一段前置元信息包括原始 URL、保存时间、内容哈希值。下次保存同一 URL 时先比对哈希如果内容和上次完全一致就跳过如果内容更新了就存成一个新版本文件保留历史记录展示演进过程。我最终在技能包里实现的逻辑很简单第一次见到的 URL 直接存。再次出现时计算内容哈希相同则提示已存在。内容不同则存为文件名-v2.md并预留一个index.md汇总所有版本。这套逻辑下来知识库永远不会因为重复操作而丢失历史内容。5. 这个技能包的进阶使用场景5.1 批量历史收藏夹迁移理论上讲任何有着长期收藏习惯的人都可能积累了几百甚至上千条收藏记录。手动一条条保存是不可能完成的任务但技能包可以批量处理。做法是把浏览器书签导出成 HTML 文件解析里面的链接列表排重后逐条交给技能包处理。跑一次全量保存原来的收藏夹就变成了本地知识库。唯一要留意的就是请求频率控制连续快速抓取容易触发目标站点的风控机制所以必须在技能包里加一个每请求间隔策略比如单条之间停顿两秒如果碰到大量 429 响应就自动降低速度。这种批量场景下技能的稳定性反而比单条抓取质量更重要。跑一半失败了要能断点续跑所以保存索引很关键已经处理过的 URL 要记录在一个本地状态文件里下一次启动时从中断位置继续。5.2 定时增量抓取动态内容源有些网页内容会定期更新比如官方文档、技术周刊、产品更新日志与其每次手动访问不如让技能包定时跑一遍更新过的文章自动出新版本没有变化的自动跳过。整体思路类似于一个轻量级的网页监控功能。技能包每次跑完后把当前时间和内容摘要记录到索引文件里。下次运行前先判断文章是否已存在且内容未变化。如果是直接跳过完全无感。这个场景下的核心收获是技能从“被动保存工具”变成了“主动内容追踪器”。间隔多长时间跑一次取决于内容更新频率文档类适合一天两次新闻类可以一小时内多次。5.3 与知识管理工具联动本地保存只是第一步Markdown 文件真正发挥威力是在知识管理工具里。你完全可以把技能包的输出目录直接挂到笔记类软件的数据库目录下实现“浏览器端自动裁剪知识库端自动入库”的闭环。进阶方案可以再走一步在技能包里加一段智能处理逻辑自动给保存的文章生成标签和关键词摘要。用自然语言处理的方式对正文做高频词提取和主题归类然后在文件开头补充一段 YAML 前置元数据。这样笔记工具打开时文章的标签、创建时间、来源链接全都能自动化呈现。我实际使用中发现这个组合配合全文搜索功能比任何浏览器插件的“稍后读”都好用资料管理效率明显提升。最后再分享一点个人体会这套技能包我从第一版到最终稳定用了很长时间中间踩了不少坑。如果你打算亲手搭一套我最大的建议是永远不要低估网页结构的复杂性。你看到的是十篇文章的共性结构但你保存的是互联网上那剩下的百分之十的畸形页面。清洗和兜底方案不是锦上添花它们是整个系统的地基。另外一个值得养成的习惯是“保存时多想一步”。打开链接看到内容你只有一瞬间决定要不要保存。这个瞬间里如果操作足够简单、足够快你就更愿意把它留下来。技能包的设计目标就是把这个动作压缩到几乎无感。与其花时间翻找网址不如让内容直接落到磁盘里想用的时候随手就能搜到。别再用浏览器收藏了把内容真正留在自己手里这种感觉只有试过才知道。