Files
Mycontent/scripts/download_chapter_images_from_md.py
卡若 6d11fb295d feat: 同步本地三端改动并清理上传凭证风险
整合小程序、管理端与后端的最新本地改动,补齐用户管理与首页入口相关能力;提交前已完成敏感信息扫描,并移除本地 gitea 远程 URL 中的明文凭证,避免隐私信息进入远程仓库。

Made-with: Cursor
2026-04-06 15:59:34 +08:00

158 lines
4.5 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 -*-
"""
从书稿 Markdown 中抓取远程图片(![alt](url) 与 <img src="...">),下载到本地 images/
并把正文中的 URL 替换为相对路径 images/文件名。
用法:
python3 scripts/download_chapter_images_from_md.py /path/to/第140场.md
python3 scripts/download_chapter_images_from_md.py --images-dir /path/to/images a.md b.md
python3 scripts/download_chapter_images_from_md.py --dry-run a.md
依赖requests与 content_download.py 一致)
"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
from urllib.parse import unquote, urlparse
try:
import requests
except ImportError:
print("请安装: pip install requests", file=sys.stderr)
sys.exit(1)
MD_IMG = re.compile(r"!\[[^\]]*\]\((https?://[^)\s]+)\)")
HTML_IMG = re.compile(r'<img[^>]+src=["\'](https?://[^"\']+)["\']', re.I)
def collect_urls(text: str) -> list[str]:
seen: set[str] = set()
out: list[str] = []
for m in MD_IMG.finditer(text):
u = m.group(1).strip()
if u not in seen:
seen.add(u)
out.append(u)
for m in HTML_IMG.finditer(text):
u = m.group(1).strip()
if u not in seen:
seen.add(u)
out.append(u)
return out
def safe_filename(url: str, index: int) -> str:
path = unquote(urlparse(url).path)
base = Path(path).name
if not base or base == "/" or ".." in base:
base = f"image_{index:02d}.bin"
base = re.sub(r"[^\w.\-一-龥]", "_", base)
return base
def download_one(url: str, dest: Path, session: requests.Session) -> None:
r = session.get(url, timeout=60, headers={"User-Agent": "Mozilla/5.0"})
r.raise_for_status()
dest.write_bytes(r.content)
def process_file(
md_path: Path,
images_dir: Path,
dry_run: bool,
session: requests.Session,
) -> tuple[int, int]:
text = md_path.read_text(encoding="utf-8")
urls = collect_urls(text)
if not urls:
print(f"{md_path}: 无远程图片")
return 0, 0
used_names: dict[str, str] = {} # url -> local filename
basename_count: dict[str, int] = {}
ok = fail = 0
for i, url in enumerate(urls, 1):
name = safe_filename(url, i)
stem = Path(name).stem
ext = Path(name).suffix
if name in basename_count:
basename_count[name] += 1
name = f"{stem}_{basename_count[name]}{ext}"
else:
basename_count[name] = 0
local_rel = f"images/{name}"
used_names[url] = name
dest = images_dir / name
print(f" GET {url} -> {local_rel}")
if dry_run:
ok += 1
continue
images_dir.mkdir(parents=True, exist_ok=True)
try:
download_one(url, dest, session)
ok += 1
except Exception as e:
print(f" FAIL {url}: {e}", file=sys.stderr)
fail += 1
if dry_run or fail:
return ok, fail
new_text = text
for url, name in used_names.items():
local_rel = f"images/{name}"
new_text = new_text.replace(f"]({url})", f"]({local_rel})")
new_text = new_text.replace(f'src="{url}"', f'src="{local_rel}"')
new_text = new_text.replace(f"src='{url}'", f"src='{local_rel}'")
md_path.write_text(new_text, encoding="utf-8")
print(f" 已写回: {md_path}")
return ok, fail
def main() -> None:
ap = argparse.ArgumentParser(description="从 md 下载远程插图并改为相对路径")
ap.add_argument("md", nargs="+", type=Path, help="Markdown 文件路径")
ap.add_argument(
"--images-dir",
type=Path,
default=None,
help="图片目录,默认与第一个 md 同级的 images/",
)
ap.add_argument("--dry-run", action="store_true", help="只打印不下载、不改文件")
args = ap.parse_args()
first = args.md[0].resolve()
images_dir = (
args.images_dir.resolve()
if args.images_dir
else first.parent / "images"
)
session = requests.Session()
total_ok = total_fail = 0
for p in args.md:
p = p.resolve()
if not p.is_file():
print(f"跳过(非文件): {p}", file=sys.stderr)
continue
print(f"=== {p.name}")
ok, fail = process_file(p, images_dir, args.dry_run, session)
total_ok += ok
total_fail += fail
print(f"完成: 成功 {total_ok}, 失败 {total_fail}")
if total_fail:
sys.exit(1)
if __name__ == "__main__":
main()