← back to Quadrille Room Credit Enrich
scrape_captions.py
78 lines
#!/usr/bin/env python3
"""Fetch every Quadrille detail page (Editorials-Pages + Product-Shot-Pages),
extract each Editorial-Image URL -> caption (designer-name span + alt text).
Free HTTP only. Builds data/image_captions.json."""
import re, json, html as H, os, sys, time
from concurrent.futures import ThreadPoolExecutor
import urllib.request
BASE = "https://www.quadrillefabrics.com"
UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
OUT = "/Users/macstudio3/Projects/quadrille-room-credit-enrich/data"
urls = [u.strip() for u in open(f"{OUT}/detail_urls.txt") if u.strip()]
S3 = "quadrillefabrics.s3.us-east-2.amazonaws.com/Editorial-Images/"
def fetch(u):
try:
req = urllib.request.Request(u, headers={"User-Agent": UA})
with urllib.request.urlopen(req, timeout=25) as r:
return u, r.read().decode("utf-8", "replace")
except Exception as e:
return u, None
def parse(u, html):
"""Return list of (image_url, caption_text, alt_text, source_page)."""
out = []
if not html:
return out
# remove scripts/styles for clean text
body = re.sub(r"<script.*?</script>", "", html, flags=re.S | re.I)
body = re.sub(r"<style.*?</style>", "", body, flags=re.S | re.I)
# Find <img ... alt="..." src="...Editorial-Images/..."> capturing alt + src in any order
for m in re.finditer(r"<img\b[^>]*?>", body, re.S | re.I):
tag = m.group(0)
src = re.search(r"src=[\"']([^\"']*Editorial-Images/[^\"']+)[\"']", tag, re.I)
if not src:
continue
img = src.group(1)
alt = re.search(r"alt=[\"']([^\"']*)[\"']", tag, re.I)
alt = H.unescape(alt.group(1)).strip() if alt else ""
# caption: nearest designer-name span AFTER the img (within 1200 chars)
seg = body[m.end():m.end() + 1500]
cap = ""
cm = re.search(r"<span[^>]*class=[\"'][^\"']*designer-name[^\"']*[\"'][^>]*>(.*?)</span>", seg, re.S | re.I)
if cm:
cap = H.unescape(re.sub(r"<[^>]+>", " ", cm.group(1)))
cap = re.sub(r"\s+", " ", cap).strip()
out.append((img, cap, alt, u))
return out
def main():
results = {}
done = 0
with ThreadPoolExecutor(max_workers=12) as ex:
for u, html in ex.map(fetch, urls):
done += 1
if done % 200 == 0:
print(f" {done}/{len(urls)}", file=sys.stderr)
for img, cap, alt, page in parse(u, html):
# keep the richest caption seen for each image
prev = results.get(img)
score = len(cap) + len(alt)
if prev is None or score > prev["_score"]:
results[img] = {"caption": cap, "alt": alt, "page": page, "_score": score}
# strip score
for v in results.values():
v.pop("_score", None)
json.dump(results, open(f"{OUT}/image_captions.json", "w"), indent=0)
print(f"images captured: {len(results)}")
rich = sum(1 for v in results.values() if "Photography by" in v["caption"] or "As Seen in" in v["caption"]
or "Photography by" in v["alt"] or "As Seen in" in v["alt"])
print(f"images with photographer/publication text: {rich}")
if __name__ == "__main__":
t = time.time()
main()
print(f"elapsed {time.time()-t:.0f}s")