Files
Mycontent/scripts/batch_upload_ip_book_preserve_images.py
2026-04-13 14:32:32 +08:00

270 lines
9.6 KiB
Python
Raw 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
# -*- coding: utf-8 -*-
"""
《卡若的IP财富旅程》PART1-5 重新上传:正文以本地 MD 为准,已入库的图片块按顺序保留(不重新上传)。
- 从 chapters.content 提取现有 <p><img .../></p>(或裸 <img>)块,顺序与 MD 中独立图片行一一对应。
- 无图片的章节:与 batch_upload_ip_book.py 相同(纯文本清洗)。
- 同步 sort_order与 fix_ip_book_sort_and_images 一致)。
"""
from __future__ import annotations
import html
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 | 未来的一些思考"),
}
FILENAME_RE = re.compile(
r"^[✅⬜]?\s*(\d+)\.(\d+)[- ]+(.+?)(?:\.md)?$",
re.UNICODE,
)
LINE_IMAGE_ONLY = re.compile(r"^\s*!\[([^\]]*)\]\(([^)]+)\)\s*$")
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_plain(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 extract_img_blocks_from_db(old_content: str) -> list[str]:
"""从已入库正文中按文档顺序提取图片块(供复用 URL"""
if not old_content or "<img" not in old_content.lower():
return []
s = old_content.strip()
blocks: list[str] = []
for m in re.finditer(r"<p[^>]*>\s*(<img\b[^>]+/?>)\s*</p>", s, re.IGNORECASE | re.DOTALL):
blocks.append(m.group(0).strip())
if blocks:
return blocks
for m in re.finditer(r"<img\b[^>]+/?>", s, re.IGNORECASE):
blocks.append(f"<p>{m.group(0).strip()}</p>")
return blocks
def body_html_preserve_images(md_path: Path, old_content: str) -> tuple[str, list[str]]:
"""
按 fix_ip_book 的 HTML 段落规则生成正文,图片行使用旧库中的块。
返回 (body, warnings)
"""
warnings: list[str] = []
old_imgs = extract_img_blocks_from_db(old_content)
raw = md_path.read_text(encoding="utf-8")
lines = strip_md_title_line(raw).splitlines()
img_idx = 0
md_img_lines = sum(1 for ln in lines if LINE_IMAGE_ONLY.match(ln))
chunks: list[str] = []
for line in lines:
if line.strip() == "---":
chunks.append("")
continue
m = LINE_IMAGE_ONLY.match(line)
if m:
if img_idx < len(old_imgs):
chunks.append(old_imgs[img_idx])
img_idx += 1
else:
warnings.append(f"MD 图片行多于库中已存图片 ({md_path.name}),已跳过该行图片")
chunks.append("<p>(图片:库中无对应已上传块,请检查)</p>")
continue
stripped = line.replace("**", "").strip()
if stripped:
chunks.append(f"<p>{html.escape(stripped)}</p>")
else:
chunks.append("")
html_parts: list[str] = []
for c in chunks:
if c == "":
if html_parts and html_parts[-1] != "":
html_parts.append("")
else:
html_parts.append(c)
body = "\n".join(html_parts).strip() + "\n"
rest = len(old_imgs) - img_idx
if rest > 0:
warnings.append(f"{md_path.name}: 库中多 {rest} 张图片未在 MD 中引用,已从新正文移除(按 MD 为准)")
if md_img_lines and not old_imgs:
warnings.append(f"{md_path.name}: MD 含图片行但库中正文无 <img>,请先跑 fix_ip_book_sort_and_images 上传图片,或使用纯文本上传")
return body, warnings
def collect_files(part_num: int) -> list[tuple[str, str, 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
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
ip_id = f"ip{part_num}.{s_num}"
section_key = f"{part_num}.{s_num}"
title = title_raw
if section_key in seen_sections:
existing_path = seen_sections[section_key][2]
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 md_has_image_only_lines(md_path: Path) -> bool:
raw = md_path.read_text(encoding="utf-8")
text = strip_md_title_line(raw)
return any(LINE_IMAGE_ONLY.match(ln) for ln in text.splitlines())
def upsert_chapter(cur, chapter_id: str, chapter_title: str, ip_id: str, title: str, body: str, price: float, sort_order: int):
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, sort_order=%s WHERE id=%s",
(title, body, PART_ID, PART_TITLE, chapter_id, chapter_title, price, sort_order, ip_id),
)
return "updated"
cur.execute(
"INSERT INTO chapters (id, section_title, content, part_id, part_title, chapter_id, chapter_title, price, sort_order) "
"VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)",
(ip_id, title, body, PART_ID, PART_TITLE, chapter_id, chapter_title, price, sort_order),
)
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_err = 0
all_warnings: list[str] = []
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 sort_order, (ip_id, title, path) in enumerate(files, start=1):
try:
has_imgs = md_has_image_only_lines(path)
if has_imgs:
cur.execute("SELECT content FROM chapters WHERE id = %s", (ip_id,))
row = cur.fetchone()
old_content = row[0] if row and row[0] else ""
body, warns = body_html_preserve_images(path, old_content)
for w in warns:
all_warnings.append(f"{ip_id}: {w}")
else:
raw = path.read_text(encoding="utf-8")
body = for_miniprogram_body_plain(strip_md_title_line(raw))
except Exception as e:
print(f" ✗ 处理失败 {ip_id} ({path.name}): {e}")
total_err += 1
continue
if dry_run:
mode = "html+保留图" if has_imgs else "plain"
print(f" [dry] {ip_id} sort={sort_order} [{mode}] {title[:36]} ({path.name})")
total_ok += 1
continue
try:
action = upsert_chapter(
cur, chapter_id, chapter_title, ip_id, title, body, price, sort_order
)
conn.commit()
mode = "html" if has_imgs else "plain"
print(f" ✓ [{action}] {ip_id} sort={sort_order} [{mode}] {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_err}")
if all_warnings:
print("\n⚠ 提示:")
for w in all_warnings[:50]:
print(f" - {w}")
if len(all_warnings) > 50:
print(f" ... 另有 {len(all_warnings) - 50}")
if dry_run:
print("dry-run 模式,未实际写库)")
if __name__ == "__main__":
main()