feat: daily reading report generator - zero token cost\n\n- Parses weread-notes/*.md to extract today/week/month highlights\n- Generates structured markdown report with overview/bookmarks/thoughts\n- Cron: 22:00 daily, no API calls needed (zero token consumption)\n- Output: weread-notes/daily/每日阅读_YYYY-MM-DD.md
This commit is contained in:
296
weread-sync/daily_report.py
Executable file
296
weread-sync/daily_report.py
Executable file
@ -0,0 +1,296 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
daily_report.py — 微信读书每日/每周/每月阅读报告
|
||||||
|
|
||||||
|
从 weread-notes 笔记中提取当日/当周/当月的划线+批注,
|
||||||
|
生成结构化阅读报告,推送到 Gitea。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python3 daily_report.py # 今日报告(默认)
|
||||||
|
python3 daily_report.py --period week # 本周报告
|
||||||
|
python3 daily_report.py --period month # 本月报告
|
||||||
|
|
||||||
|
定时(crontab):
|
||||||
|
0 22 * * * cd ~/weread-sync && python3 daily_report.py >> daily_report.log 2>&1
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
REPO_DIR = os.path.expanduser("~/weread-notes")
|
||||||
|
NOTES_DIR = os.path.join(REPO_DIR, "notes")
|
||||||
|
DAILY_DIR = os.path.join(REPO_DIR, "daily")
|
||||||
|
SYNC_SCRIPT = os.path.expanduser("~/weread-sync/sync.py")
|
||||||
|
|
||||||
|
|
||||||
|
# ─── 时间解析 ────────────────────────────────────────
|
||||||
|
|
||||||
|
def parse_time_from_line(line):
|
||||||
|
"""从 '> 🕐 2026-06-15 09:10' 中提取日期对象"""
|
||||||
|
m = re.search(r'🕐\s*(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})', line)
|
||||||
|
if m:
|
||||||
|
return datetime.strptime(f"{m.group(1)} {m.group(2)}", "%Y-%m-%d %H:%M")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_period_range(period="day"):
|
||||||
|
"""获取时间范围(起始日期 00:00, 结束日期 23:59)"""
|
||||||
|
now = datetime.now()
|
||||||
|
today = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
if period == "day":
|
||||||
|
return today, today + timedelta(days=1)
|
||||||
|
elif period == "week":
|
||||||
|
start = today - timedelta(days=today.weekday())
|
||||||
|
return start, start + timedelta(days=7)
|
||||||
|
elif period == "month":
|
||||||
|
start = today.replace(day=1)
|
||||||
|
if start.month == 12:
|
||||||
|
end = start.replace(year=start.year + 1, month=1)
|
||||||
|
else:
|
||||||
|
end = start.replace(month=start.month + 1)
|
||||||
|
return start, end
|
||||||
|
return today, today + timedelta(days=1)
|
||||||
|
|
||||||
|
|
||||||
|
def period_label(period):
|
||||||
|
labels = {"day": "今日", "week": "本周", "month": "本月"}
|
||||||
|
return labels.get(period, "今日")
|
||||||
|
|
||||||
|
|
||||||
|
# ─── 笔记解析 ────────────────────────────────────────
|
||||||
|
|
||||||
|
def parse_notes():
|
||||||
|
"""解析所有笔记文件,返回结构化数据"""
|
||||||
|
entries = []
|
||||||
|
for fname in os.listdir(NOTES_DIR):
|
||||||
|
if not fname.endswith(".md"):
|
||||||
|
continue
|
||||||
|
filepath = os.path.join(NOTES_DIR, fname)
|
||||||
|
book_title = fname[:-3] # 去掉 .md
|
||||||
|
with open(filepath, "r", encoding="utf-8") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
# 提取书籍信息头部
|
||||||
|
author = ""
|
||||||
|
progress = ""
|
||||||
|
for line in lines[:30]:
|
||||||
|
m = re.match(r'\|\s*\*\*作者\*\*\s*\|\s*(.+?)\s*\|', line)
|
||||||
|
if m:
|
||||||
|
author = m.group(1).strip()
|
||||||
|
m = re.match(r'\|\s*\*\*阅读进度\*\*\s*\|\s*(\d+)%\s*\|', line)
|
||||||
|
if m:
|
||||||
|
progress = f"{m.group(1)}%"
|
||||||
|
|
||||||
|
# 解析划线 + 想法
|
||||||
|
i = 0
|
||||||
|
while i < len(lines):
|
||||||
|
line = lines[i]
|
||||||
|
# 找时间戳行
|
||||||
|
ts = parse_time_from_line(line)
|
||||||
|
if ts:
|
||||||
|
# 划线:时间戳上一行是内容(> 开头)
|
||||||
|
if i > 0:
|
||||||
|
prev = lines[i - 1].strip()
|
||||||
|
content_text = re.sub(r'^>\s*', '', prev).strip()
|
||||||
|
# 找紧跟的 💬 想法
|
||||||
|
thought = ""
|
||||||
|
for j in range(i + 1, min(i + 5, len(lines))):
|
||||||
|
t_line = lines[j].strip()
|
||||||
|
if t_line.startswith("💬"):
|
||||||
|
thought = re.sub(r'^💬\s*\*{0,2}(.*?)\*{0,2}$', r'\1', t_line).strip()
|
||||||
|
break
|
||||||
|
entries.append({
|
||||||
|
"book": book_title,
|
||||||
|
"author": author,
|
||||||
|
"progress": progress,
|
||||||
|
"time": ts,
|
||||||
|
"type": "bookmark",
|
||||||
|
"text": content_text,
|
||||||
|
"thought": thought,
|
||||||
|
})
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
# ─── 报告生成 ────────────────────────────────────────
|
||||||
|
|
||||||
|
def generate_report(entries, period="day"):
|
||||||
|
"""生成阅读报告 Markdown"""
|
||||||
|
now = datetime.now()
|
||||||
|
start, end = get_period_range(period)
|
||||||
|
label = period_label(period)
|
||||||
|
date_str = now.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
# 筛选时间范围内的条目
|
||||||
|
filtered = [e for e in entries if start <= e["time"] < end]
|
||||||
|
|
||||||
|
if not filtered:
|
||||||
|
return f"# {label}阅读报告 · {date_str}\n\n> 暂无阅读记录\n"
|
||||||
|
|
||||||
|
# 按书籍分组
|
||||||
|
by_book = defaultdict(list)
|
||||||
|
for e in filtered:
|
||||||
|
by_book[e["book"]].append(e)
|
||||||
|
|
||||||
|
# 按时间排序
|
||||||
|
filtered.sort(key=lambda x: x["time"], reverse=True)
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
lines.append(f"# {label}阅读报告 · {date_str}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# ── 概览 ──
|
||||||
|
total_bookmarks = sum(1 for e in filtered if e["type"] == "bookmark" and not e["thought"])
|
||||||
|
total_thoughts = sum(1 for e in filtered if e["thought"])
|
||||||
|
finished_books = set(e["book"] for e in filtered)
|
||||||
|
|
||||||
|
lines.append("## 📊 阅读概览")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(f"| 项目 | 数量 |")
|
||||||
|
lines.append(f"|:---|:---:|")
|
||||||
|
lines.append(f"| 阅读书籍 | {len(finished_books)} 本 |")
|
||||||
|
lines.append(f"| 新增划线 | {total_bookmarks} 条 |")
|
||||||
|
lines.append(f"| 新增批注 | {total_thoughts} 条 |")
|
||||||
|
|
||||||
|
# 各书进度
|
||||||
|
lines.append("")
|
||||||
|
progress_lines = []
|
||||||
|
for book_name in sorted(by_book.keys()):
|
||||||
|
entries_list = by_book[book_name]
|
||||||
|
p = entries_list[0]["progress"] if entries_list[0]["progress"] else "—"
|
||||||
|
bm_count = sum(1 for e in entries_list if e["type"] == "bookmark" and not e["thought"])
|
||||||
|
th_count = sum(1 for e in entries_list if e["thought"])
|
||||||
|
progress_lines.append(f"- **{book_name}**({p})— {bm_count}条划线, {th_count}条批注")
|
||||||
|
if progress_lines:
|
||||||
|
lines.append("\n".join(progress_lines))
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# ── 划线精选 ──
|
||||||
|
bookmarks_only = [e for e in filtered if e["type"] == "bookmark"]
|
||||||
|
if bookmarks_only:
|
||||||
|
lines.append("---")
|
||||||
|
lines.append("## 📖 划线精选")
|
||||||
|
lines.append("")
|
||||||
|
for book_name in sorted(by_book.keys()):
|
||||||
|
bm_list = [e for e in by_book[book_name] if e["type"] == "bookmark"]
|
||||||
|
if not bm_list:
|
||||||
|
continue
|
||||||
|
lines.append(f"### 《{book_name}》")
|
||||||
|
lines.append("")
|
||||||
|
for e in bm_list:
|
||||||
|
lines.append(f"> {e['text']}")
|
||||||
|
lines.append(f"> 🕐 {e['time'].strftime('%Y-%m-%d %H:%M')}")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# ── 批注精选 ──
|
||||||
|
thoughts = [e for e in filtered if e["thought"]]
|
||||||
|
if thoughts:
|
||||||
|
lines.append("---")
|
||||||
|
lines.append("## 💬 批注精选")
|
||||||
|
lines.append("")
|
||||||
|
for book_name in sorted(by_book.keys()):
|
||||||
|
th_list = [e for e in by_book[book_name] if e["thought"]]
|
||||||
|
if not th_list:
|
||||||
|
continue
|
||||||
|
lines.append(f"### 《{book_name}》")
|
||||||
|
lines.append("")
|
||||||
|
for e in th_list:
|
||||||
|
if e["text"]:
|
||||||
|
lines.append(f"> {e['text']}")
|
||||||
|
lines.append(f"> 🕐 {e['time'].strftime('%Y-%m-%d %H:%M')}")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(f"💬 **{e['thought']}**")
|
||||||
|
lines.append("")
|
||||||
|
if e["text"]:
|
||||||
|
lines.append("---")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── 输出 ────────────────────────────────────────────
|
||||||
|
|
||||||
|
def save_report(md_content, period="day"):
|
||||||
|
"""保存报告文件并推送到 Gitea"""
|
||||||
|
os.makedirs(DAILY_DIR, exist_ok=True)
|
||||||
|
now = datetime.now()
|
||||||
|
date_str = now.strftime("%Y-%m-%d")
|
||||||
|
period_map = {"day": "每日", "week": "每周", "month": "每月"}
|
||||||
|
period_cn = period_map.get(period, "每日")
|
||||||
|
|
||||||
|
filename = f"{period_cn}阅读_{date_str}.md"
|
||||||
|
filepath = os.path.join(DAILY_DIR, filename)
|
||||||
|
with open(filepath, "w", encoding="utf-8") as f:
|
||||||
|
f.write(md_content)
|
||||||
|
print(f"[OUTPUT] {filepath}")
|
||||||
|
|
||||||
|
# Git 推送
|
||||||
|
try:
|
||||||
|
subprocess.run(["git", "add", "daily/"], cwd=REPO_DIR,
|
||||||
|
capture_output=True, timeout=30)
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "commit", "-m", f"report: {period_cn}阅读 {date_str}", "--allow-empty"],
|
||||||
|
cwd=REPO_DIR, capture_output=True, timeout=30
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
subprocess.run(["git", "push"], cwd=REPO_DIR,
|
||||||
|
capture_output=True, timeout=60)
|
||||||
|
print(f"[GIT] Pushed: {period_cn}阅读报告 {date_str}")
|
||||||
|
else:
|
||||||
|
print(f"[GIT] No changes (already up to date)")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"[GIT] Error: {e.stderr.decode()[:200]}")
|
||||||
|
|
||||||
|
|
||||||
|
# ─── 主流程 ───────────────────────────────────────────
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# 解析参数
|
||||||
|
period = "day"
|
||||||
|
if "--period" in sys.argv:
|
||||||
|
idx = sys.argv.index("--period")
|
||||||
|
if idx + 1 < len(sys.argv):
|
||||||
|
period = sys.argv[idx + 1]
|
||||||
|
|
||||||
|
label = period_label(period)
|
||||||
|
now = datetime.now()
|
||||||
|
print(f"\n{'='*50}")
|
||||||
|
print(f"{label}阅读报告 | {now.strftime('%Y-%m-%d %H:%M')}")
|
||||||
|
print(f"{'='*50}")
|
||||||
|
|
||||||
|
# 先同步最新笔记
|
||||||
|
print("\n[1/4] 同步微信读书笔记...")
|
||||||
|
if os.path.exists(SYNC_SCRIPT):
|
||||||
|
result = subprocess.run(["python3", SYNC_SCRIPT],
|
||||||
|
capture_output=True, timeout=300)
|
||||||
|
print(result.stdout.decode()[-200:] if result.stdout else "")
|
||||||
|
else:
|
||||||
|
print(f" [SKIP] Sync script not found: {SYNC_SCRIPT}")
|
||||||
|
|
||||||
|
# 解析笔记
|
||||||
|
print(f"\n[2/4] 解析笔记文件...")
|
||||||
|
entries = parse_notes()
|
||||||
|
print(f" 共解析 {len(entries)} 条记录")
|
||||||
|
|
||||||
|
# 生成报告
|
||||||
|
print(f"\n[3/4] 生成报告...")
|
||||||
|
md = generate_report(entries, period)
|
||||||
|
|
||||||
|
# 保存 + 推送
|
||||||
|
print(f"\n[4/4] 推送到 Gitea...")
|
||||||
|
save_report(md, period)
|
||||||
|
|
||||||
|
print(f"\n{'='*50}")
|
||||||
|
print(f"{label}阅读报告完成 ✅")
|
||||||
|
print(f"{'='*50}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user