← back to Designer Wallcoverings
sangetsu: parse 5 pricebook PDFs -> 1,007 domestic-SKU specs (width/repeat/fire/price/functions)
d613c488345dc260d459774cb9a642577d07d696 · 2026-07-02 07:28:56 -0700 · Steve
Files touched
A onboarding/sangetsu-lilycolor/scripts/parse-sangetsu-pdf-specs.py
Diff
commit d613c488345dc260d459774cb9a642577d07d696
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Jul 2 07:28:56 2026 -0700
sangetsu: parse 5 pricebook PDFs -> 1,007 domestic-SKU specs (width/repeat/fire/price/functions)
---
.../scripts/parse-sangetsu-pdf-specs.py | 146 +++++++++++++++++++++
1 file changed, 146 insertions(+)
diff --git a/onboarding/sangetsu-lilycolor/scripts/parse-sangetsu-pdf-specs.py b/onboarding/sangetsu-lilycolor/scripts/parse-sangetsu-pdf-specs.py
new file mode 100644
index 00000000..6090f20d
--- /dev/null
+++ b/onboarding/sangetsu-lilycolor/scripts/parse-sangetsu-pdf-specs.py
@@ -0,0 +1,146 @@
+#!/usr/bin/env python3
+"""Parse the 5 Sangetsu Japanese price-book .txt extracts (pricebooks/*.txt) into a
+clean per-SKU spec table -> staging/sangetsu-pdf-specs.jsonl.
+
+Row shape in the 価格表 (price table):
+ [item#] [SKU(s)] [unit] [price1] [price2] [functions...] 巾<W>cm[×..][切売可/巻] \
+ [repeat_v] [repeat_h] [range] [fire: 不燃 NM-#### 準不燃 QM-#### 難燃 RM-####...]
+
+SKUs expand: LIS-42001~42003 -> 42001,42002,42003 ; LL-6913・6914 -> 6913,6914
+Domestic SKU namespace (LV/LW/LL/LMT/LIS/LR/LWA/LWT/LLT) — NOT the .co.th export SKUs.
+"""
+import re, json, os, sys
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+PROJ = os.path.dirname(HERE)
+PB = os.path.join(PROJ, "pricebooks")
+OUT = os.path.join(PROJ, "staging", "sangetsu-pdf-specs.jsonl")
+
+COLLECTION = { # file -> human collection label
+ "V-wall": "SP (V-wall / vinyl)", "WILL": "WILL", "LIGHT": "LIGHT",
+ "MATERIALS": "MATERIALS", "ImportSelection": "Import Selection",
+}
+
+# a spec row must carry a width token; that's our anchor
+WIDTH_RE = re.compile(r"巾\s*([0-9]+(?:\.[0-9]+)?)\s*cm")
+# SKU head: LETTERS + '-' + first number, optionally with a space after '-'
+SKUHEAD_RE = re.compile(r"\b(L[A-Z]{0,2}T?A?)-\s?([0-9]{4,6})")
+# fire certs
+FIRE_RE = re.compile(r"(不燃|準不燃|難燃)\s*([A-Z]{2}-[0-9]{3,4})")
+# functions vocabulary seen in the books
+FUNCS = ["帯電防止", "消臭", "抗菌", "抗ウイルス", "防カビ", "通気性", "透湿性",
+ "吸放湿", "ホツレ止", "表面強化", "汚れ防止", "水拭き", "スーパー耐久",
+ "マイナスイオン", "蓄光", "屋外", "オレフィン"]
+# repeat_v repeat_h range(e.g 1-4) — the reliable middle anchor
+REP_RANGE_RE = re.compile(
+ r"(—|―|-|[0-9]+(?:\.[0-9]+)?)\s+(—|―|-|[0-9]+(?:\.[0-9]+)?)\s+([0-9]-[0-9])\b")
+# Real ¥ prices in these books are ALWAYS comma-grouped (1,000 / 40,000 / 7,700).
+# Requiring a comma excludes bare SKU digits (42001) that share the row.
+PRICE_RE = re.compile(r"([0-9]{1,3}(?:,[0-9]{3})+)")
+
+
+def expand_skus(seg, prefix):
+ """seg is the raw SKU chunk after prefix, e.g. '2001・2002' or '1742~1745'.
+ Returns list of full SKUs like ['LWA-1742', ...]."""
+ seg = seg.replace("、", "・").replace(",", "・").strip()
+ out = []
+ for part in seg.split("・"):
+ part = part.strip()
+ m = re.match(r"([0-9]{4,6})\s*[~~]\s*([0-9]{4,6})$", part)
+ if m:
+ a, b = int(m.group(1)), int(m.group(2))
+ if 0 <= b - a <= 60:
+ out += [f"{prefix}-{n}" for n in range(a, b + 1)]
+ continue
+ m2 = re.match(r"([0-9]{4,6})$", part)
+ if m2:
+ out.append(f"{prefix}-{m2.group(1)}")
+ return out
+
+
+def parse_row(line):
+ wm = WIDTH_RE.search(line)
+ if not wm:
+ return None
+ sh = SKUHEAD_RE.search(line)
+ if not sh:
+ return None
+ prefix = sh.group(1)
+ # SKU chunk = from first number up to the unit token (m/m/本/枚/特) or width
+ after = line[sh.start(2):]
+ chunk = re.split(r"\s+(?:m|m|本|枚|特)\s|\s{2,}巾|\s+巾", after, maxsplit=1)[0]
+ skus = expand_skus(chunk, prefix)
+ if not skus:
+ skus = [f"{prefix}-{sh.group(2)}"]
+
+ unit = None
+ # \b doesn't bound CJK; match unit token followed by whitespace instead.
+ um = re.search(r"(m|m|本|枚)(?=\s)", line[sh.end():])
+ if um:
+ unit = {"m": "m", "m": "m", "本": "roll", "枚": "sheet"}[um.group(1)]
+
+ # prices: first two comma-grouped ¥ numbers after the SKU chunk (or unit)
+ start = (sh.end() + um.end()) if um else sh.end()
+ prices = [int(p.replace(",", "")) for p in PRICE_RE.findall(line[start:])][:2]
+ price_product = prices[0] if len(prices) >= 1 else None
+ price_material = prices[1] if len(prices) >= 2 else None
+
+ width_cm = float(wm.group(1))
+ roll = re.search(r"巾[0-9.]+cm\s*×\s*([0-9.]+)m", line)
+ roll_len_m = float(roll.group(1)) if roll else None
+ cut_sale = "切売可" in line
+
+ rr = REP_RANGE_RE.search(line[wm.end():])
+ def num(x):
+ return None if x in ("—", "―", "-") else float(x)
+ rep_v = rep_h = rng = None
+ if rr:
+ rep_v, rep_h, rng = num(rr.group(1)), num(rr.group(2)), rr.group(3)
+
+ fires = [{"grade": g, "cert": c} for g, c in FIRE_RE.findall(line)]
+ funcs = [f for f in FUNCS if f in line]
+
+ return {
+ "unit": unit, "price_product_yen": price_product,
+ "price_material_yen_m2": price_material,
+ "width_cm": width_cm, "roll_len_m": roll_len_m, "cut_sale": cut_sale,
+ "repeat_v_cm": rep_v, "repeat_h_cm": rep_h, "roll_class": rng,
+ "fire_ratings": fires, "functions": funcs,
+ }, skus
+
+
+def main():
+ rows = {}
+ per_file = {}
+ for fn, coll in COLLECTION.items():
+ path = os.path.join(PB, fn + ".txt")
+ if not os.path.exists(path):
+ continue
+ n = 0
+ for line in open(path, encoding="utf-8"):
+ r = parse_row(line.rstrip("\n"))
+ if not r:
+ continue
+ spec, skus = r
+ for sku in skus:
+ rows[sku] = {"sku": sku, "collection": coll, "source_pdf": fn + ".pdf",
+ **spec, "_source": "sangetsu-pricebook-pdf"}
+ n += 1
+ per_file[fn] = n
+ with open(OUT, "w", encoding="utf-8") as f:
+ for sku in sorted(rows):
+ f.write(json.dumps(rows[sku], ensure_ascii=False) + "\n")
+ # report
+ have_w = sum(1 for x in rows.values() if x["width_cm"])
+ have_rep = sum(1 for x in rows.values() if x["repeat_v_cm"] is not None or x["repeat_h_cm"] is not None)
+ have_fire = sum(1 for x in rows.values() if x["fire_ratings"])
+ have_price = sum(1 for x in rows.values() if x["price_product_yen"])
+ print(json.dumps({
+ "total_sku_specs": len(rows), "per_file_rows": per_file,
+ "with_width": have_w, "with_repeat": have_rep,
+ "with_fire": have_fire, "with_price": have_price, "out": OUT,
+ }, ensure_ascii=False, indent=2))
+
+
+if __name__ == "__main__":
+ main()
← 005e77b9 auto-save: 2026-07-02T06:41:33 (6 files) — pending-approval/
·
back to Designer Wallcoverings
·
sangetsu: store-api detail enrich — descriptions 4->614 patt 1f7a0643 →