简介:面向需要自动化处理中文主流平台网页内容的开发者,这套Claude Code Skill以网页读取为核心,同时提供小红书自动化能力,覆盖微信公众号、小红书、今日头条等平台,支持自动发布、自动评论与自动检索,可无缝接入OpenClaw、Codex、CC等工具链。资源包共11个文件,主体为6个Python脚本,分别负责URL识别、微信文章转换、内容保存等任务,辅以Markdown使用说明、JSON配置与gitignore文件,整体体积仅22KB,轻量易部署且模块边界清晰。目前已有73人参与学习,适合希望快速构建内容采集与小红书运营流程的开发者或自动化爱好者。包内url-reader-main目录按功能拆分了读取器核心逻辑,既可直接调用现成脚本处理公众号链接,也能在此基础上扩展其他平台适配器,为二次开发提供了完整可参考的样例结构。
1. 智能网页内容读取器是什么:一个拆开就能用的 Claude Code Skill
很多人拿到公众号文章链接时,顺手就把它丢给 Claude 让它读,结果 Claude 回了一句“这是一篇文章”,因为模型根本看不到页面里的正文。智能网页内容读取器这个 Claude Code Skill 解决的就是这个断层:它先按域名和 URL 特征识别平台,再把微信公众号、小红书、今日头条这类中国主流平台的网页正文抓下来、清洗成干净的 Markdown,最后交给模型去总结、改写或提取信息。它不是一个爬虫框架,而是一套 SKILL.md 加按站点配置的解析规则。适合批量把链接变成结构化素材、需要持续维护多平台解析的开发者。
2. 读取链路拆解:Skill 怎么拿到网页并吐出 Markdown
2.1 Claude Code 的 Skill 机制:靠 description 触发,不靠命令
Claude Code 的 Skill 本质是一组放在固定目录下的文件,核心是SKILL.md。Claude Code 会扫描用户的~/.claude/skills/和项目里的.claude/skills/目录,把每个 Skill 的 frontmatter 里的name和description索引进上下文。当用户输入的内容和某个 Skill 的描述匹配时,这个 Skill 才会被加载进当前会话。所以写 Skill 最要紧的不是给它起多好听的名字,而是 description 里要把触发场景写清楚,最好把平台名、动作词、URL 形式都列进去。
举个例子,如果 description 只写“读取网页”,模型会在很多无关场景误触发;如果写“当用户提供微信公众号、小红书、今日头条链接,要求总结或提取内容时使用”,触发就会精准得多。description 就是接口,接口写得越具体,Skill 的命中率越高。这也是为什么读取器这类工具适合做成 Skill 而不是写进 system prompt——Skill 只在需要时才加载,不额外占用上下文。
2.2 一条链接的完整旅程:识别、抓取、解析、清洗四步
我在本地把读取器拆成了四个阶段。第一步识别平台,拿到 URL 后先解析 host 和路径,再用正则匹配平台特征,比如mp.weixin.qq.com/s/是公众号文章,xiaohongshu.com/explore/是小红书笔记,toutiao.com/article/是头条文章。第二步抓取,用配置好的 User-Agent 请求页面,三平台基本都能用静态 HTML 拿全,部分场景需要额外带 Referer。第三步解析,这块差异最大:公众号正文藏在#js_content容器里,小红书正文在window.__INITIAL_STATE__这个 JSON 里,头条则在__NEXT_DATA__或 JSON-LD 里。第四步清洗,把所有 script、style、iframe 标签删掉,把连续空行压缩,再把 HTML 转成 Markdown 输出。
这四个阶段里最容易被低估的是第四步。你从页面里拿到的原始 HTML 可能包含导航、推荐位、二维码、评论区,这些噪声对模型理解正文没有任何帮助,反而会增加 token 消耗、干扰总结质量。清洗不是可选项,而是读取器能不能用起来的关键。常见做法是先用选择器锚定正文容器,比如#js_content、article、main,只在容器内做文本提取;容器拿不到时再降级到整页清洗。
2.3 requests 够用,还是必须上无头浏览器?
一开始做这类工具,容易被“这平台有反爬”吓到,第一反应就是上 Playwright 或 Puppeteer。实际做下来,我的结论是:先用 requests 把静态 HTML 拿回来,不行的平台再考虑无头浏览器。公众号、今日头条的正文基本都在服务端渲染的 HTML 里;小红书虽然大部分正文在 JS 变量里,但那也是静态 script 标签里的字符串,requests 就能拿到,不需要真跑一遍浏览器。无头浏览器最大的问题不是慢,而是容易被风控识别,启动 Chrome 的指纹特征本身就比 requests 更像机器人。
我一般会把无头浏览器作为二级降级方案,而不是主链路。优先走“静态请求 + JSON 状态提取”,这条链路失败时再用无头浏览器渲染一次。这个设计能让读取器保持轻量,也方便在本地快速调试。你只需要在 Python 环境里装 requests、BeautifulSoup 和 lxml 三个库,就能跑通全部平台。
3. 从零搭建读取器:SKILL.md、解析器与站点配置三件套
3.1 目录结构与 SKILL.md:三行 frontmatter 决定触发时机
先建目录。我把这套 Skill 放到~/.claude/skills/web-reader/下,里面只放三个文件:SKILL.md、reader.py、site_config.json。目录结构保持极简,不要堆一堆无关文件,Claude Code 在加载 Skill 时会把整个目录的内容都纳入上下文,文件越少越省 token。
~/.claude/skills/web-reader/ ├── SKILL.md ├── reader.py └── site_config.jsonSKILL.md 是这个 Skill 的入口,Claude Code 靠它判断“什么时候该用”。我把触发条件写在 description 里,把执行步骤写在正文里,让模型按流程调用解析器:
--- name: web-content-reader description: 当用户提供微信公众号、小红书、今日头条等中国主流平台的网页链接,要求读取、总结、改写或提取正文内容时使用。也适用于用户粘贴平台文章 URL 询问“这篇讲了什么”的场景。 --- # 网页内容读取器使用说明 1. 收到用户提供的链接后,先调用 `python3 reader.py <url>` 抓取并解析正文。 2. 解析结果是 JSON,字段包含 title、content、author、publish_time、image_urls。 3. 把 content 字段中的 Markdown 文本交给用户,或按用户要求基于它做总结和改写。 4. 如果解析结果中出现 "error" 字段,直接向用户说明读取失败,不要尝试用链接内容猜测文章含义。这段 frontmatter 里没有写allowed-tools字段,因为新版 Claude Code Skill 的触发主要靠 description 匹配,工具权限由父级会话统一管理。如果你用的是旧版本,可以在 frontmatter 里补上tools: ["bash"]或对应允许执行的工具列表。正文里的步骤一定要写清楚“先跑命令再总结”,否则模型可能会跳过解析脚本,直接凭链接文本瞎猜内容。
3.2 核心解析器:用 Python 把 HTML 变成可读 Markdown
reader.py是整个 Skill 的执行核心。它的职责是接收 URL,输出 JSON。我把它拆成四个函数:加载配置、匹配平台、抓取页面、解析清洗。代码不长,但每一步都要稳。
#!/usr/bin/env python3 import json import re import sys from urllib.parse import urlparse import requests from bs4 import BeautifulSoup def load_config(path="site_config.json"): with open(path, "r", encoding="utf-8") as f: return json.load(f) def match_platform(url, config): """按 URL 特征匹配平台。命中返回平台 key,否则返回 default""" parsed = urlparse(url) target = (parsed.netloc + parsed.path).replace("www.", "") for key, conf in config["platforms"].items(): for pattern in conf["url_pattern"]: if re.search(pattern, target): return key return "default" def fetch_html(url, conf, default_ua): """抓取页面。UA、超时、重试次数都从配置里读,不硬编码""" headers = {"User-Agent": conf.get("user_agent", default_ua)} if conf.get("referer"): headers["Referer"] = conf["referer"] resp = requests.get(url, headers=headers, timeout=conf.get("timeout", 10)) resp.raise_for_status() resp.encoding = resp.apparent_encoding or "utf-8" return resp.text def clean_html_fragment(content_html): """把正文里的 HTML 片段清洗成 Markdown 文本""" soup = BeautifulSoup(content_html, "lxml") for tag in soup.find_all(["script", "style", "noscript", "iframe"]): tag.decompose() text = soup.get_text("\n", strip=True) return re.sub(r"\n{3,}", "\n\n", text) def extract_by_selector(html_text, selector): """按 CSS 选择器提取文本,没有命中时返回空串""" if not selector: return "" soup = BeautifulSoup(html_text, "lxml") node = soup.select_one(selector) return node.get_text("\n", strip=True) if node else "" def main(): url = sys.argv[1] config = load_config() key = match_platform(url, config) conf = config["platforms"].get(key) html_text = fetch_html(url, conf, config["default_ua"]) result = { "platform": key, "url": url, "title": extract_by_selector(html_text, conf["selectors"].get("title")), "content": clean_html_fragment( extract_by_selector(html_text, conf["selectors"].get("content")) ), "author": extract_by_selector(html_text, conf["selectors"].get("author")), "publish_time": extract_by_selector(html_text, conf["selectors"].get("publish_time")), } print(json.dumps(result, ensure_ascii=False, indent=2)) if __name__ == "__main__": main()核心逻辑是先按 URL 命中平台,再从配置里读出该平台的 UA 和选择器,最后把选中容器里的 HTML 片段清洗成纯文本。fetch_html 里有一行resp.encoding = resp.apparent_encoding or "utf-8",这是处理中文编码的关键。有些老平台页面声明的是 GBK,但没有 HTTP 头字段,requests 默认会按 ISO-8859-1 解码,抓回来就是乱码。apparent_encoding 会根据字节特征猜测真实编码,能覆盖大部分情况。
3.3 站点配置拆解:把平台差异赶出代码
解析逻辑里不能写死任何平台选择器,否则每适配一个新平台就要改主流程代码。我把所有平台差异收敛到site_config.json里,新增平台时只改配置,不动 Python。
{ "default_ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", "platforms": { "wechat": { "url_pattern": ["mp\\.weixin\\.qq\\.com/s"], "selectors": { "title": "h1.rich_media_title", "content": "#js_content", "author": "#js_name", "publish_time": "#publish_time" }, "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", "referer": "https://mp.weixin.qq.com/" }, "default": { "url_pattern": [], "selectors": { "title": "h1", "content": "article, main" } } } }这个配置文件里,url_pattern 是正则列表,用来做平台匹配;selectors 是 CSS 选择器组,分别对应标题、正文、作者、发布时间;user_agent 和 referer 用于绕过反爬校验。default 平台是兜底,当 URL 不属于任何已知平台时,它会尝试拿article或main容器。这样处理后,主流程对平台的感知降到了最低,新增平台的过程变成了“加一段配置,跑一次测试”。
4. 微信/小红书/今日头条适配:三个平台的选择器与正则细节
4.1 微信公众号:UA 校验与 #js_content 正文容器
公众号文章是三个平台里结构最稳定的,服务端渲染完整,正文就在#js_content这个 div 里。但它最烦人的地方是风控。直接用默认 requests UA 去抓,很容易被微信识别成机器请求,返回一个“环境异常”的验证页面。解决办法是带上完整的浏览器 UA,并把 Referer 设置成https://mp.weixin.qq.com/。这两项都写进配置里,不放在代码中。
微信公众号的解析器还需要做一步验证页检测。验证页的特征是标题变成“环境异常”,或者页面里出现“访问过于频繁”这类关键词。如果不做检测,抓回来的是一个没有正文的空壳,后续总结出来的内容全是“该内容无法显示”。在抓取函数里加一个判空逻辑:如果正文容器长度小于 50 个字符,就认为触发风控,返回错误信息,提示用户稍后重试或换个入口链接。
# 微信公众号解析核心逻辑,附加在 reader.py 的 main 流程中 # 微信正文最小阈值设为 50 字,低于这个值基本是验证页或空模板 content_text = extract_by_selector(html_text, conf["selectors"]["content"]) if len(content_text) < 50: result["error"] = "wechat_risk_control" result["content"] = "" else: result["content"] = clean_html_fragment(content_text)50 这个阈值是我自己定的。有的纯图片文章正文确实很短,但公众号编辑器会生成一堆空段落,文本提取后不会少于 50 字。如果抓到的正文是 30 个字,大概率是验证页。这个参数你可以按自己的样本调整,核心思路是先判断是否命中反爬,再决定要不要继续解析,避免把验证页当正文喂给模型。
4.2 小红书:正文在 window.INITIAL_STATE里,不在 HTML 里
小红书笔记的 HTML 看起来结构完整,但你把正文提取出来往往是空的。这是因为正文数据并不渲染在 DOM 里,而是以 JSON 字符串的形式藏在 script 标签里。解析小红书的第一步是在 HTML 里找到window.__INITIAL_STATE__或window.__INITIAL_SSR_STATE__,然后提取=后面的 JSON 对象。
# 小红书解析函数,放在 reader.py 里,main 流程按平台分发调用 def parse_xhs_state(html_text, note_id): # 兼容新旧两种变量名,SSR 版是近期改版后的命名 m = re.search( r"window\.__INITIAL_(?:SSR_)?STATE__\s*=\s*(\{.*?\})\s*</script>", html_text, re.S) if not m: return {"error": "xhs_state_not_found"} data = json.loads(m.group(1)) # 常见数据路径:note.noteDetailMap[note_id].note note_map = data.get("note", {}).get("noteDetailMap", {}) if note_id not in note_map: return {"error": "xhs_note_missing"} note = note_map[note_id]["note"] tag_list = [tag["name"] for tag in note.get("tagList", [])] # 图片 URL 常带 !nweb 或 x-oss-process 水印参数,要去掉才能看原图 image_urls = [] for img in note.get("imageList", []): url = img.get("urlDefault") or img.get("url") url = re.sub(r"!.*$", "", url) image_urls.append(url) return { "title": note.get("title") or note.get("displayTitle", ""), "content": note.get("desc", ""), "author": note.get("user", {}).get("nickname", ""), "publish_time": note.get("time", 0), "tags": tag_list, "image_urls": image_urls, }这个函数里有几个参数值得细说。兼容新旧变量名用了一个非捕获分组(?:SSR_)?,这样无论小红书前端改成__INITIAL_STATE__还是__INITIAL_SSR_STATE__,都能命中。图片 URL 的水印参数通常以!开头或在 URL 里带x-oss-process,去掉尾部参数能拿到原图地址。还有一点,小程序的笔记 ID 可能过长,匹配不上 noteDetailMap 的 key,需要在外面单独做一次遍历,比对noteId字段;常见的做法是先按 URL 里的 ID 找,找不到就去 noteDetailMap 里遍历所有 key。
小红书还有个登录墙问题。在未登录状态下,部分热门笔记会返回重定向到登录页的 HTML。这个页面里同样有__INITIAL_STATE__,但 noteDetailMap 是空的。遇到这种情况,解析结果里会出现xhs_note_missing错误。我没有太好的绕过办法,通常是提示用户换一个链接,或者用分享到微信后生成的短链作为输入。
4.3 今日头条:NEXT_DATA与 JSON-LD 双通道降级
今日头条的文章页分为两类。PC 端详情页用 Next.js,正文数据嵌在<script id="__NEXT_DATA__" type="application/json">里;另一类页面使用 JSON-LD 结构化数据,正文在<script type="application/ld+json">里。我在解析器里做了双通道降级:先找__NEXT_DATA__,找不到就找 JSON-LD,再找不到才回退到 CSS 选择器。
# 头条解析函数,同样由 main 流程按平台分发 def parse_toutiao(html_text): # 通道一:Next.js 数据,最新版头条详情页走这里 m = re.search( r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', html_text, re.S) if m: data = json.loads(m.group(1)) page_props = data.get("props", {}).get("pageProps", {}) article = page_props.get("articleInfo", {}) return { "title": article.get("title", ""), "content": clean_html_fragment(article.get("content", "")), "author": article.get("authorName", ""), "publish_time": article.get("publishTime", ""), } # 通道二:JSON-LD 结构化数据,适合部分旧版文章页 m = re.search(r'<script type="application/ld\+json">(.*?)</script>', html_text, re.S) if m: data = json.loads(m.group(1)) return { "title": data.get("headline", ""), "content": clean_html_fragment(data.get("articleBody", "")), "author": data.get("author", {}).get("name", ""), "publish_time": data.get("datePublished", ""), } return {"error": "toutiao_structured_data_not_found"}头条__NEXT_DATA__里的articleInfo.content本身是 HTML 片段,里面是段落、图片、标题标签的混合体。直接把这个 HTML 字符串传给模型不是不行,但会夹杂很多没用的标签,而且图片的src可能带有尺寸参数。我一般先过一次clean_html_fragment,再把图片 URL 单独提取出来放到image_urls字段,让模型在总结时只看文字部分。
三个平台适配完,我把选择和入口整理成一张表,方便对照排查。
| 平台 | URL 特征 | 正文入口 | 主要风险 | 关键参数 |
|---|---|---|---|---|
| 微信公众号 | mp.weixin.qq.com/s/ | #js_content | 环境异常验证页,UA 校验 | 完整 UA、Referer、正文最短长度 50 |
| 小红书 | xiaohongshu.com/explore/ | window.INITIAL_STATE | JSON 变量名改版、图片水印、登录墙 | 兼容 SSR 变量名、去 ! 后缀参数 |
| 今日头条 | toutiao.com/article/ | NEXT_DATA/ JSON-LD | 两种数据格式并存 | 优先 Next.js,降级到 ld+json |
| 未知平台 | 任意 | article / main | 选择器命中率低 | 兜底配置,抓 main 容器 |
5. 网页内容读取器避坑与排查:五个高频翻车点
5.1 抓回来是空字符串,HTML 里却明明有正文
现象是解析器返回的 content 是空的,但用浏览器打开页面,正文明明就在那里。原因大概率是正文由 JavaScript 动态渲染,requests 抓到的 HTML 里只有一个空壳容器。解决方法是先检查页面里有没有内嵌的 JSON 状态变量,比如__INITIAL_STATE__、__NEXT_DATA__、__NUXT__;有就优先解析这些变量里的数据,而不是找 HTML 标签。这个方法对小红书和头条都适用。如果页面连 JSON 也没有,再考虑上 Playwright 渲染,但这种平台毕竟是少数。
5.2 微信链接第一次能读,第二次就返回“环境异常”
现象是同一个公众号链接,第一次抓取正常,过几分钟再抓就返回验证页。原因是微信对单个 IP 的短时间请求频率有限制,连续访问多次会触发风控。解决方法是先在代码里检测验证页特征词,检测到就立即停止解析、返回错误提示,不要返回空模板;然后再做一层请求间隔,可以把抓取函数包装成带重试和延时。我一般会设置两次请求之间至少间隔 3 秒,连续失败两次就不再重试。如果你的场景需要大量抓公众号文章,需要准备代理池,不然频率问题绕不开。
5.3 小红书正文抓回来了,图片却全挂
现象是输出结果里标题、正文都对,但图片 URL 打不开。原因是小红书的图片地址带有水印参数,常见的格式是 URL 后面跟着!nweb、!nd_dft或者x-oss-process=image/...,这些参数会把图片改造成带水印的压缩图。解决方法是把imageList每个 URL 里!后面的部分去掉,取原图地址;如果 URL 里是x-oss-process,需要用正则把该参数及其值整段删除。另外还要给图片加 Referer 头,直链访问小红书图床会返回 403,Referer 设为https://www.xiaohongshu.com/能解。
5.4 本地脚本跑通,挂到 Claude Code 里就超时
现象是python3 reader.py在终端执行一切正常,但在 Claude Code 里调用时频繁报超时,或者返回结果很慢。原因是 Claude Code 调 bash 工具默认有单次执行的时间上限,读取器里如果加了重试逻辑,一个链接来回请求 3 次,很容易把时间拖到超时。解决方法是把重试次数从 3 降到 1,把单次请求超时时间缩短到 10 秒以内,把队列里第一个请求跑对;同时把抓取和解析拆成两个独立步骤,先抓取到本地缓存,再解析,避免超时后全部重新执行。
5.5 输出里混进了导航、广告和评论区
现象是解析出的 content 不仅有正文,还带着页面底部的“相关阅读”和评论区内容。原因是选择器锚定的容器范围太宽了,比如直接选了body或整个div而非正文容器。解决方法是把选择器写具体到正文容器本身,比如公众号的#js_content;找不到容器时,可以用article、main这类语义标签兜底,但兜底结果一定要检查一遍。更稳妥的做法是在清洗函数里过滤掉 class 名包含comment、recommend、related的节点,避免评论区混入正文。
提示:所有平台的选择器都会随前端改版失效,建议每次解析后保存一条含平台、URL、选择器、正文长度的日志,改版时比对日志就能快速定位是哪个选择器挂了。
6. 投入前最后一步:验证输出质量并沉淀成通用技能
读取器写完后,别急着扔给 Claude 用。我通常准备一个验证集,每个平台找两三条代表性链接,跑脚本后人工核对几个指标:标题是否准确、正文长度是否合理、发布时间是否有值、图片是否能打开。把结果记录成一张表,用前后对比的差异来判断解析是否稳定。
| 平台 | 预期标题 | 实际标题 | 正文字数 | 图片数 | 是否通过 |
|---|---|---|---|---|---|
| 公众号文章 A | XX 产品发布 | 一致 | 4862 | 4 | 是 |
| 公众号文章 B | 团队访谈 | 环境异常 | 0 | 0 | 否 |
| 小红书笔记 A | 周末露营 | 一致 | 320 | 9 | 是 |
| 头条文章 A | 行业分析 | 一致 | 2100 | 3 | 是 |
验证发现失败后,先看是反爬还是选择器问题。公众号验证页可能是频率限制,休息几分钟重试即可;小红书抓不到内容大概率是变量名又变了,去 HTML 里搜索__INITIAL确认新命名。把这次排查经验沉淀回配置文件和代码注释,下次同类报错能少走一半弯路。
通用化改造的方向是让新增平台像填表一样简单。我的做法是在 site_config.json 里维护一个平台清单,整个 Skill 的核心不再关心“这个平台是什么”,而是把url_pattern、selectors、parser三个字段变成平台的元信息。将来适配知乎、CSDN、掘金,只需新增一段配置,并在解析器里挂一个针对性的提取函数。这也是我维护这类读取器的一点简化技巧。希望帮到你。
本文还有配套的精品资源,点击获取