#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 批量上传《卡若的IP财富旅程》PART1-5 到小程序。 part_id : part-new-17755513182 part_title: 卡若的IP财富旅程 各 PART 对应独立 chapter: PART1 -> chapter-ip-1 | PART1 | 我少年时期的互联网启蒙 PART2 -> chapter-ip-2 | PART2 | 从0-1的过程 PART3 -> chapter-ip-3 | PART3 | 我的创业旅程 PART4 -> chapter-ip-4 | PART4 | 私域流量到AI时代的商业进化 PART5 -> chapter-ip-5 | PART5 | 未来的一些思考 ID 格式: ip. 例: ip1.1, ip2.10, ip3.5 """ from __future__ import annotations import importlib.util import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent BOOK_DIR = Path("/Users/karuo/Documents/个人/2、我写的书/《卡若的IP财富旅程》") PART_ID = "part-new-17755513182" PART_TITLE = "卡若的IP财富旅程" CHAPTERS = { 1: ("chapter-ip-1", "PART1 | 我少年时期的互联网启蒙"), 2: ("chapter-ip-2", "PART2 | 从0-1的过程"), 3: ("chapter-ip-3", "PART3 | 我的创业旅程"), 4: ("chapter-ip-4", "PART4 | 私域流量到AI时代的商业进化"), 5: ("chapter-ip-5", "PART5 | 未来的一些思考"), } # 匹配 ✅1.5-标题 或 ✅1.5- 标题 形式的文件名,提取 section_num 和 title FILENAME_RE = re.compile( r"^[✅⬜]?\s*(\d+)\.(\d+)[- ]+(.+?)(?:\.md)?$", re.UNICODE, ) def load_db(): mig = ROOT / "scripts" / "migrate_2026_sections.py" spec = importlib.util.spec_from_file_location("_mig_db", mig) mod = importlib.util.module_from_spec(spec) assert spec.loader is not None spec.loader.exec_module(mod) return mod.DB_CONFIG def strip_md_title_line(text: str) -> str: lines = text.splitlines() if lines and lines[0].lstrip().startswith("#"): return "\n".join(lines[1:]).lstrip("\n") return text def for_miniprogram_body(text: str) -> str: out_lines: list[str] = [] for line in text.splitlines(): if line.strip() == "---": out_lines.append("") else: out_lines.append(line) body = "\n".join(out_lines) body = body.replace("**", "") body = re.sub(r"\n{3,}", "\n\n", body) return body.strip() + "\n" def collect_files(part_num: int) -> list[tuple[str, str, Path]]: """返回 [(ip_id, title, path)] 已去重且按节号排序""" part_dir = BOOK_DIR / f"PART{part_num}" if not part_dir.exists(): print(f" ⚠ 目录不存在: {part_dir}", file=sys.stderr) return [] seen_sections: dict[str, tuple[str, str, Path]] = {} for f in part_dir.iterdir(): name = f.name # 去掉 .md 后缀再匹配(处理双后缀 .md.md) clean_name = re.sub(r"\.md(\.md)*$", "", name, flags=re.IGNORECASE) m = FILENAME_RE.match(clean_name) if not m: print(f" ⚠ 跳过无法解析的文件: {name}") continue p_num, s_num, title_raw = m.group(1), m.group(2), m.group(3).strip() if int(p_num) != part_num: print(f" ⚠ PART号不匹配,跳过: {name}") continue # 把文件名里的冒号/括号等清理出来作为标题 title = title_raw.strip(":: \t") # 还原完整标题(包含冒号) title = title_raw ip_id = f"ip{part_num}.{s_num}" section_key = f"{part_num}.{s_num}" # 优先选有 .md 后缀的文件(去重) if section_key in seen_sections: existing_path = seen_sections[section_key][2] # 若新文件有 .md 后缀,替换 if str(f).lower().endswith(".md") and not str(existing_path).lower().endswith(".md"): seen_sections[section_key] = (ip_id, title, f) else: seen_sections[section_key] = (ip_id, title, f) # 按节号排序 result = list(seen_sections.values()) result.sort(key=lambda x: float(re.sub(r"ip\d+\.", "", x[0]))) return result def upsert_chapter(cur, chapter_id: str, chapter_title: str, ip_id: str, title: str, body: str, price: float): cur.execute("SELECT id FROM chapters WHERE id = %s", (ip_id,)) if cur.fetchone(): cur.execute( "UPDATE chapters SET section_title=%s, content=%s, part_id=%s, part_title=%s, " "chapter_id=%s, chapter_title=%s, price=%s WHERE id=%s", (title, body, PART_ID, PART_TITLE, chapter_id, chapter_title, price, ip_id), ) return "updated" else: cur.execute( "INSERT INTO chapters (id, section_title, content, part_id, part_title, chapter_id, chapter_title, price) " "VALUES (%s, %s, %s, %s, %s, %s, %s, %s)", (ip_id, title, body, PART_ID, PART_TITLE, chapter_id, chapter_title, price), ) return "created" def main(): try: import pymysql except ImportError: print("需要: pip install pymysql", file=sys.stderr) sys.exit(1) dry_run = "--dry-run" in sys.argv price = 1.0 cfg = load_db() conn = pymysql.connect(**cfg) cur = conn.cursor() total_ok = 0 total_skip = 0 total_err = 0 for part_num, (chapter_id, chapter_title) in CHAPTERS.items(): print(f"\n{'='*60}") print(f"PART{part_num} → {chapter_id} | {chapter_title}") print(f"{'='*60}") files = collect_files(part_num) if not files: print(" 无文件,跳过") continue for ip_id, title, path in files: try: raw = path.read_text(encoding="utf-8") except Exception as e: print(f" ✗ 读取失败 {path.name}: {e}") total_err += 1 continue body = for_miniprogram_body(strip_md_title_line(raw)) if dry_run: print(f" [dry] {ip_id} {title[:30]} ({path.name})") total_ok += 1 continue try: action = upsert_chapter(cur, chapter_id, chapter_title, ip_id, title, body, price) conn.commit() print(f" ✓ [{action}] {ip_id} {title[:40]}") total_ok += 1 except Exception as e: conn.rollback() print(f" ✗ 上传失败 {ip_id} ({path.name}): {e}") total_err += 1 conn.close() print(f"\n{'='*60}") print(f"完成:成功 {total_ok} | 跳过 {total_skip} | 失败 {total_err}") if dry_run: print("(dry-run 模式,未实际写库)") if __name__ == "__main__": main()