← back to Japan Enrich

scrape_sangetsu_brands.py

70 lines

#!/usr/bin/env python3
"""Scrape the REAL per-pattern distributor (the .psg-single-brands-main brand link:
Koroseal / Eijffinger / J.Josephson / Omexco / Versa / Cole & Son / ...) from each
Sangetsu pattern page on sangetsu-goodrich.co.th, into staging/sangetsu-brands.json
as {source_url: {distributor, slug}}. Headless Chrome (JS-rendered site), parallel,
RESUMABLE (skips URLs already in the output). $0 local."""
import json, os, re, subprocess, sys
from concurrent.futures import ThreadPoolExecutor, as_completed

HERE = os.path.dirname(__file__)
STAGING = os.path.join(HERE, 'staging', 'sangetsu-staging.jsonl')
OUT = os.path.join(HERE, 'staging', 'sangetsu-brands.json')
CHROME = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
WORKERS = int(os.environ.get('WORKERS', '6'))

def urls():
    s = set()
    for line in open(STAGING):
        line = line.strip()
        if line:
            u = json.loads(line).get('source_url')
            if u: s.add(u)
    return sorted(s)

BRAND_RE = re.compile(r'psg-single-brands-main"?>(.*?)</div>', re.S | re.I)
LINK_RE = re.compile(r'/brand/([a-z0-9-]+)/"[^>]*>([^<]+)<', re.I)

def scrape(u):
    try:
        html = subprocess.run(
            [CHROME, "--headless=new", "--disable-gpu", "--no-sandbox",
             "--virtual-time-budget=11000", "--dump-dom", u],
            capture_output=True, timeout=45, text=True, errors='ignore').stdout
        m = BRAND_RE.search(html or '')
        if not m: return u, None, None
        a = LINK_RE.search(m.group(1))
        if a: return u, a.group(2).strip(), a.group(1).strip()
        txt = re.sub(r'\s+', ' ', re.sub(r'<[^>]+>', ' ', m.group(1))).strip()
        return u, (txt.split('  ')[0] or None), None
    except Exception:
        return u, None, None

def main():
    out = {}
    if os.path.exists(OUT):
        try: out = json.load(open(OUT))
        except Exception: out = {}
    todo = [u for u in urls() if u not in out]
    print(f"total {len(out)+len(todo)} patterns | already {len(out)} | scraping {len(todo)} | {WORKERS} workers")
    done = 0
    with ThreadPoolExecutor(max_workers=WORKERS) as ex:
        futs = {ex.submit(scrape, u): u for u in todo}
        for f in as_completed(futs):
            u, dist, slug = f.result()
            out[u] = {"distributor": dist, "slug": slug}
            done += 1
            if done % 25 == 0 or done == len(todo):
                json.dump(out, open(OUT, 'w'), ensure_ascii=False, indent=0)
                print(f"  {done}/{len(todo)} … last: {dist!r}")
    json.dump(out, open(OUT, 'w'), ensure_ascii=False, indent=0)
    from collections import Counter
    c = Counter(v.get('distributor') for v in out.values())
    print("DONE. distributor tally:")
    for k, n in c.most_common(): print(f"  {n:4d}  {k!r}")
    miss = sum(1 for v in out.values() if not v.get('distributor'))
    print(f"missing/none: {miss}")

if __name__ == '__main__':
    main()