Skip to content

About

把网页存成 Markdown(只留正文)或 HTML(保留原站样式、去广告,多图可翻页,图片可转 base64 或转存图床)。适配公众号、微博、小红书、X、知识星球、Gist、豆瓣。CLI + Agent Skill

Resources

Stars

4 stars

Watchers

0 watching

Forks

Repository files navigation

web-extract

把网页存成两种格式:

  • 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 存一个能单独打开的 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(上传函数))。 复制过去别在下游改,改这里再同步。

Agent Skill

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 的 description
  • bin/web-extract.mjs 的 HELP 第一行(--help 打出来的那句)
  • GitHub 仓库描述:gh repo edit seyeeL/web-extract --description "…"(不在仓库里,push 之后单独改)

许可

PolyForm Noncommercial 1.0.0:个人使用、学习、研究、非营利组织可以免费用和改,商用要先找我授权。

About

把网页存成 Markdown(只留正文)或 HTML(保留原站样式、去广告,多图可翻页,图片可转 base64 或转存图床)。适配公众号、微博、小红书、X、知识星球、Gist、豆瓣。CLI + Agent Skill

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages