作者:Charlotte
自动抓取每日资讯、摘要重写、分析扩写,并生成适配公众号的 Markdown 和 HTML,支持一键上传微信草稿箱或预览。
✨ 项目特点
-
🔍 自动抓取微信公众号、企业官网等资讯页面
-
🧠 使用 OpenAI 模型生成摘要与扩写分析
-
📄 自动生成 Markdown 和美化 HTML(适配公众号)
-
🚀 一键推送至微信公众号草稿箱(支持草稿 / 预览 / 群发)
-
🕓 支持每日定时运行(基于
schedule
)
📦 项目结构
.
├── main.py # 主入口脚本,调度全流程
├── fetcher.py # 支持微信公众号 / 通用网页的文章抓取
├── rewriter.py # OpenAI 摘要生成与扩写
├── renderer.py # 将文章列表渲染为 Markdown
├── md2html.py # Markdown 转公众号样式 HTML
├── publisher.py # 微信公众号草稿 / 群发管理
├── utils.py # 配置读取、日志设置、已处理记录管理
├── test.py # 获取粉丝 OpenID 工具脚本
├── config/
│ ├── urls.txt # 手动输入模式的文章链接列表
│ └── settings.yaml # API 密钥与发布参数配置
├── data/
│ ├── seen.json # 已处理过的文章链接记录
│ ├── output/ # 输出 Markdown 和 HTML
│ └── logs/ # 日志记录
⚙️ 安装依赖
pip install -r requirements.txt
示例依赖包括:
openai
,wechatpy
,loguru
,requests
,bs4
,markdown
,pyyaml
🚀 使用方法
▶ 手动模式(读取 config/urls.txt
)
python main.py --manual
▶ 自动模式(从网站自动抓取)
python main.py --once
▶ 每日定时调度(默认每天早上 8:00)
python main.py
🔑 配置文件说明(config/settings.yaml
)
openai_api_key: "你的OpenAI API Key"
wechat_app_id: "你的微信公众号 app_id"
wechat_app_secret: "你的微信公众号 app_secret"
preview_openid: "测试预览用 openid"
extra_info: "我认为该技术的关键在于..."
thumb_path: "config/thumb.jpg" # 封面图路径
📤 发布模式支持(可选项)
-
none
:不调用微信 API -
draft_only
:只创建草稿(默认) -
preview
:草稿 + 推送给指定用户预览 -
sendall
:草稿 + 群发(⚠危险操作)
配置方式见 main.py
中 publish_mode
参数说明。
🧪 粉丝 OpenID 查询(选填)
python test.py
用于获取你的微信粉丝 OpenID,用于预览发送。
📌 示例截图
main.py
from pathlib import Path
from typing import List
import argparse
import os
import time
import schedule
from utils import load_settings, load_seen, save_seen, logger, OUTPUT_DIR, ROOT_DIR
from fetcher import get_article, fetch_latest_news
from openai import OpenAI
from rewriter import rewrite_article, expand_summary
from renderer import render_markdown
from md2html import md_to_html
from publisher import publish
CONFIG_URLS_PATH = ROOT_DIR / 'config' / 'urls.txt'
def load_urls_from_file() -> List[str]:
if not CONFIG_URLS_PATH.exists():
logger.error(f"未找到 URL 列表文件: {CONFIG_URLS_PATH}")
return []
with open(CONFIG_URLS_PATH, 'r', encoding='utf-8') as f:
return [ln.strip() for ln in f if ln.strip() and not ln.startswith('#')]
def process_articles(urls: List[str], client: OpenAI):
cfg = load_settings()
seen = load_seen()
records = []
for url in urls:
if url in seen:
logger.info(f"跳过已处理 URL: {url}")
continue
logger.info(f"抓取文章: {url}")
try:
art = get_article(url)
except Exception:
logger.exception(f"抓取失败: {url}")
continue
logger.info(f"摘要文章: {url}")
try:
summary = rewrite_article(art, client)
except Exception:
logger.exception(f"摘要失败: {url}")
continue
art['summary'] = summary
logger.info(f"扩写文章: {url}")
extra_info = cfg.get('extra_info', '我认为该技术的关键在于...')
try:
commentary = expand_summary(summary, client, extra=extra_info)
except Exception:
commentary = summary
art['commentary'] = commentary
records.append(art)
seen.add(url)
save_seen(seen)
if not records:
logger.info('没有成功处理的文章,结束。')
return
md = render_markdown(records)
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
out_md = OUTPUT_DIR / 'digest_latest.md'
out_md.write_text(md, encoding='utf-8')
logger.info(f"Markdown 已写入 {out_md}")
html = md_to_html(md)
out_html = OUTPUT_DIR / 'digest_latest.html'
out_html.write_text(html, encoding='utf-8')
logger.info(f"HTML 已写入 {out_html}")
if cfg.get('wechat_app_id') and cfg.get('wechat_app_secret'):
status, media_id = publish(
markdown=md,
html=html,
app_id=cfg['wechat_app_id'],
app_secret=cfg['wechat_app_secret'],
publish_mode="draft_only",
thumb_path=cfg.get('thumb_path', 'config/thumb.jpg'),
)
logger.info(f"发布状态: {status}, media_id={media_id}")
else:
logger.warning('未配置公众号凭证:跳过发布。')
def run(mode='auto'):
cfg = load_settings()
api_key = cfg.get('openai_api_key')
if not api_key:
raise RuntimeError('OpenAI API Key 未配置 (openai_api_key)')
os.environ['OPENAI_API_KEY'] = api_key
client = OpenAI()
if mode == 'manual':
urls = load_urls_from_file()
if not urls:
logger.warning('urls.txt 为空,退出。')
return
else:
logger.info("自动模式下尝试获取新闻源链接...")
urls = fetch_latest_news()
if not urls:
logger.warning("未获取到任何新闻链接,跳过处理。")
return
process_articles(urls, client)
def schedule_job():
logger.info("定时任务触发中...")
run(mode='auto')
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--force", action="store_true", help="忽略已处理记录,强制重跑所有 URL")
parser.add_argument("--manual", action="store_true", help="使用 urls.txt 作为输入(旧逻辑)")
parser.add_argument("--once", action="store_true", help="只执行一次,不启用定时器")
args = parser.parse_args()
if args.manual or args.once:
run(mode='manual' if args.manual else 'auto')
else:
# 每天早上 8:00 定时执行
schedule.every().day.at("8:00").do(schedule_job)
logger.info("已启动定时调度,每天 8:00 自动爬取发布")
while True:
schedule.run_pending()
time.sleep(60)
if __name__ == '__main__':
main()