- SKILL.md: 完整设计规范(色彩体系、微信公众号兼容规则、图片排版方案) - scripts/extract_wechat.py: 微信公众号文章抓取脚本 - scripts/build_hundun_html.py: 混沌风格 HTML 生成脚本(组件化) - examples/hundun-lead-article.html: 领教营二期排版示例 - resources/color-palette.md: 混沌品牌色彩参考
157 lines
4.8 KiB
Python
157 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
微信公众号文章抓取脚本
|
||
用途:抓取微信公众号文章原始 HTML,提取文字内容与图片 URL 列表。
|
||
|
||
使用方法:
|
||
python3 extract_wechat.py <微信文章URL> [输出目录]
|
||
|
||
示例:
|
||
python3 extract_wechat.py "https://mp.weixin.qq.com/s/xxxx" ./output
|
||
|
||
输出文件:
|
||
- raw_article.html 原始 HTML 源码
|
||
- extracted_content.txt 提取的图片列表与纯文本摘要
|
||
"""
|
||
|
||
import sys
|
||
import os
|
||
import re
|
||
import urllib.request
|
||
import html as html_lib
|
||
from pathlib import Path
|
||
|
||
|
||
def fetch_wechat_html(url: str) -> str:
|
||
"""
|
||
使用桌面微信 User-Agent 抓取微信公众号文章原始 HTML。
|
||
|
||
:param url: 微信公众号文章 URL
|
||
:return: 原始 HTML 字符串
|
||
:raises: urllib.error.URLError 如果请求失败
|
||
"""
|
||
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 '
|
||
'MicroMessenger/8.0.0'
|
||
),
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||
}
|
||
req = urllib.request.Request(url, headers=headers)
|
||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||
return resp.read().decode('utf-8', errors='replace')
|
||
|
||
|
||
def extractTitle(htmlContent: str) -> str:
|
||
"""从 HTML 中提取文章标题。"""
|
||
match = re.search(r'<title[^>]*>(.*?)</title>', htmlContent, re.DOTALL | re.IGNORECASE)
|
||
if match:
|
||
title = re.sub(r'<[^>]+>', '', match.group(1)).strip()
|
||
return html_lib.unescape(title)
|
||
return "未知标题"
|
||
|
||
|
||
def extractImages(htmlContent: str) -> list:
|
||
"""
|
||
从微信文章 HTML 中提取所有图片 URL。
|
||
优先提取 data-src 属性(微信懒加载),其次 src 属性。
|
||
|
||
:param htmlContent: 原始 HTML 字符串
|
||
:return: 去重后的图片 URL 列表
|
||
"""
|
||
imgUrls = []
|
||
seen = set()
|
||
|
||
# 优先匹配 data-src(微信懒加载图片)
|
||
for match in re.finditer(r'data-src=["\']([^"\']+mmbiz[^"\']+)["\']', htmlContent):
|
||
url = html_lib.unescape(match.group(1))
|
||
if url not in seen:
|
||
imgUrls.append(url)
|
||
seen.add(url)
|
||
|
||
# 补充匹配 src(部分图片无 data-src)
|
||
for match in re.finditer(r'<img[^>]+src=["\']([^"\']+mmbiz[^"\']+)["\']', htmlContent):
|
||
url = html_lib.unescape(match.group(1))
|
||
if url not in seen:
|
||
imgUrls.append(url)
|
||
seen.add(url)
|
||
|
||
return imgUrls
|
||
|
||
|
||
def extractText(htmlContent: str) -> str:
|
||
"""
|
||
提取文章正文区域的纯文本(js_content 内部)。
|
||
|
||
:param htmlContent: 原始 HTML 字符串
|
||
:return: 清理后的纯文本
|
||
"""
|
||
contentMatch = re.search(
|
||
r'id=["\']js_content["\'][^>]*>(.*?)(?=<div\s+class=["\']ct_mpda_wrp|$)',
|
||
htmlContent,
|
||
re.DOTALL | re.IGNORECASE
|
||
)
|
||
if not contentMatch:
|
||
return ""
|
||
|
||
text = contentMatch.group(1)
|
||
# 移除所有 HTML 标签
|
||
text = re.sub(r'<[^>]+>', ' ', text)
|
||
# 解码 HTML 实体
|
||
text = html_lib.unescape(text)
|
||
# 清理多余空白
|
||
text = re.sub(r'\s+', ' ', text).strip()
|
||
return text
|
||
|
||
|
||
def main():
|
||
"""主入口函数。"""
|
||
if len(sys.argv) < 2:
|
||
print("用法: python3 extract_wechat.py <微信文章URL> [输出目录]")
|
||
print("示例: python3 extract_wechat.py 'https://mp.weixin.qq.com/s/xxxx' ./output")
|
||
sys.exit(1)
|
||
|
||
url = sys.argv[1]
|
||
outputDir = Path(sys.argv[2]) if len(sys.argv) > 2 else Path("./output")
|
||
outputDir.mkdir(parents=True, exist_ok=True)
|
||
|
||
print(f"📡 正在抓取: {url}")
|
||
htmlContent = fetch_wechat_html(url)
|
||
|
||
# 保存原始 HTML
|
||
rawPath = outputDir / "raw_article.html"
|
||
rawPath.write_text(htmlContent, encoding='utf-8')
|
||
print(f"💾 原始 HTML 已保存: {rawPath} ({len(htmlContent):,} 字节)")
|
||
|
||
# 提取标题
|
||
title = extractTitle(htmlContent)
|
||
print(f"📰 标题: {title}")
|
||
|
||
# 提取图片
|
||
imgUrls = extractImages(htmlContent)
|
||
print(f"🖼️ 发现 {len(imgUrls)} 张图片")
|
||
|
||
# 提取文本
|
||
textContent = extractText(htmlContent)
|
||
|
||
# 写入提取结果
|
||
extractPath = outputDir / "extracted_content.txt"
|
||
with open(extractPath, 'w', encoding='utf-8') as f:
|
||
f.write(f"Title: {title}\n\n")
|
||
f.write(f"Images ({len(imgUrls)}):\n")
|
||
for i, imgUrl in enumerate(imgUrls, 1):
|
||
f.write(f"[{i}] {imgUrl}\n")
|
||
f.write(f"\n--- 正文摘要 (前 2000 字) ---\n\n")
|
||
f.write(textContent[:2000])
|
||
|
||
print(f"📄 提取结果已保存: {extractPath}")
|
||
print(f"\n✅ 完成!共提取 {len(imgUrls)} 张图片,正文约 {len(textContent)} 字。")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|