Files
zhuangang/tools/gen_week_topics.py

104 lines
4.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""根据 catalog.json 中每个条目的 week 字段,在 plan/weekNN-*.md 中生成“本周相关专题”一节。
- 生成内容放在 <!-- week-topics:start --> 和 <!-- week-topics:end --> 之间,重复运行会替换,不会重复插入
- 第一次插入的位置:“## 本周自查清单 / ## 本周验收”之前(代码块之外);没有这两个标题时放在文末
- 先运行 tools/build_catalog.py 生成 catalog.json
用法:python3 tools/gen_week_topics.py [--check] (--check 只检查是否需要更新,需要时返回 1)
"""
from __future__ import annotations
import json
import re
import sys
from collections import defaultdict
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
START, END = "<!-- week-topics:start -->", "<!-- week-topics:end -->"
ANCHOR = re.compile(r"^## (本周自查清单|本周验收)")
FENCE = re.compile(r"^\s*(```|~~~)")
TYPE_ORDER = {"lesson": 0, "scenario": 1, "interview": 2}
TYPE_NAME = {"lesson": "课程", "scenario": "场景", "interview": "面试题"}
AI_CODING_NOTE = ("- **AI 编程练习(每周)**:本周至少一个功能用 AI 编程工具(Cursor / Claude Code / Codex 等)完成,"
"按 [AI 编程:工作流](../topics/ai/ai-coding/01-workflow-rules-skills.md) 写清需求和规则,"
"按 [验证与证据](../topics/ai/ai-coding/02-verify-evidence.md) 保存测试结果和会话记录(脱敏后),"
"第 15 周整理成“AI 编程 + 验证方法”的应聘案例。")
def build_section(week: int, items: list[dict], stacks: dict[str, dict]) -> str:
by_stack: dict[str, list[dict]] = defaultdict(list)
cross: list[dict] = []
for it in items:
if it["type"] == "scenario" and it["domain"] == "cross":
cross.append(it)
elif it["type"] in TYPE_ORDER and it.get("stack"):
by_stack[it["path"].split("/")[2]].append(it)
lines = [START, "", "## 本周相关专题", "",
"> 本节由 `tools/gen_week_topics.py` 根据专题文件的 `week` 字段自动生成,请勿手改;"
"想调整就修改对应专题文件的 frontmatter 后重新生成。", ""]
if week <= 14:
lines += [AI_CODING_NOTE, ""]
if by_stack:
lines += ["| 技术栈 | 课程 / 场景 / 面试题 |", "| --- | --- |"]
for sid in sorted(by_stack, key=lambda s: (stacks.get(s, {}).get("domain", ""), s)):
st = stacks.get(sid, {})
name = f"[{st.get('title', sid)}](../{st['index']})" if st.get("index") else sid
links = []
for it in sorted(by_stack[sid], key=lambda x: (TYPE_ORDER[x["type"]], x.get("order") or 0, x["path"])):
title = TYPE_NAME[it["type"]] if it["type"] == "interview" else it["title"].split(":")[0]
links.append(f"{TYPE_NAME[it['type']]}:[{title}](../{it['path']})" if it["type"] != "interview"
else f"[面试题](../{it['path']})")
lines.append(f"| {name} | {';'.join(links)} |")
lines.append("")
if cross:
lines += ["**跨技术栈场景**:" + "、".join(f"[{c['title'].split(':')[0]}](../{c['path']})" for c in sorted(cross, key=lambda c: c["path"])), ""]
lines.append(END)
return "\n".join(lines)
def insert(text: str, section: str) -> str:
if START in text and END in text:
return re.sub(re.escape(START) + r".*?" + re.escape(END), lambda _: section, text, count=1, flags=re.S)
in_fence = False
lines = text.split("\n")
for i, line in enumerate(lines):
if FENCE.match(line):
in_fence = not in_fence
elif not in_fence and ANCHOR.match(line):
return "\n".join(lines[:i] + [section, ""] + lines[i:])
return text.rstrip("\n") + "\n\n" + section + "\n"
def main() -> int:
check = "--check" in sys.argv
cat = json.loads((ROOT / "catalog.json").read_text(encoding="utf-8"))
stacks = {s["stack"]: s for s in cat["stacks"]}
per_week: dict[int, list[dict]] = defaultdict(list)
for it in cat["items"]:
if it["path"].startswith(("topics/", "scenarios/")):
for w in it.get("week") or []:
per_week[int(w)].append(it)
stale = []
for f in sorted((ROOT / "plan").glob("week*.md")):
week = int(re.match(r"week(\d+)", f.name).group(1))
old = f.read_text(encoding="utf-8")
new = insert(old, build_section(week, per_week.get(week, []), stacks))
if new != old:
stale.append(f.name)
if not check:
f.write_text(new, encoding="utf-8")
if check:
if stale:
print("需要重新生成:" + "、".join(stale))
return 1
print("各周“本周相关专题”已是最新")
return 0
print(f"已更新 {len(stale)} 个周计划文件")
return 0
if __name__ == "__main__":
sys.exit(main())