这两年我越来越受不了浏览器收藏夹了。收藏了一堆网页,真正要找的时候却搜不到,好不容易翻出来,打开一看不是链接失效就是页面改版,内容早就不是当初那样子了。后来我把思路换了一下,与其“收藏一个网址”,不如“下载一份内容”。这个 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”
很多人会用浏览器的 Ctrl+S 把网页存下来,但那样得到的 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/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", } response = requests.get(url, headers=headers, timeout=20, verify=True) 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_comments=False, include_tables=True, output_format="markdown", with_metadata=True, ) if extracted: return {"success": True, "content": extracted} bare_html = trafilatura.extract(html, output_format="html") 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(parents=True, exist_ok=True) 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, encoding="utf-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 前置元数据。这样笔记工具打开时,文章的标签、创建时间、来源链接全都能自动化呈现。
我实际使用中发现,这个组合配合全文搜索功能,比任何浏览器插件的“稍后读”都好用,资料管理效率明显提升。
最后再分享一点个人体会
这套技能包我从第一版到最终稳定用了很长时间,中间踩了不少坑。如果你打算亲手搭一套,我最大的建议是永远不要低估网页结构的复杂性。你看到的是十篇文章的共性结构,但你保存的是互联网上那剩下的百分之十的畸形页面。清洗和兜底方案不是锦上添花,它们是整个系统的地基。
另外一个值得养成的习惯是“保存时多想一步”。打开链接看到内容,你只有一瞬间决定要不要保存。这个瞬间里,如果操作足够简单、足够快,你就更愿意把它留下来。技能包的设计目标就是把这个动作压缩到几乎无感。与其花时间翻找网址,不如让内容直接落到磁盘里,想用的时候随手就能搜到。别再用浏览器收藏了,把内容真正留在自己手里,这种感觉只有试过才知道。