← back to Elitis Price 2026
auto-save: 2026-07-11T08:42:28 (3 files) — exec/__pycache__/elitis_feed.cpython-314.pyc exec/elitis_feed.py exec/extract_nuxt.js
ff985e8516753f959606df3a7a2a3255040f3e51 · 2026-07-11 08:42:30 -0700 · Steve Abrams
Files touched
A exec/__pycache__/elitis_feed.cpython-314.pycA exec/elitis_feed.pyA exec/extract_nuxt.js
Diff
commit ff985e8516753f959606df3a7a2a3255040f3e51
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Jul 11 08:42:30 2026 -0700
auto-save: 2026-07-11T08:42:28 (3 files) — exec/__pycache__/elitis_feed.cpython-314.pyc exec/elitis_feed.py exec/extract_nuxt.js
---
exec/__pycache__/elitis_feed.cpython-314.pyc | Bin 0 -> 9820 bytes
exec/elitis_feed.py | 128 +++++++++++++++++++++++++++
exec/extract_nuxt.js | 32 +++++++
3 files changed, 160 insertions(+)
diff --git a/exec/__pycache__/elitis_feed.cpython-314.pyc b/exec/__pycache__/elitis_feed.cpython-314.pyc
new file mode 100644
index 0000000..a80331c
Binary files /dev/null and b/exec/__pycache__/elitis_feed.cpython-314.pyc differ
diff --git a/exec/elitis_feed.py b/exec/elitis_feed.py
new file mode 100644
index 0000000..33f1c97
--- /dev/null
+++ b/exec/elitis_feed.py
@@ -0,0 +1,128 @@
+#!/usr/bin/env python3
+"""Elitis feed-first scraper (D). $0 - local curl + node-eval of the elitis.fr
+__NUXT__ hydration payload. NO Shopify writes, NO Browserbase, NO paid vision.
+
+Pipeline:
+ 1. sitemap-en.xml -> wall/panoramic/HP-contract collection URLs (~217).
+ 2. fetch each (polite delay), extract the window.__NUXT__ IIFE blob.
+ 3. node extract_nuxt.js -> [{name, slug, n, images:[originals]}] per page.
+ 4. aggregate pattern -> {collection, images} ; write elitis_feed.json.
+ 5. coverage: accent-normalized match vs our Needs-Image drafts + missing patterns.
+
+Real brand domain is elitis.fr (NOT elitis.com = an IT consultancy).
+Images come from backoffice.elitis.fr/media/cache/resolve/original/... (public)."""
+import sys, os, re, json, subprocess, time, urllib.request, unicodedata
+
+HERE = os.path.dirname(__file__)
+ROOT = os.path.join(HERE, "..")
+UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
+SITEMAP = "https://elitis.fr/sitemap-en.xml"
+WALL_CATS = ("walls", "panoramic", "high-performance-contract")
+FEED_OUT = os.path.join(ROOT, "elitis_feed.json")
+NUXT_JS = os.path.join(HERE, "extract_nuxt.js")
+
+
+def norm(s):
+ s = unicodedata.normalize("NFKD", s or "")
+ s = "".join(c for c in s if not unicodedata.combining(c))
+ return " ".join(s.casefold().split())
+
+
+def fetch(url, timeout=30):
+ req = urllib.request.Request(url, headers={"User-Agent": UA})
+ with urllib.request.urlopen(req, timeout=timeout) as r:
+ return r.read().decode("utf-8", "replace")
+
+
+def collection_urls():
+ xml = fetch(SITEMAP)
+ urls = re.findall(r"<loc>(https://elitis\.fr/en/collections/[^<]+)</loc>", xml)
+ out = [u for u in urls if u.split("/collections/", 1)[1].split("/")[0] in WALL_CATS
+ and u.count("/collections/") and len(u.split("/collections/")[1].split("/")) == 2]
+ return sorted(set(out))
+
+
+def extract_page(html):
+ m = re.search(r"window\.__NUXT__\s*=\s*", html)
+ if not m:
+ return []
+ end = html.find("</script>", m.end())
+ blob = html[m.end():end].strip().rstrip(";")
+ p = subprocess.run(["node", NUXT_JS], input=blob, capture_output=True, text=True, timeout=60)
+ try:
+ data = json.loads(p.stdout)
+ except Exception:
+ return []
+ return data if isinstance(data, list) else []
+
+
+NAV = {"walls", "fabric", "accessoire", "panoramic", "outdoor", "in-outdoor",
+ "high-performance-contract", "high performance contract"}
+
+
+def run_scrape():
+ urls = collection_urls()
+ print(f"wall/panoramic/HP collection pages: {len(urls)}")
+ feed = {} # norm(pattern) -> {name, collection, images:set}
+ errs = 0
+ for i, u in enumerate(urls):
+ coll = u.rsplit("/", 1)[1]
+ try:
+ pats = extract_page(fetch(u))
+ except Exception as e:
+ errs += 1
+ print(f" [err] {coll}: {str(e)[:70]}")
+ continue
+ for p in pats:
+ nm = (p.get("name") or "").strip()
+ if not nm or norm(nm) in {norm(x) for x in NAV}:
+ continue
+ key = norm(nm)
+ slot = feed.setdefault(key, {"name": nm, "collection": coll, "images": []})
+ for img in p.get("images", []):
+ if img not in slot["images"]:
+ slot["images"].append(img)
+ if (i + 1) % 25 == 0:
+ print(f" ...{i+1}/{len(urls)} patterns={len(feed)} errs={errs}")
+ time.sleep(0.3)
+ out = {k: {"name": v["name"], "collection": v["collection"],
+ "n_images": len(v["images"]), "images": v["images"]}
+ for k, v in feed.items()}
+ json.dump(out, open(FEED_OUT, "w"), ensure_ascii=False, indent=1)
+ print(f"\nWROTE {FEED_OUT}: {len(out)} patterns, "
+ f"{sum(x['n_images'] for x in out.values())} image URLs, errs={errs}")
+ return out
+
+
+def coverage(feed=None):
+ if feed is None:
+ feed = json.load(open(FEED_OUT))
+ feed_norm = set(feed.keys())
+ # Needs-Image draft patterns: derive from staging join + the 47 missing list is implicit
+ sj = os.path.join(ROOT, "OUT_577_staging_join.csv")
+ ndc = os.path.join(ROOT, "OUT_577_no_catalog_data.csv")
+ import csv
+ want = {}
+ for path, tag in [(sj, "staging"), (ndc, "no-data")]:
+ if not os.path.exists(path):
+ continue
+ for r in csv.DictReader(open(path)):
+ pat = r.get("pattern") or ""
+ if pat:
+ want.setdefault(norm(pat), (pat, tag))
+ hit = {k: v for k, v in want.items() if k in feed_norm}
+ miss = {k: v for k, v in want.items() if k not in feed_norm}
+ print(f"\nCOVERAGE vs list patterns needing data: {len(want)} distinct")
+ print(f" COVERED by feed (>=1 image): {len(hit)}")
+ print(f" still missing : {len(miss)}")
+ print(" -- still-missing patterns --")
+ for k, (pat, tag) in sorted(miss.items()):
+ print(f" {pat!r} [{tag}]")
+
+
+if __name__ == "__main__":
+ if "--coverage" in sys.argv:
+ coverage()
+ else:
+ feed = run_scrape()
+ coverage(feed)
diff --git a/exec/extract_nuxt.js b/exec/extract_nuxt.js
new file mode 100644
index 0000000..3d8d3d2
--- /dev/null
+++ b/exec/extract_nuxt.js
@@ -0,0 +1,32 @@
+// Reads an elitis.fr __NUXT__ IIFE blob on stdin, evals it in a sandboxed window,
+// walks for pattern nodes ({name, images:[{cachedPath|path}]}) and prints ONE
+// JSON array to stdout. $0 (local node). No network.
+let blob = "";
+process.stdin.on("data", d => (blob += d));
+process.stdin.on("end", () => {
+ try {
+ globalThis.window = {};
+ // eslint-disable-next-line no-eval
+ (0, eval)("window.__NUXT__=" + blob);
+ const acc = [];
+ const seen = new Set();
+ (function walk(o) {
+ if (o && typeof o === "object") {
+ if (o.name && Array.isArray(o.images) && o.images.length) {
+ const imgs = o.images
+ .map(x => (x && (x.cachedPath || x.path)) || null)
+ .filter(Boolean);
+ const key = o.slug + "|" + o.name + "|" + imgs.length;
+ if (imgs.length && !seen.has(key)) {
+ seen.add(key);
+ acc.push({ name: o.name, slug: o.slug || null, n: imgs.length, images: imgs });
+ }
+ }
+ for (const k in o) { try { walk(o[k]); } catch (e) {} }
+ }
+ })(window.__NUXT__);
+ process.stdout.write(JSON.stringify(acc));
+ } catch (e) {
+ process.stdout.write(JSON.stringify({ error: String(e).slice(0, 200) }));
+ }
+});
← 8620c28 WS3b: Bucket B accent/fuzzy matcher recovers 30 draft colorw
·
back to Elitis Price 2026
·
D: elitis.fr feed-first scraper ($0 __NUXT__ harvest) -> 559 d3ccffb →