#!/usr/bin/env python3 """ Site Cloner v2 — smarter resource extraction """ import os, re, time, json, hashlib, urllib.request, urllib.parse from pathlib import Path TARGET_URL = "https://minutedrama.com/en" OUTPUT_ROOT = Path("/www/wwwroot/www.q22.pro/dev/minutedrama") MAX_RETRY = 3 RETRY_DELAY = 2 TIMEOUT = 30 MAX_VIDEO_MB = 50 UA_LIST = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Version/17.0 Safari/605.1.15", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/122.0 Safari/537.36", ] DIRS = { 'css': OUTPUT_ROOT / 'static/css', 'js': OUTPUT_ROOT / 'static/js', 'images': OUTPUT_ROOT / 'static/images', 'fonts': OUTPUT_ROOT / 'static/fonts', 'flash': OUTPUT_ROOT / 'static/flash', } for d in DIRS.values(): d.mkdir(parents=True, exist_ok=True) EXT_MAP = { **{e: 'css' for e in ['css']}, **{e: 'js' for e in ['js', 'mjs']}, **{e: 'images' for e in ['jpg','jpeg','png','gif','webp','svg','ico','bmp','avif']}, **{e: 'fonts' for e in ['woff','woff2','ttf','eot','otf']}, **{e: 'flash' for e in ['mp4','webm','ogv','ogg','mp3','wav','swf','flv']}, } IMAGE_EXTS = {'jpg','jpeg','png','gif','webp','svg','ico','bmp','avif'} RESOURCE_EXTS = IMAGE_EXTS | {'css','js','mjs','woff','woff2','ttf','eot','otf','mp4','webm'} RESOURCE_PATHS = ['/css2', '/css'] url_to_local = {} failed_urls = [] skipped_large = [] queue = [] queued_set = set() parsed_files = set() def normalize_url(url, base): url = url.strip().strip('"\'') if not url or url.startswith('data:') or url.startswith('javascript:') or url.startswith('#'): return None if not (url.startswith('http://') or url.startswith('https://') or url.startswith('//') or url.startswith('/')): return None abs_url = urllib.parse.urljoin(base, url) parsed = urllib.parse.urlparse(abs_url) if parsed.scheme not in ('http', 'https'): return None return urllib.parse.urlunparse(parsed._replace(fragment='')) def has_resource_ext(url): path = urllib.parse.urlparse(url).path ext = path.rsplit('.', 1)[-1].lower().split('?')[0] if '.' in path else '' if ext in RESOURCE_EXTS: return True for rp in RESOURCE_PATHS: if path == rp or path.startswith(rp + '?'): return True return False def get_category(url): path = urllib.parse.urlparse(url).path ext = path.rsplit('.', 1)[-1].lower().split('?')[0] if '.' in path else '' return EXT_MAP.get(ext, 'images') def safe_filename(url): path = urllib.parse.urlparse(url).path name = path.rsplit('/', 1)[-1].split('?')[0] name = re.sub(r'[<>:"/\\|?*]', '_', name) if len(name) > 100: ext = name.rsplit('.', 1)[-1] if '.' in name else '' name = hashlib.md5(name.encode()).hexdigest()[:16] + (f'.{ext}' if ext else '') return name or hashlib.md5(url.encode()).hexdigest()[:10] def unique_dest(cat, fname): dest = DIRS[cat] / fname if not dest.exists(): return dest stem = fname.rsplit('.', 1)[0] if '.' in fname else fname suf = ('.' + fname.rsplit('.', 1)[1]) if '.' in fname else '' for i in range(2, 9999): c = DIRS[cat] / f"{stem}_{i}{suf}" if not c.exists(): return c def local_rel_path(p: Path) -> str: return str(p.relative_to(OUTPUT_ROOT)).replace('\\', '/') def download_url(url) -> bytes | None: for attempt in range(MAX_RETRY): ua = UA_LIST[attempt % len(UA_LIST)] try: req = urllib.request.Request(url, headers={ 'User-Agent': ua, 'Referer': TARGET_URL, 'Accept': '*/*', }) with urllib.request.urlopen(req, timeout=TIMEOUT) as r: cl = r.headers.get('Content-Length') if cl and int(cl) > MAX_VIDEO_MB * 1024 * 1024: if get_category(url) == 'flash': print(f" ⏭ Skip large({int(cl)//1024//1024}MB): {url.split('?')[0]}") skipped_large.append((url, int(cl))) return None return r.read() except UnicodeEncodeError: print(f" ⚠️ Invalid URL (skipping): {url[:80]}...") return None except urllib.error.HTTPError as e: if e.code in (404, 403): print(f" ⚠️ {e.code} skip: {url.split('?')[0]}") failed_urls.append(url) return None print(f" ⚠️ HTTP {e.code} [{url.split('?')[0]}] attempt {attempt+1}") if attempt < MAX_RETRY - 1: time.sleep(RETRY_DELAY * (attempt + 1)) except Exception as e: print(f" ⚠️ {e.__class__.__name__} [{url.split('?')[0][:50]}] attempt {attempt+1}") if attempt < MAX_RETRY - 1: time.sleep(RETRY_DELAY * (attempt + 1)) failed_urls.append(url) return None def enqueue(url): if url and url not in queued_set: queued_set.add(url) queue.append(url) def process_one(url): if url in url_to_local: return url_to_local[url] cat = get_category(url) fname = safe_filename(url) dest = unique_dest(cat, fname) data = download_url(url) if not data: return None dest.write_bytes(data) lp = local_rel_path(dest) url_to_local[url] = lp print(f" ✅ {lp} ← {url.split('?')[0]}") return lp # ============== URL Extraction ============== def extract_urls_from_html(html, base): """Extract resource URLs, filtering to only real resources""" found = set() # 1. Extract ', '', html, flags=re.S | re.I) # 3. (CSS, favicons, fonts, preload) for m in re.finditer(r']+href=["\']([^"\']+)["\']', html_no_scripts, re.I): u = normalize_url(m.group(1), base) if u and has_resource_ext(u): found.add(u) # 4. , ,