[object Object]

← back to Rentv Sheet Enrich

master enrichment: registered Unique Contacts tab, +websites(3230)+ported 56 cos; phone_scrape.py ($0 website phone harvester)

94e653b3ae71eb886109ea36a2b21e763e3a6b4e · 2026-08-13 10:57:51 -0700 · Steve Abrams

Files touched

Diff

commit 94e653b3ae71eb886109ea36a2b21e763e3a6b4e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Aug 13 10:57:51 2026 -0700

    master enrichment: registered Unique Contacts tab, +websites(3230)+ported 56 cos; phone_scrape.py ($0 website phone harvester)
---
 enrich.py       |  2 ++
 phone_scrape.py | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 72 insertions(+)

diff --git a/enrich.py b/enrich.py
index 1468b19..216c288 100644
--- a/enrich.py
+++ b/enrich.py
@@ -37,6 +37,8 @@ TABS = {
         header_row=1, data_start=2, company=3, email=7, base=11),   # L-O (skips K status col)
     1929460437: dict(name="Event Sponsors",
         header_row=1, data_start=2, company=2, email=6, base=9),    # J-M (after I status col)
+    3823360: dict(name="Unique Contacts (all tabs)",                 # the deduped MASTER
+        header_row=0, data_start=1, company=2, email=1, base=15),   # cols P.. (append after O)
 }
 
 NEW_LABELS = ["Website", "LinkedIn (Company)", "LinkedIn (Contact)",
diff --git a/phone_scrape.py b/phone_scrape.py
new file mode 100644
index 0000000..1c1366e
--- /dev/null
+++ b/phone_scrape.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python3
+"""
+phone_scrape.py — $0 company-phone harvester.
+
+Reads the master tab's Website column, fetches each unique company domain (homepage,
+then /contact), extracts the main phone number (tel: links preferred, else the most
+repeated US phone pattern — footer numbers repeat), and saves data/phones.json.
+Concurrent (threads). No API cost. A separate step writes results to the sheet.
+"""
+import lib, enrich, json, re, os, urllib.request, ssl
+from concurrent.futures import ThreadPoolExecutor
+
+GID = 3823360
+ctx = ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE
+UA = {"User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/537.36"}
+TEL = re.compile(r'tel:\+?1?[-.\s]?\(?(\d{3})\)?[-.\s]?(\d{3})[-.\s]?(\d{4})')
+PHONE = re.compile(r'\(?\b(\d{3})\)?[-.\s]?(\d{3})[-.\s]?(\d{4})\b')
+BAD = {"0000000000","1111111111","1234567890","8888888888"}
+
+def fetch(url):
+    try:
+        req=urllib.request.Request(url, headers=UA)
+        return urllib.request.urlopen(req, timeout=8, context=ctx).read(200000).decode("utf-8","ignore")
+    except Exception:
+        return ""
+
+def phone_of(domain):
+    for path in ("", "/contact", "/contact-us", "/about"):
+        for scheme in ("https://","http://"):
+            html=fetch(scheme+domain+path)
+            if not html: continue
+            m=TEL.search(html)
+            if m:
+                p="".join(m.groups())
+                if p not in BAD and p[0] not in "01": return "-".join(m.groups())
+            cands=["".join(g) for g in PHONE.findall(html)]
+            cands=[c for c in cands if c not in BAD and c[0] not in "01" and c[3] not in "01"]
+            if cands:
+                best=max(set(cands), key=cands.count)  # most-repeated = footer/main line
+                return f"{best[:3]}-{best[3:6]}-{best[6:]}"
+        # only try /contact paths on the primary scheme round; break after homepage attempts
+    return ""
+
+def main():
+    tok=lib.access_token()
+    title={s["properties"]["sheetId"]:s["properties"]["title"] for s in lib.get_meta(tok)["sheets"]}[GID]
+    rows=lib.read_tab(tok,title); hdr=rows[0]
+    wi=hdr.index("Website")
+    def g(r,i): return (r[i] if i<len(r) else "").strip()
+    domains=sorted({g(rows[r],wi) for r in range(1,len(rows)) if g(rows[r],wi)})
+    print(f"{len(domains)} unique domains to scrape", flush=True)
+    out={}
+    cache_path=os.path.join(os.path.dirname(__file__),"data","phones.json")
+    if os.path.exists(cache_path): out=json.load(open(cache_path))
+    todo=[d for d in domains if d not in out]
+    print(f"{len(todo)} remaining (cache has {len(out)})", flush=True)
+    done=0
+    with ThreadPoolExecutor(max_workers=24) as ex:
+        for d,ph in zip(todo, ex.map(phone_of, todo)):
+            out[d]=ph; done+=1
+            if done%50==0:
+                json.dump(out,open(cache_path,"w"))
+                found=sum(1 for v in out.values() if v)
+                print(f"  {done}/{len(todo)} scraped | {found} phones so far", flush=True)
+    json.dump(out,open(cache_path,"w"))
+    found=sum(1 for v in out.values() if v)
+    print(f"DONE: {len(out)} domains, {found} phones found -> data/phones.json", flush=True)
+
+if __name__=="__main__":
+    main()

← 5357336 auto-data-snapshot: 2026-08-13T10:54:53 (51 data files) — AG  ·  back to Rentv Sheet Enrich  ·  master: split Marketing/VP into Name/Position/LinkedIn/Phone 27fa73a →