#!/usr/bin/env python3 """Crawl www.mitsuthailand.com -> site/ as a static mirror with dynamic (relative) URLs.""" import os, re, queue, threading, urllib.parse, html import httpx from common import (BASE_HOST, BASE_URL, MEDIA_HOST, YP_HOST, OUT_DIR, assets_rel_for, safe_asset_path, enqueue_asset, claim_asset_urls, mark_asset_result, is_downloaded, canonical_page_path) client = httpx.Client( follow_redirects=True, timeout=40.0, headers={"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36", "Accept-Language": "th,en;q=0.9"}, ) # ---------------- URL classification ---------------- def resolve_url(raw, base): raw = html.unescape(raw).strip() if not raw or raw.startswith(("data:", "mailto:", "tel:", "javascript:", "#")): return None if raw.startswith(("//", "http")): parsed = urllib.parse.urlparse(raw) host = (parsed.hostname or "").lower() if host not in (BASE_HOST, MEDIA_HOST, YP_HOST): return None if "/lp/" in parsed.path or parsed.path.startswith("/lp"): return None return urllib.parse.urlunparse(parsed._replace(scheme="https")) if parsed.scheme in ("", "http") else raw absurl = urllib.parse.urljoin(base, raw) parsed = urllib.parse.urlparse(absurl) host = (parsed.hostname or "").lower() if host and host not in (BASE_HOST, MEDIA_HOST, YP_HOST): return None if "/lp/" in parsed.path or parsed.path.startswith("/lp"): return None return absurl def is_page_url(url): parsed = urllib.parse.urlparse(url) host = (parsed.hostname or "").lower() # The www.yellowpages.co.th/lp/... pages are a YellowPages mirror of the same content # and will die too; skip them. Only mirror the main mitsuthailand.com pages. if host == YP_HOST: return False path = parsed.path.rstrip("/") # Skip /lp/... "live preview" duplicate copies of the same content (very long paths). if "/lp/" in path or path.startswith("/lp"): return False # skip Drupal file assets (they have extensions or are /sites /core /themes /system paths) if path.startswith(("/sites/", "/core/", "/themes/", "/system/", "/sites")): return False ext = os.path.splitext(path)[1].lower() # Drupal content routes (no file extension) are pages return ext in (".html", "") # ---------------- asset workers ---------------- _jobs = queue.Queue() _shutdown = threading.Event() _submitted = set() # URLs already pushed to _jobs (guarantees no dup put) _sub_lock = threading.Lock() def start_workers(n=12): for _ in range(n): t = threading.Thread(target=_worker, daemon=True) t.start() def stop_workers(): _shutdown.set() def submit_assets(urls): """Push each URL to _jobs exactly once; returns count of new submissions.""" new = [] with _sub_lock: for u in urls: if u not in _submitted and not is_downloaded(u): _submitted.add(u) new.append(u) for u in new: _jobs.put(u) return len(new) def _worker(): while not _shutdown.is_set(): try: url = _jobs.get(timeout=0.5) except queue.Empty: continue try: r = client.get(url) if r.status_code != 200: print(f" [asset {r.status_code}] {url}") mark_asset_result(url, None) continue ctype = r.headers.get("content-type", "") data = r.content rel = safe_asset_path(assets_rel_for(url), url) dest = os.path.join(OUT_DIR, rel) os.makedirs(os.path.dirname(dest), exist_ok=True) if "css" in ctype or url.rstrip("?;").endswith(".css"): try: text = data.decode("utf-8", "replace") text = rewrite_css(url, text) data = text.encode("utf-8") except Exception as e: print(" [css err]", e) with open(dest, "wb") as f: f.write(data) mark_asset_result(url, rel) except Exception as e: print(f" [asset ERR] {url}: {e}") mark_asset_result(url, None) # ---------------- CSS rewriting ---------------- def rewrite_css(css_url, css_text): def repl(m): u = m.group(1).strip() if u.startswith(("data:", "#", "http", "//")) or u.startswith("url("): return m.group(0) abs = urllib.parse.urljoin(css_url, u) parsed = urllib.parse.urlparse(abs) host = (parsed.hostname or "").lower() if host and host not in (BASE_HOST, MEDIA_HOST, YP_HOST): return m.group(0) rel = safe_asset_path(assets_rel_for(abs), abs) enqueue_asset(abs) # css files live under assets/