← back to Rentv Sheet Enrich Refine
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
M enrich.pyA phone_scrape.py
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 Refine
·
master: split Marketing/VP into Name/Position/LinkedIn/Phone 27fa73a →