# -*- coding: utf-8 -*- """Parse public SMM panel service catalogues into a normalised service list.""" import gzip, re, json, html, os, sys # Reads the stored captures next to this script and writes the parsed list back # beside them. Until 13 September 2026 this line named a temporary directory on # the laptop that parsed the first capture, and the parser wrote `_parsed.json` # into it - so the published script could not run on the published pages, on a # survey whose method note says it reproduces offline. Run against `pages/` it # rebuilds the published `services-all.json.gz` row for row (checked: 44,064 of # 44,064 identical). # # python3 parse_panels.py # pages/*.html.gz -> services-all.json.gz (2 Sep capture) # python3 parse_panels.py w2 # pages/w2/*.html.gz -> services-all-w2.json.gz (13 Sep capture) HERE = os.path.dirname(os.path.abspath(__file__)) WAVE = sys.argv[1] if len(sys.argv) > 1 else "" if WAVE and not re.fullmatch(r"w[0-9]+", WAVE): sys.exit("usage: parse_panels.py [w2|w3|...]") R = os.path.join(HERE, "pages", WAVE) if WAVE else os.path.join(HERE, "pages") OUT = os.path.join(HERE, "services-all%s.json.gz" % ("-" + WAVE if WAVE else "")) COMMENT = re.compile(r"", re.S) def load(n): h = gzip.open(os.path.join(R, n + ".html.gz"), "rt", encoding="utf-8", errors="replace").read() return COMMENT.sub("", h) # strip HTML comments (they split prices/names in some templates) TAG = re.compile(r"<[^>]+>") def txt(s): s = re.sub(r"", " \n ", s, flags=re.I) s = TAG.sub("", s) s = html.unescape(s) s = s.replace(" ", " ").replace(" ", " ").replace(" ", " ") s = re.sub(r"[ \t]+", " ", s) s = re.sub(r"\s*\n\s*", " | ", s) return s.strip() def num(s): if s is None: return None s = s.replace(" ", "").replace(",", "").replace(" ", "") m = re.search(r"\d+(?:\.\d+)?", s) return float(m.group()) if m else None def cur(s): for sym in ["$", "₹", "€", "£", "₺", "₽", "R$"]: if sym in s: return sym return "" # ---------------- generic parser ----------------- def parse_table(h, panel): """Parse every
whose header row names a per-1000 rate column.""" out = [] for tm in re.finditer(r"]*>(.*?)
", h, re.S | re.I): tbl = tm.group(1) hm = re.search(r"]*>(.*?)", tbl, re.S | re.I) if not hm: continue heads = [txt(c) for c in re.findall(r"]*>(.*?)", hm.group(1), re.S | re.I)] joined = " ".join(heads).lower() if not any(k in joined for k in ("per 1000", "per 1,000", "rate per", "price per", "rate / 1000", "/ 1000")): continue body = tbl[hm.end():] bm = re.search(r"]*>(.*)", body, re.S | re.I) if bm: body = bm.group(1) idx = {} for i, hd in enumerate(heads): l = hd.lower() if l.startswith("id") or l == "#": idx["id"] = i elif "service" in l: idx["name"] = i elif any(k in l for k in ("per 1000", "per 1,000", "rate per", "price per", "rate / 1000", "/ 1000")): idx["rate"] = i elif "min" in l and "max" in l: idx["minmax"] = i elif "min" in l: idx["min"] = i elif "max" in l: idx["max"] = i elif "average" in l or "delivery" in l or "time" in l: idx["avg"] = i elif "descr" in l: idx["desc"] = i elif "quality" in l: idx["quality"] = i elif "platform" in l: idx["platform"] = i cat = "" for rm in re.finditer(r"]*)>(.*?)", body, re.S | re.I): inner = rm.group(2) cells = re.findall(r"]*)>(.*?)", inner, re.S | re.I) if not cells: continue if len(cells) == 1 and ("colspan" in cells[0][0].lower()): c = txt(cells[0][1]) if c: cat = c continue vals = [txt(c[1]) for c in cells] if len(vals) < 3: continue def g(k): i = idx.get(k) return vals[i] if i is not None and i < len(vals) else None name, rate = g("name"), g("rate") if not name or rate is None or num(rate) is None: continue mn, mx = g("min"), g("max") if mn is None and g("minmax"): parts = re.split(r"[-\u2013]", g("minmax")) if len(parts) == 2: mn, mx = parts[0], parts[1] out.append(dict(panel=panel, sid=(g("id") or "").strip(), category=cat, name=name, rate_raw=rate, rate=num(rate), currency=cur(rate), min=mn or "", max=mx or "", avg=g("avg") or "", desc="", quality=g("quality") or "")) return out, heads if out else [] # ---------------- 1xpanel: div cards + inline modal ------------ def parse_1xpanel(h, panel="1xpanel"): catmap = dict(re.findall(r'', h)) out = [] blocks = re.split(r'(?=
.*?(.*?)', b, re.S) mx = re.search(r'.*?(.*?)', b, re.S) av = re.search(r']*>.*?(.*?)', b, re.S) d = re.search(r'id="serviceModalText"[^>]*>(.*?)
', b, re.S) out.append(dict(panel=panel, sid=i.group(1) if i else "", category=catmap.get(c.group(1), "") if c else "", name=html.unescape(n.group(1)).strip(), rate_raw=p.group(1), rate=num(p.group(1)), currency=cur(p.group(1)), min=txt(mn.group(1)) if mn else "", max=txt(mx.group(1)) if mx else "", avg=txt(av.group(1)) if av else "", desc=txt(d.group(1)) if d else "", quality="")) return out, ["div-card"] # ---------------- morethanpanel: div rows + modal -------------- def parse_mtp(h, panel="morethanpanel"): out = [] blocks = re.split(r'(?=
]*>(.*?)', b, re.S) p = re.search(r'data-title="Rate per 1000"[^>]*>\s*]*>(.*?)', b, re.S) mn = re.search(r'(.*?)', b, re.S) mx = re.search(r'(.*?)', b, re.S) if not (n and p): continue # description = modal body text d = re.search(r'class="modal-body"[^>]*>(.*?)
\s*', b, re.S) out.append(dict(panel=panel, sid=i.group(1) if i else "", category="", name=txt(n.group(1)), rate_raw=txt(p.group(1)), rate=num(txt(p.group(1))), currency=cur(txt(p.group(1))), min=txt(mn.group(1)) if mn else "", max=txt(mx.group(1)) if mx else "", avg="", desc=txt(d.group(1))[:1200] if d else "", quality="")) return out, ["div-row"] PARSERS = {"1xpanel": parse_1xpanel, "morethanpanel": parse_mtp} PANELS = ["smmlaunch","adderpanel","crescitaly","smmcost","smmkings","growfastsmm", "1xpanel","morethanpanel","mysmm","smmfolgen","easytopromo","smmpanelone", "globalsmm","autosmo","peakerr"] if __name__ == "__main__": allrows = [] for p in PANELS: h = load(p) fn = PARSERS.get(p, parse_table) rows, heads = (fn(h, p) if p in PARSERS else fn(h, p)) curs = {} for r in rows: curs[r["currency"]] = curs.get(r["currency"], 0) + 1 print(f"{p:15s} rows={len(rows):6d} headers={heads} currencies={curs}") if rows: print(" e.g.", rows[len(rows)//2]["name"][:80], "|", rows[len(rows)//2]["rate_raw"], "|", rows[len(rows)//2]["min"], "-", rows[len(rows)//2]["max"], "| avg:", rows[len(rows)//2]["avg"][:30]) allrows += rows with gzip.open(OUT, "wt", encoding="utf-8") as fh: json.dump(allrows, fh, ensure_ascii=False) print("TOTAL", len(allrows), "->", os.path.basename(OUT))