#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
从书稿 Markdown 中抓取远程图片( 与
),下载到本地 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'
]+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()