把网页存成两种格式:
- Markdown(
--md):只留正文,标题、作者、发布时间单独给出 - HTML(
--html):保留原站样式,去掉广告和脚本,存成一个文件。多图轮播存下来照样能左右翻;图片可以塞进文件(base64)或者转存到自己的图床
大部分文章页用默认规则就行;公众号、微博、小红书、X、知识星球、GitHub Gist、豆瓣广播和话题有专门的站点规则。
Save a web page as Markdown (main content only, with title, author and publish date) or as HTML (the site's own styles kept, ads and scripts removed, one file; image carousels still switch, images embedded as base64 or re-hosted on your own image host). Default rules cover most article pages; WeChat, Weibo, Xiaohongshu, X/Twitter, zsxq, GitHub Gist and Douban have their own site rules.
一份代码三种用法:
- 命令行:
web-extract parse <url> --md,静态抓取或用 headless Chrome 渲染后提取 - 库:浏览器里直接 import(不依赖任何包),Node 里配 linkedom
- Agent Skill:
skills/web-extract/SKILL.md,教 AI 什么时候、怎么用这个 CLI,设计照搬 kepano/obsidian-skills 的 defuddle
从 simplenote-clipper 的页面解析部分拆出来,那个扩展现在用的是这里的副本。
要 Node 22+。
npm install -g github:seyeeL/web-extract
# 或者不装,直接跑
npx github:seyeeL/web-extract parse <url> --md--browser / --attach 还要一个本机 Chrome(或 Edge)。找不到时用 CHROME_PATH 环境变量指一个。
web-extract parse <url> --md # Markdown
web-extract parse <url> --json # 元信息 + markdown + html
web-extract parse <url> -p title # 单个字段
web-extract parse <url> --md -o out.md # 写文件
web-extract parse <url> --html -o out.html # 带原站样式的单文件 HTML(见下)
web-extract parse <url> # 清洗后的正文 HTML--json 的字段:
| 字段 | 说明 |
|---|---|
url |
来源地址。跟完重定向;X 的 status 链接去掉 ?s=46&t=… 这类分享参数 |
title |
标题。没有独立标题的站点(微博、推文)取正文开头 |
author |
作者 |
publishedAt |
发布时间,原样给出(站点给什么格式就是什么格式,X 是本机时区的 YYYY-MM-DD) |
siteName |
站点名,og:site_name,没有就用域名 |
domain |
域名,去掉 www. |
markdown |
正文 Markdown。站内相对链接和图片地址已补成绝对地址(按页面地址或 <base href>) |
html |
清洗后的正文 HTML |
-p 接受上面的字段名,另有 published、site、content 三个别名。
| 方式 | 参数 | 适合 |
|---|---|---|
| 静态 | (默认) | 服务端直出 HTML 的页面:博客、新闻、文档站。几百毫秒 |
| 浏览器 | --browser |
正文靠 JS 渲染的:X、微博、公众号。起一个一次性 profile 的 headless Chrome,十几秒 |
| 连已开的浏览器 | --attach [--port 9222] |
要登录态的:小红书、知识星球。不导航、不读 cookie,只在已打开的 tab 里跑提取 |
静态抓取有 30 秒超时(--timeout)和 20 MB 上限,按响应头或 <meta> 识别 GBK 这类编码。
浏览器模式页面 load 之后再等 5 秒(--wait)给首屏 JS 把正文塞进 DOM;headless 的 UA 会换成
同版本的普通 Chrome,不然 X 直接 403。
--attach 要那个浏览器带调试端口启动,并且已经打开了目标页:
chrome --remote-debugging-port=9222 --user-data-dir=<一个专门的目录>
web-extract parse "<url>" --attach --md正文不到 100 个字符时 stderr 会提示下一步怎么办(换 --browser、换 --attach,或者告诉你被跳到了哪个落地页),stdout 照常输出。
--html 存一个能单独打开的 HTML,长得和原页面一样:默认整页,只去掉广告、脚本和看不见的元素;
--article 只存正文(导航侧栏评论区都不要)。站点 CSS 只留页面上用到的那部分,图片不转 base64,留远程地址:
web-extract parse <url> --browser --html -o page.html --assets page_files # 整页,图片存到本地
web-extract parse <url> --html --article -o page.html # 只存正文豆瓣、微博这类有防盗链的站点离开原站图片打不开,小红书的图片地址带签名、几天后失效。图片有三种存法:
| 参数 | 做法 |
|---|---|
--assets <目录> |
图片(含 CSS 背景图)下载到 HTML 旁边,用相对路径引用 |
--embed-images |
图片塞进 HTML(base64),只出一个文件,手机上好传,体积大不少 |
--upload-images |
转存到自己的图床(阿里云 OSS,或者 PicGo 这类自定义命令),HTML 里换成图床地址:一个文件、体积小、长期有效。配置放 ~/.config/web-extract/.env,不进仓库,见 docs/image-host.md |
图片轮播(Swiper,小红书的多图笔记)存下来换成纯 CSS 的版本:左右箭头、页码、圆点都能点,不用脚本。
小红书整条笔记怎么存、要注意什么:docs/sites.md 的小红书一节。
小红书的评论区存的是页面上已经加载出来的那些;加 --load-comments(配 --attach / --browser)先在页面上
往下翻、点开楼中楼再存,每步隔 3–5 秒、最多 200 条,弹验证就停(小红书有风控,翻太快账号会被要求扫码验证)。
体积和抓不到的样式表报在 stderr。怎么裁、实测体积、边界:docs/styled-html.md。
其它参数:
| 参数 | 作用 |
|---|---|
--wx |
用微信 UA(公众号链接需要) |
--user-agent <UA> |
自定义 UA |
--headed |
浏览器模式开真窗口(挪到屏幕外),不用 headless |
--plain |
Markdown 去掉所有格式标记,只留文字、图片、列表和表格。给排版坏掉的页面用:公众号编辑器有把三千字正文塞进一个 <h1> 的,照标题渲染会压成一行 |
--no-replies |
不收主贴底下的回复(目前只有 X 的规则收回复,默认收) |
--load-comments |
--html 配 --browser / --attach 时先在页面上把评论往下翻、楼中楼点开再存(目前只有小红书) |
--url <地址> |
输入是本地文件或标准输入(-)时,告诉提取器原始地址,决定命中哪条站点规则 |
退出码:0 成功,1 抓取或运行出错,2 参数用错。
浏览器里(扩展的 content script、书签脚本),src/ 下的文件不依赖任何包:
import { extractArticle, htmlToMarkdown } from './src/index.js';
const article = extractArticle(document, location.href, { withReplies: true });
// article.root 是清洗过的克隆节点,页面本身不会被改动
const markdown = htmlToMarkdown(article.root);extractDocument(document, url, options) 一步给出和 CLI --json 一样的对象。
Node 里:
import { extractHtml, extractUrl, styledHtml } from 'web-extract/node';
const fromHtml = extractHtml(html, 'https://example.com/post');
const fromUrl = await extractUrl('https://example.com/post', { plain: false });
// 带样式的单文件 HTML(要联网抓样式表,所以是异步的)
// articleOnly: true 只存正文,不给就整页(去广告);assetsDir / embedImages 存图片
const { html: page, warnings, stats } = await styledHtml(html, 'https://example.com/post');
// 转存图床:读 ~/.config/web-extract/.env 的配置,见 docs/image-host.md
import { createImageUploader } from 'web-extract/image-host';
await styledHtml(html, url, { uploadImage: createImageUploader() });浏览器渲染:
import { extractUrlInBrowser } from 'web-extract/browser';
const result = await extractUrlInBrowser('https://x.com/u/status/123');浏览器模式会把 src/site-rules.js、html2md.js、extract.js、snapshot.js、result.js、dump.js 去掉 import / export 拼成一段脚本注进页面,所以这几个文件的 import 只能写单行,顶层名字不能互相重名(tests/cli.test.mjs 里有用例守着)。
扩展这类没有打包工具的下游可以直接复制 src/extract.js、src/html2md.js、src/site-rules.js 三个文件用。
要在浏览器里存带样式的 HTML,再加 src/snapshot.js(页面里取材料)、src/css.js、src/styled-core.js(拼文件,
不依赖 Node,parseDocument 传 DOMParser,图片用 embedStore() / uploadStore(上传函数))。
复制过去别在下游改,改这里再同步。
skills/web-extract/SKILL.md 告诉 AI:要网页正文时优先用这个 CLI 而不是 WebFetch,什么站点要加 --browser / --attach,抓空了怎么办。
Claude Code 里当插件装:
/plugin marketplace add seyeeL/web-extract
/plugin install web-extract@web-extract
其它工具(Codex 等)把 skills/web-extract 目录链接或复制到对应的 skills 目录即可。
大部分文章页走默认规则就能抓:先按 article、main、.post-content 这类语义选择器猜正文容器,猜不中就按「段落文字量 × (1 - 链接密度)」打分,最后在克隆体上删噪声(导航、推荐、评论区……)。
默认规则对付不了的站点写了专用规则(src/site-rules.js):
| 站点 | 要点 |
|---|---|
| 微信公众号 | 普通图文、纯文字分享、贴图三种版式;正文挂在 share 类名上也不误删 |
| 微博 | 九宫格配图换原图;没有标题,取正文开头 |
| 小红书 | 正文分段靠 \n;发布时间从 __INITIAL_STATE__ 取;要登录态。--html 时多图能翻、评论区可以先翻开再存 |
| X / Twitter | 按 URL 框出主推;引用推文、回复排成引用段;视频留封面;日期从推文 id 算 |
| 知识星球 | 独立详情页和主页详情浮层都只收那一条主题;正文分段靠 \n;要登录态 |
| GitHub Gist | 多文件都收,不带页头和评论;时间是创建时间(页头那个是「Last active」) |
| 豆瓣广播 | 只管广播详情页;配图从脚本里的 JSON 造出来;转发的原广播排成引用段;回应要 --browser |
| 豆瓣话题 | 只管 /topic/<id>/;一个 <p> 里装好几段、靠 \n 分段,保住;侧栏、话题标签、赞那排不进来 |
每个站点怎么取、实测过哪些页面、踩过什么坑、怎么加新站点:docs/sites.md。
web-extract dump "<url>" [--browser] # 页面里文字量最大的容器,正文到底在哪
web-extract parse "<url>" --json # 提取选中了什么两者对不上就是选容器的问题(改 src/extract.js 的 PREFERRED 或 scoreNode,或者给站点写 root);选对了但 markdown 很短,就是被噪声过滤误删了(改 ALWAYS_JUNK / WEAK_JUNK,或者在规则里用 keep 豁免)。
- 默认规则是按页面结构猜的。 论坛、瀑布流、SPA 这类页面会漏,遇到一个验一个写站点规则。
- 噪声过滤按 class / id 命名猜,会误伤。 命名带
share、nav、footer这类弱证据词但文字量大、不是链接堆的容器会保留;正文容器要是被命名成related/comments就一定会丢。 - 静态模式只看服务端 HTML。 JS 渲染的正文要
--browser;要登录的要--attach。 - 公众号会限频。 同一台机器短时间请求多了会跳人机验证页(
wappoc_appmsgcaptcha),这时只能在自己的浏览器里打开后--attach。
npm ci
npm test测试用 Node 原生 test runner。html2md 用手搓的假节点(tests/fake-dom.mjs),提取和站点规则用 linkedom 跑离线 DOM,不依赖网络和登录态。
加站点、加功能时,这 7 处对外的描述要一起改(站点列表、能做什么),漏一处就会有地方还停在旧版本:
README.md:开头两段(中英文)、「站点规则」那张表、参数说明docs/sites.md:兼容域名清单和站点那一节skills/web-extract/SKILL.md:frontmatter 的description(AI 靠它决定用不用这个 skill)和用法表package.json的description.claude-plugin/plugin.json的description、keywords,.claude-plugin/marketplace.json的descriptionbin/web-extract.mjs的HELP第一行(--help打出来的那句)- GitHub 仓库描述:
gh repo edit seyeeL/web-extract --description "…"(不在仓库里,push 之后单独改)
PolyForm Noncommercial 1.0.0:个人使用、学习、研究、非营利组织可以免费用和改,商用要先找我授权。