149 lines
5.9 KiB
Python
149 lines
5.9 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
从小程序/正式 API 下载单章正文,保存为书稿目录下的 md 文件。
|
||
|
||
用法:
|
||
SOUL_TEST_ENV=soulapi python3 scripts/content_download.py 128
|
||
SOUL_TEST_ENV=soulapi python3 scripts/content_download.py --id 10.27
|
||
python3 scripts/content_download.py 128 --out-dir /path/to/2026每日派对干货
|
||
|
||
2026 每日派对章节挂在 part-2026-02 / part-2026-03 / part-2026-04 等篇章下,
|
||
业务 id(如 10.34)与「第 N 场」不是简单线性公式;若按场次推算 404,脚本会
|
||
自动在上述 part 里按 sectionTitle「第N场」解析真实 id。
|
||
"""
|
||
import argparse
|
||
import os
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
# 项目根
|
||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||
sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "test"))
|
||
try:
|
||
from config import API_BASE, ENV_LABEL
|
||
except Exception:
|
||
API_BASE = os.environ.get("SOUL_API_BASE", "https://soulapi.quwanzhi.com").rstrip("/")
|
||
ENV_LABEL = "env"
|
||
|
||
# 书稿 2026 目录默认路径(与上传 README 一致)
|
||
DEFAULT_BOOK_2026 = Path(
|
||
os.environ.get(
|
||
"SOUL_BOOK_2026",
|
||
"/Users/karuo/Documents/个人/2、我写的书/《一场soul的创业实验》/2026每日派对干货",
|
||
)
|
||
)
|
||
|
||
|
||
def field_to_id(field: int) -> str:
|
||
"""首猜 id:≥102 用 10.(field-102+1);与线上一致性不保证,404 时会走 resolve。"""
|
||
if field >= 102:
|
||
n = field - 102 + 1
|
||
return f"10.{n:02d}"
|
||
if field >= 1:
|
||
return f"9.{field:02d}"
|
||
raise ValueError("场次请用 1~999")
|
||
|
||
|
||
def resolve_2026_chapter_id_by_field(base: str, field: int, requests_mod) -> str | None:
|
||
"""在 part-2026-* 下按标题「第N场」查真实 chapters.id。"""
|
||
parts_url = f"{base}/api/miniprogram/book/parts"
|
||
pr = requests_mod.get(parts_url, timeout=30)
|
||
if pr.status_code != 200:
|
||
return None
|
||
pdata = pr.json()
|
||
parts = pdata.get("parts") or []
|
||
needle = f"第{field}场"
|
||
for p in parts:
|
||
pid = p.get("id") or ""
|
||
if not pid.startswith("part-2026-"):
|
||
continue
|
||
url = f"{base}/api/miniprogram/book/chapters-by-part?partId={pid}"
|
||
cr = requests_mod.get(url, timeout=30)
|
||
if cr.status_code != 200:
|
||
continue
|
||
cdata = cr.json().get("data") or []
|
||
for row in cdata:
|
||
title = row.get("sectionTitle") or ""
|
||
if needle in title:
|
||
return row.get("id")
|
||
return None
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description="从小程序 API 下载单章为 md")
|
||
parser.add_argument("field", nargs="?", type=int, help="场次号,如 128 表示第128场")
|
||
parser.add_argument("--id", type=str, help="章节 id,如 10.27(与 field 二选一)")
|
||
parser.add_argument("--out-dir", type=Path, default=DEFAULT_BOOK_2026, help="输出目录,默认 2026每日派对干货")
|
||
parser.add_argument("--base", type=str, default=API_BASE, help="API 根地址")
|
||
args = parser.parse_args()
|
||
|
||
if args.id:
|
||
chapter_id = args.id
|
||
elif args.field is not None:
|
||
chapter_id = field_to_id(args.field)
|
||
else:
|
||
parser.error("请指定 field(如 128)或 --id(如 10.27)")
|
||
|
||
base = args.base.rstrip("/")
|
||
try:
|
||
import requests
|
||
except ImportError:
|
||
print("请安装: pip install requests", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
url = f"{base}/api/miniprogram/book/chapter/by-id/{chapter_id}"
|
||
print(f"环境: {ENV_LABEL} | GET {url}")
|
||
r = requests.get(url, timeout=30)
|
||
# 2026 场次可能仍挂在第9章(9.28=第128场),404 时用 9.xx 再试
|
||
if r.status_code == 404 and args.field is not None and not args.id and args.field >= 1 and args.field <= 101:
|
||
fallback_id = f"9.{args.field:02d}"
|
||
url = f"{base}/api/miniprogram/book/chapter/by-id/{fallback_id}"
|
||
print(f"404,尝试第9章 id: {fallback_id} | GET {url}")
|
||
r = requests.get(url, timeout=30)
|
||
if r.status_code == 200:
|
||
chapter_id = fallback_id
|
||
if r.status_code == 404 and args.field is not None and not args.id and args.field >= 102:
|
||
fallback_id = f"9.{(args.field - 100):02d}" # 128 → 9.28
|
||
url = f"{base}/api/miniprogram/book/chapter/by-id/{fallback_id}"
|
||
print(f"404,尝试第9章 id: {fallback_id} | GET {url}")
|
||
r = requests.get(url, timeout=30)
|
||
if r.status_code == 200:
|
||
chapter_id = fallback_id
|
||
if r.status_code == 404 and args.field is not None and not args.id:
|
||
resolved = resolve_2026_chapter_id_by_field(base, args.field, requests)
|
||
if resolved:
|
||
chapter_id = resolved
|
||
url = f"{base}/api/miniprogram/book/chapter/by-id/{chapter_id}"
|
||
print(f"404,按 2026 篇章标题解析到 id: {chapter_id} | GET {url}")
|
||
r = requests.get(url, timeout=30)
|
||
r.raise_for_status()
|
||
data = r.json()
|
||
if not data.get("success"):
|
||
print("API 返回失败:", data.get("error", data), file=sys.stderr)
|
||
sys.exit(2)
|
||
|
||
content = data.get("content") or (data.get("data") or {}).get("content") or ""
|
||
section_title = data.get("sectionTitle") or (data.get("data") or {}).get("sectionTitle") or f"第{args.field or '?'}场"
|
||
if not content:
|
||
print("未获取到正文 content", file=sys.stderr)
|
||
sys.exit(3)
|
||
|
||
# 若标题里没有「第X场」,用场次补上(sectionTitle 可能是「赚最多那个月…」)
|
||
if args.field and "第" not in section_title and "场" not in section_title:
|
||
display_title = f"第{args.field}场|{section_title}"
|
||
else:
|
||
display_title = section_title
|
||
|
||
out_dir = args.out_dir
|
||
out_dir.mkdir(parents=True, exist_ok=True)
|
||
out_file = out_dir / f"{display_title}.md"
|
||
body = f"# {display_title}\n\n{content.strip()}\n"
|
||
out_file.write_text(body, encoding="utf-8")
|
||
print(f"已写入: {out_file}")
|
||
print(f"字数: {len(content)}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|