← back to Rentv Sheet Enrich Refine
phone_scrape.py
71 lines
#!/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()