← back to Stroheim Onboard

scrape_details.py

86 lines

#!/usr/bin/env python3
"""
Stroheim detail-spec enrichment — feed-first, $0 local.
Reads data/listing.jsonl, fetches each public detail page, extracts
width / repeat / content(material) / book(collection) / type.
Writes data/details.jsonl  (append-safe: skips skus already done).
"""
import re, json, sys, os, time
from concurrent.futures import ThreadPoolExecutor, as_completed
import urllib.request

UA = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 "
      "(KHTML, like Gecko) Version/17 Safari/605.1.15")

def fetch(url, tries=3):
    for t in range(tries):
        try:
            req = urllib.request.Request(url, headers={"User-Agent": UA})
            with urllib.request.urlopen(req, timeout=30) as r:
                return r.read().decode("utf-8", "replace")
        except Exception:
            if t == tries - 1:
                return ""
            time.sleep(1.2 * (t + 1))
    return ""

def spec(html, *labels):
    """Find a spec value following any of the label words in a tech block."""
    for lab in labels:
        m = re.search(rf'{lab}\s*[:<][^>]*>?\s*</?[^>]*>?\s*([^<]{{1,60}})', html, re.I)
        if m:
            v = re.sub(r'\s+', ' ', m.group(1)).strip(" : ")
            if v and v.lower() not in ('', 'n/a'):
                return v
    return None

def parse_detail(html):
    # narrow to product tech / specs region if present
    tech = ''
    i = html.find('product_tech')
    if i != -1:
        tech = html[i:i+4000]
    region = tech or html
    d = {}
    d['width'] = spec(region, 'Width')
    d['repeat_v'] = spec(region, 'Vertical Repeat', 'Repeat')
    d['repeat_h'] = spec(region, 'Horizontal Repeat')
    d['material'] = spec(region, 'Content', 'Composition', 'Material')
    d['match_type'] = spec(region, 'Match')
    d['book'] = spec(region, 'Book', 'Collection')
    # product type from breadcrumb/type label
    tm = re.search(r'<div class="type">\s*([^<]+?)\s*</div>', html)
    d['type_label'] = re.sub(r'\s+', ' ', tm.group(1)).strip() if tm else None
    return d

def main():
    listing = [json.loads(l) for l in open('data/listing.jsonl')]
    done = set()
    if os.path.exists('data/details.jsonl'):
        for l in open('data/details.jsonl'):
            try:
                done.add(json.loads(l)['mfr_sku'])
            except (json.JSONDecodeError, KeyError):
                pass
    todo = [r for r in listing if r['mfr_sku'] not in done]
    sys.stderr.write(f"details: {len(todo)} to fetch ({len(done)} already done)\n")
    n = 0
    with open('data/details.jsonl', 'a') as out:
        with ThreadPoolExecutor(max_workers=10) as ex:
            futs = {ex.submit(fetch, r['product_url']): r for r in todo}
            for f in as_completed(futs):
                r = futs[f]
                html = f.result()
                rec = {"mfr_sku": r['mfr_sku']}
                if html:
                    rec.update(parse_detail(html))
                out.write(json.dumps(rec) + "\n")
                out.flush()
                n += 1
                if n % 200 == 0:
                    sys.stderr.write(f"  {n}/{len(todo)}\n")
    print(f"DONE details: {n} fetched")

if __name__ == "__main__":
    main()