@clawhub-limingfa-fd947501e8
抓取微信公众号文章并转换为 Markdown 格式。支持提取标题、作者、发布时间、封面图、正文内容(含图片、视频链接)。 当用户提到以下场景时触发: - 读取/抓取/下载微信公众号文章 - 将公众号文章转为 Markdown - 提取 mp.weixin.qq.com 链接内容 - 保存公众号文章到本地 - 微信...
---
name: wechat-mp-reader
description: |
抓取微信公众号文章并转换为 Markdown 格式。支持提取标题、作者、发布时间、封面图、正文内容(含图片、视频链接)。
当用户提到以下场景时触发:
- 读取/抓取/下载微信公众号文章
- 将公众号文章转为 Markdown
- 提取 mp.weixin.qq.com 链接内容
- 保存公众号文章到本地
- 微信文章备份、存档
关键词:微信公众号、公众号文章、mp.weixin.qq.com、微信文章抓取、微信文章转 Markdown
---
# WeChat MP Reader — 微信公众号文章抓取工具
## 功能
抓取微信公众号文章(`mp.weixin.qq.com` 链接),提取完整内容并转换为 Markdown 格式保存到本地。
## 支持提取的信息
- **标题** — 文章标题
- **公众号名称** — 作者/来源
- **发布时间** — 文章发布日期
- **封面图** — 文章封面图片链接
- **正文内容** — 完整的文章正文,包含:
- 文本段落、标题层级
- 图片(保留原图链接)
- 视频链接
- 超链接
- 列表、引用、加粗/斜体等格式
## 使用方法
### 命令行方式
```bash
python scripts/fetch_wechat_article.py <文章链接> [选项]
```
**参数:**
- `url` — 微信公众号文章链接(必需)
- `-o, --output` — 输出目录(默认:当前目录)
- `--images` — 下载图片到本地(开发中)
- `--json` — 以 JSON 格式输出元数据
**示例:**
```bash
# 基本用法
python scripts/fetch_wechat_article.py "https://mp.weixin.qq.com/s/xxxxx"
# 指定输出目录
python scripts/fetch_wechat_article.py "https://mp.weixin.qq.com/s/xxxxx" -o ./articles
# 只输出 JSON 元数据
python scripts/fetch_wechat_article.py "https://mp.weixin.qq.com/s/xxxxx" --json
```
### Python API 方式
```python
from scripts.fetch_wechat_article import fetch_article
result = fetch_article(
url="https://mp.weixin.qq.com/s/xxxxx",
output_dir="./articles"
)
print(result['title']) # 文章标题
print(result['author']) # 公众号名称
print(result['content']) # Markdown 正文
print(result['filepath']) # 保存的文件路径
```
## 输出格式
生成的 Markdown 文件结构:
```markdown
# 文章标题
**公众号**: 公众号名称
**发布时间**: 2024-01-01
**封面**: 
**原文链接**: https://mp.weixin.qq.com/s/xxxxx
---
正文内容...

[视频](视频链接)
```
## 依赖
- Python 3.8+
- `requests` 库(用于 HTTP 请求)
安装依赖:
```bash
pip install requests
```
## 注意事项
1. **网络要求** — 需要能访问 `mp.weixin.qq.com`
2. **反爬机制** — 频繁抓取可能触发微信的反爬机制,建议适当控制请求频率
3. **链接有效性** — 确保文章链接未过期或被删除
4. **图片链接** — 生成的 Markdown 中图片使用微信 CDN 原链接,长期有效性取决于微信策略
## 故障排查
| 问题 | 可能原因 | 解决方案 |
|------|---------|---------|
| 无法提取正文 | 页面结构变化 | 检查微信是否更新了页面结构 |
| 返回 403 | 被反爬拦截 | 稍后再试,或更换 IP |
| 标题为空 | 文章被删除/受限 | 确认链接可在浏览器正常打开 |
| 图片不显示 | 微信 CDN 链接过期 | 使用 `--images` 下载到本地 |
FILE:scripts/fetch_wechat_article.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
微信公众号文章抓取工具
支持提取标题、作者、发布时间、正文内容,并转换为 Markdown 格式
"""
import argparse
import json
import os
import re
import sys
from datetime import datetime
from html import unescape
from urllib.parse import unquote, urlparse
import requests
def clean_html(text):
"""清理 HTML 实体和多余空白"""
text = unescape(text)
text = re.sub(r'\s+', ' ', text)
return text.strip()
def extract_title(html_text):
"""提取文章标题"""
# 尝试多种标题匹配模式
patterns = [
r'<h1[^>]*class=["\']rich_media_title[^>]*>(.*?)</h1>',
r'<h2[^>]*class=["\']rich_media_title[^>]*>(.*?)</h2>',
r'var msg_title = ["\'](.+?)["\']\.html\(false\)',
r'activity_name = ["\'](.+?)["\']',
]
for pattern in patterns:
match = re.search(pattern, html_text, re.DOTALL)
if match:
title = clean_html(match.group(1))
# 移除 HTML 标签
title = re.sub(r'<[^>]+>', '', title)
return title
return None
def extract_author(html_text):
"""提取公众号名称/作者"""
patterns = [
r'<a[^>]*id=["\']js_name[^>]*>(.*?)</a>',
r'var nickname = ["\'](.+?)["\']',
r'"nick_name":"([^"]+)"',
r'<span[^>]*class=["\']profile_nickname[^>]*>(.*?)</span>',
]
for pattern in patterns:
match = re.search(pattern, html_text, re.DOTALL)
if match:
return clean_html(match.group(1))
return None
def extract_publish_time(html_text):
"""提取发布时间"""
patterns = [
r'<em[^>]*id=["\']publish_time[^>]*>(.*?)</em>',
r'var publish_time = ["\'](.+?)["\']',
r's="(\d{4}-\d{2}-\d{2})"',
r'"svr_time":(\d+)',
]
for pattern in patterns:
match = re.search(pattern, html_text, re.DOTALL)
if match:
time_str = match.group(1)
# 尝试解析时间戳
if time_str.isdigit():
return datetime.fromtimestamp(int(time_str)).strftime('%Y-%m-%d %H:%M:%S')
return clean_html(time_str)
return None
def extract_cover_image(html_text):
"""提取封面图 URL"""
patterns = [
r'var msg_cdn_url = ["\'](.+?)["\']',
r'<img[^>]*data-src=["\'](https://mmbiz\.qpic\.cn[^"\']+)["\'][^>]*>',
]
for pattern in patterns:
match = re.search(pattern, html_text, re.DOTALL)
if match:
return match.group(1)
return None
def html_to_markdown(html_text, base_url=None):
"""将 HTML 内容转换为 Markdown"""
md = html_text
# 1. 处理图片
def replace_img(match):
attrs = match.group(1)
# 提取 data-src 或 src
src_match = re.search(r'data-src=["\']([^"\']+)["\']', attrs)
if not src_match:
src_match = re.search(r'src=["\']([^"\']+)["\']', attrs)
src = src_match.group(1) if src_match else ''
# 提取 alt
alt_match = re.search(r'alt=["\']([^"\']*)["\']', attrs)
alt = alt_match.group(1) if alt_match else 'image'
if src:
return f'\n\n\n\n'
return ''
md = re.sub(r'<img([^>]*)>', replace_img, md)
# 2. 处理视频
def replace_video(match):
attrs = match.group(1)
src_match = re.search(r'data-src=["\']([^"\']+)["\']', attrs)
src = src_match.group(1) if src_match else ''
if src:
return f'\n\n[视频]({src})\n\n'
return ''
md = re.sub(r'<iframe([^>]*)>', replace_video, md)
md = re.sub(r'<mpvideo([^>]*)>', replace_video, md)
# 3. 处理标题
md = re.sub(r'<h1[^>]*>(.*?)</h1>', r'\n# \1\n', md, flags=re.DOTALL)
md = re.sub(r'<h2[^>]*>(.*?)</h2>', r'\n## \1\n', md, flags=re.DOTALL)
md = re.sub(r'<h3[^>]*>(.*?)</h3>', r'\n### \1\n', md, flags=re.DOTALL)
md = re.sub(r'<h4[^>]*>(.*?)</h4>', r'\n#### \1\n', md, flags=re.DOTALL)
md = re.sub(r'<h5[^>]*>(.*?)</h5>', r'\n##### \1\n', md, flags=re.DOTALL)
md = re.sub(r'<h6[^>]*>(.*?)</h6>', r'\n###### \1\n', md, flags=re.DOTALL)
# 4. 处理段落和换行
md = re.sub(r'<p[^>]*>(.*?)</p>', r'\n\1\n', md, flags=re.DOTALL)
md = re.sub(r'<br\s*/?>', '\n', md)
md = re.sub(r'<section[^>]*>(.*?)</section>', r'\n\1\n', md, flags=re.DOTALL)
# 5. 处理文本样式
md = re.sub(r'<strong[^>]*>(.*?)</strong>', r'**\1**', md, flags=re.DOTALL)
md = re.sub(r'<b[^>]*>(.*?)</b>', r'**\1**', md, flags=re.DOTALL)
md = re.sub(r'<em[^>]*>(.*?)</em>', r'*\1*', md, flags=re.DOTALL)
md = re.sub(r'<i[^>]*>(.*?)</i>', r'*\1*', md, flags=re.DOTALL)
# 6. 处理链接
def replace_link(match):
attrs = match.group(1)
text = match.group(2)
href_match = re.search(r'href=["\']([^"\']+)["\']', attrs)
href = href_match.group(1) if href_match else ''
if href and text.strip():
return f'[{text.strip()}]({href})'
return text
md = re.sub(r'<a([^>]*)>(.*?)</a>', replace_link, md, flags=re.DOTALL)
# 处理没有 href 的 a 标签
md = re.sub(r'<a[^>]*>(.*?)</a>', r'\1', md, flags=re.DOTALL)
# 7. 处理列表
md = re.sub(r'<ul[^>]*>(.*?)</ul>', r'\n\1\n', md, flags=re.DOTALL)
md = re.sub(r'<ol[^>]*>(.*?)</ol>', r'\n\1\n', md, flags=re.DOTALL)
md = re.sub(r'<li[^>]*>(.*?)</li>', r'- \1\n', md, flags=re.DOTALL)
# 8. 处理引用
md = re.sub(r'<blockquote[^>]*>(.*?)</blockquote>', r'> \1\n', md, flags=re.DOTALL)
# 9. 处理代码
md = re.sub(r'<code[^>]*>(.*?)</code>', r'`\1`', md, flags=re.DOTALL)
md = re.sub(r'<pre[^>]*>(.*?)</pre>', r'```\n\1\n```', md, flags=re.DOTALL)
# 10. 清理剩余 HTML 标签
md = re.sub(r'<span[^>]*>(.*?)</span>', r'\1', md, flags=re.DOTALL)
md = re.sub(r'<div[^>]*>(.*?)</div>', r'\1', md, flags=re.DOTALL)
md = re.sub(r'<[^>]+>', '', md)
# 11. 清理多余空白
md = re.sub(r'\n{3,}', '\n\n', md)
md = re.sub(r'[ \t]+\n', '\n', md)
return md.strip()
def extract_content(html_text):
"""提取文章正文内容"""
# 尝试多种内容匹配模式
patterns = [
# 标准模式
r'<div[^>]*id=["\']js_content[^>]*>(.*?)</div>\s*</div>\s*<script',
# 备用模式
r'<div[^>]*id=["\']js_content[^>]*>(.*?)</div>\s*<script',
# 更宽松的模式
r'<div[^>]*id=["\']js_content[^>]*>(.*?)<script[^>]*>',
]
for pattern in patterns:
match = re.search(pattern, html_text, re.DOTALL)
if match:
return match.group(1)
return None
def fetch_article(url, output_dir=None, save_images=False):
"""
抓取微信公众号文章
Args:
url: 文章链接
output_dir: 输出目录,默认为当前目录
save_images: 是否下载图片到本地
Returns:
dict: 包含文章信息的字典
"""
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.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',
}
print(f'正在抓取: {url}')
resp = requests.get(url, headers=headers, timeout=30)
resp.raise_for_status()
html_text = resp.text
# 提取信息
title = extract_title(html_text)
author = extract_author(html_text)
publish_time = extract_publish_time(html_text)
cover = extract_cover_image(html_text)
content_html = extract_content(html_text)
if not content_html:
raise ValueError('无法提取文章正文,可能是页面结构变化或文章已被删除')
# 转换为 Markdown
content_md = html_to_markdown(content_html)
# 构建 Markdown 文档
md_lines = []
if title:
md_lines.append(f'# {title}')
md_lines.append('')
if author:
md_lines.append(f'**公众号**: {author}')
if publish_time:
md_lines.append(f'**发布时间**: {publish_time}')
if cover:
md_lines.append(f'**封面**: ')
md_lines.append(f'**原文链接**: {url}')
md_lines.append('')
md_lines.append('---')
md_lines.append('')
md_lines.append(content_md)
markdown = '\n'.join(md_lines)
# 保存文件
if output_dir:
os.makedirs(output_dir, exist_ok=True)
else:
output_dir = os.getcwd()
# 生成文件名
safe_title = re.sub(r'[^\w\u4e00-\u9fff-]', '_', title or 'untitled')[:50]
filename = f"{safe_title}.md"
filepath = os.path.join(output_dir, filename)
with open(filepath, 'w', encoding='utf-8') as f:
f.write(markdown)
print(f'已保存: {filepath}')
return {
'title': title,
'author': author,
'publish_time': publish_time,
'cover': cover,
'url': url,
'content': content_md,
'markdown': markdown,
'filepath': filepath,
}
def main():
parser = argparse.ArgumentParser(description='抓取微信公众号文章并转换为 Markdown')
parser.add_argument('url', help='微信公众号文章链接')
parser.add_argument('-o', '--output', help='输出目录', default=None)
parser.add_argument('--images', action='store_true', help='下载图片到本地')
parser.add_argument('--json', action='store_true', help='以 JSON 格式输出')
args = parser.parse_args()
try:
result = fetch_article(args.url, output_dir=args.output, save_images=args.images)
if args.json:
# 移除 content 和 markdown 避免输出过大
output = {k: v for k, v in result.items() if k not in ('content', 'markdown')}
print(json.dumps(output, ensure_ascii=False, indent=2))
else:
print(f"\n标题: {result['title']}")
print(f"作者: {result['author']}")
print(f"时间: {result['publish_time']}")
print(f"文件: {result['filepath']}")
except requests.RequestException as e:
print(f'网络请求失败: {e}', file=sys.stderr)
sys.exit(1)
except ValueError as e:
print(f'解析失败: {e}', file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f'错误: {e}', file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()
Generates admin/list prototypes from requirements. Supports mountListPage-style frameworks (e.g. kfk-mock-ui), standalone HTML, or project-specific setups. U...
---
name: prototype-generator
description: Generates admin/list prototypes from requirements. Supports mountListPage-style frameworks (e.g. kfk-mock-ui), standalone HTML, or project-specific setups. Use when user wants to create a prototype, generate prototype from requirements, add list pages, or says "根据需求生成原型" / "自动生成一套原型" / "创建原型".
---
# 通用原型自动生成
根据用户输入的需求,自动生成后台列表原型。支持多种输出模式,适配不同项目结构。
## 触发条件
- 用户要求「根据需求生成原型」「自动生成一套原型」「创建原型」
- 用户描述业务场景并希望得到可运行的原型页面
- 用户要求新增列表页或管理页面
## 第一步:检测项目上下文
生成前先扫描项目,确定输出模式:
| 检测到 | 输出模式 | 说明 |
|--------|----------|------|
| `kfk-mock-ui.js` 或 `KFK.mountListPage` | **列表框架模式** | 生成 menu.js、view_*.html,在现有 mock-ui 中加假数据分支 |
| `kfk-admin.css` 或上层 UI 库目录 | **子项目模式** | 同上,资源路径为 `../父目录/` |
| 无上述依赖 | **独立模式** | 生成自包含的 HTML(内联样式+脚本),无外部依赖 |
若用户明确指定(如「用现有框架」「独立页面」「纯 HTML」),优先按指定执行。
## 第二步:解析需求
从用户描述中提取或推断:
| 项 | 说明 | 缺失时默认 |
|----|------|-----------|
| 项目/模块名称 | 品牌或模块名 | 询问或取首句关键词 |
| 目标目录 | 输出位置 | 项目根或同名子目录 |
| 菜单分组 | 按业务域分组 | 如「核心业务」「基础数据」「系统配置」 |
| 页面列表 | 每页:标题、表格列、查询条件、行操作 | 从需求逐条对应 |
**字段推断规则:**
- 名称/标题/编码类 → 模糊查询 `mode: 'like'`
- 是/否、状态、枚举 → 下拉 `type: 'select'`,`options: [{value:'',label:'全部'},...]`
- 年月/日期 → `type: 'month'` 或 placeholder `如 2025-10`
- 枚举选项从需求或示例中抽取
## 第三步:生成文件(按模式)
### 列表框架模式(存在 kfk-mock-ui 或类似 mountListPage 框架)
1. **menu.js**:`{ group, items: [{ id, title, href }] }`,id 用 kebab-case
2. **view_xxx.html**:引用 css/js,调用 `mountListPage(app, config)`(如 KFK.mountListPage)
3. **mock-ui.js**:新增 `generateXxxRow`,在 `generateRows` 中加分支
4. **sql/*.sql**(可选):与业务字段对应的建表语句
**资源路径约定**:若项目中有上层 UI 库目录(如父级原型目录),用 `../父目录/css/`、`../父目录/js/`;否则用项目内 `./css/`、`./js/`。
### 独立模式(无框架依赖)
生成单页 HTML,包含:
- 查询表单(根据 queryFields)
- 数据表格(根据 columns)
- 简单分页
- 行操作按钮(查看/编辑/删除等)
- 内联样式与脚本,可直接用浏览器打开
结构:query-form + data-table + pager,内联 CSS 与脚本。详见 [references/standalone-template.md](references/standalone-template.md)。
### 多页独立模式
当有多个列表页且用户需要菜单导航时,可生成:
- `index.html`:左侧菜单 + iframe 展示
- `view_xxx.html`:各列表页(独立 HTML)
- `menu.js` 或内联菜单数据
## 第四步:校验清单
- [ ] menu item 的 id 与 pageId / 页面标识一致
- [ ] columns 的 key 与假数据对象 key 一致
- [ ] 下拉类 queryFields 使用 `type: 'select'` 和 `options`
- [ ] 独立模式下样式、脚本可正常运行
## 配置模板(列表框架模式)
### queryFields
```javascript
{ key: 'name', label: '名称', placeholder: '请输入', mode: 'like' }
{ key: 'status', label: '状态', type: 'select', mode: 'eq', options: [
{ value: '', label: '全部' }, { value: '是', label: '是' }, { value: '否', label: '否' }
]}
{ key: 'summaryMonth', label: '月份', type: 'month', mode: 'eq' }
```
### columns
```javascript
{ key: 'serialNo', title: '序号' }
{ key: 'code', title: '编码', mono: true }
{ key: 'name', title: '名称', align: 'left', required: true }
{ key: 'remark', title: '备注', align: 'left' }
{ key: 'createTime', title: '创建时间', mono: true }
```
### rowActions + onRowAction
```javascript
rowActions: [
{ act: 'view', label: '查看' },
{ act: 'edit', label: '编辑' },
{ act: 'delete', label: '删除', danger: true }
]
```
## 需求示例
| 用户输入 | 产出 |
|----------|------|
| 「做一个供应商管理,名称、主体、备注」 | 1 页供应商列表,3 列 + remark/createTime |
| 「发票池按销方、购方、是否推送查询」 | queryFields:销方/购方/是否推送(下拉) |
| 「列表要有报废、重置按钮」 | rowActions 增加自定义操作,onRowAction 实现逻辑 |
| 「独立原型,不要依赖」 | 单 HTML 文件,内联样式和脚本 |
## 可选引用
项目中若有以下规则,可一并遵循:
- `.cursor/rules/prototype-menu.mdc`:菜单与列表页结构
- 项目中若有列表页 config、CRUD 约定规则,可一并遵循
## 参考
- 独立模式详细模板:[references/standalone-template.md](references/standalone-template.md)
FILE:clawhub.json
{
"name": "Prototype Generator",
"tagline": "Generate admin list prototypes from requirements. Cursor, OpenClaw, Claude Code.",
"description": "Automatically generates backend list prototypes from natural language requirements. Supports multiple output modes: mountListPage-style frameworks (e.g. kfk-mock-ui), standalone HTML with inline styles/scripts, or project-specific setups. Auto-detects project context to choose the right mode. Works with Cursor, OpenClaw, and Claude Code.",
"category": "development",
"tags": ["prototype", "admin", "list", "html", "cursor", "claude"],
"version": "1.0.0",
"license": "MIT",
"pricing": "free",
"support_url": "https://github.com/limingfa/prototype-skills/issues",
"homepage": "https://github.com/limingfa/prototype-skills"
}