#!/usr/bin/env python3 """Скачать арты триальной деки с Cardfight!! Vanguard Wiki. Пример: python tools/import_card_art.py \ --url "https://cardfight.fandom.com/wiki/Trial_Deck_1:_Blaster_Blade" \ --output client/Assets/Vanguard/Resources/CardArt Fandom иногда отвечает Cloudflare 403 на запросы не из браузера. В этом случае сохраните HTML/API-ответ страницы из браузера и передайте: python tools/import_card_art.py --html saved.html --output ... """ from __future__ import annotations import argparse import html import json import re import sys import urllib.parse import urllib.request from pathlib import Path def fetch(url: str) -> str: request = urllib.request.Request( url, headers={ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 Chrome/126 Safari/537.36", "Accept": "text/html,application/json;q=0.9,*/*;q=0.8", }, ) with urllib.request.urlopen(request, timeout=60) as response: return response.read().decode("utf-8", errors="replace") def page_html(url: str, local_html: str | None) -> str: if local_html: raw = Path(local_html).read_text(encoding="utf-8", errors="replace") if raw.lstrip().startswith("{"): payload = json.loads(raw) return payload["parse"]["text"] return raw # Пытаемся через API: он отдаёт HTML статьи без рекламной обвязки. page = url.split("/wiki/", 1)[-1] api = ( "https://cardfight.fandom.com/api.php?action=parse&format=json" "&formatversion=2&prop=text&page=" + urllib.parse.quote(page) ) raw = fetch(api) payload = json.loads(raw) if "error" in payload: raise RuntimeError(payload["error"].get("info", "Wiki API error")) return payload["parse"]["text"] def set_gallery_links(page: str) -> list[str]: links = re.findall(r'href="(/wiki/Set_Gallery:[^"]+)"', page) return list(dict.fromkeys("https://cardfight.fandom.com" + link for link in links)) def image_candidates(html_text: str, code: str) -> list[str]: """Ищем картинку, у которой alt начинается с кода принта.""" pattern = re.compile(r"]*>", re.I) result: list[tuple[int, str]] = [] for tag in pattern.findall(html_text): alt_match = re.search(r'\balt="([^"]*)"', tag, re.I) src_match = re.search(r'\bsrc="([^"]*)"', tag, re.I) if not alt_match or not src_match: continue alt = html.unescape(alt_match.group(1)) src = html.unescape(src_match.group(1)) if not alt.startswith(code) or not src.startswith("https://static.wikia.nocookie.net/"): continue score = 0 if "Sample" in alt: score += 10 if "Holo" in alt or "Parallel" in alt: score += 5 result.append((score, src)) result.sort(key=lambda item: item[0]) urls = [] for _, src in result: # Берём оригинал, а не превью 126px. src = re.sub(r"/scale-to-width-down/\d+", "", src) src = src.split("?")[0] + "?format=original" if src not in urls: urls.append(src) return urls def save_image(url: str, output: Path, image_key: str) -> None: extension = ".png" if ".png" in url.lower() else ".jpg" target = output / (image_key + extension) if target.exists(): return request = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) with urllib.request.urlopen(request, timeout=60) as response: data = response.read() target.write_bytes(data) print(f" {target.name} ({len(data) // 1024} KiB)") def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--url", required=False, help="Страница триал-деки") parser.add_argument("--html", help="Сохранённый HTML/API-ответ страницы") parser.add_argument("--output", required=True, help="Папка CardArt") args = parser.parse_args() if not args.url and not args.html: parser.error("нужен --url или --html") output = Path(args.output) output.mkdir(parents=True, exist_ok=True) source_url = args.url or "https://cardfight.fandom.com/wiki/" page = page_html(source_url, args.html) galleries = set_gallery_links(page) if not galleries: print("Не нашёл ссылку Set_Gallery на странице.", file=sys.stderr) return 1 # Английские VGE-галереи приоритетнее, чтобы не перезаписать арт # японской/тайский версией. galleries.sort(key=lambda value: 0 if "/VGE-" in value else 1 if "/VG-" in value else 2) downloaded = 0 for gallery in galleries: try: gallery_html = page_html(gallery, None) except Exception as exc: # noqa: BLE001 print(f"Пропуск {gallery}: {exc}", file=sys.stderr) continue # Коды вида TD01/001EN, alt картинки: "TD01/001EN Name". codes = sorted(set(re.findall(r"\b(TD\d{2}/\d{3})(?:EN)?\b", gallery_html))) for code in codes: urls = image_candidates(gallery_html, code) if not urls: continue image_key = code.lower().replace("/", "_") try: save_image(urls[0], output, image_key) downloaded += 1 except Exception as exc: # noqa: BLE001 print(f"Ошибка {code}: {exc}", file=sys.stderr) print(f"Готово: обработано карт {downloaded}.") return 0 if __name__ == "__main__": raise SystemExit(main())