vehiq: ratsit-telefonkonnektor — företagsnummer via residensiell stealth (57% träff, kraschtålig, resume)
This commit is contained in:
@@ -30,3 +30,16 @@ VehIQ-marknadsdata från vehiq-db (server10, 98M-fordonstabellen):
|
||||
- `market_value_se.jsonl` — 10 511 märke×modell×år-prisposter (median/p10-p90/annonser) → `vehiq_market_context.json`
|
||||
- `fleet_composition_se.jsonl` — bestånd per märke (Volvo 1,62M) och topp 1 500 modeller
|
||||
- Fordonsägare per bolag kräver fortfarande Transportstyrelsen-prenumeration (ägaruppgifter ingår inte i registerkällan)
|
||||
|
||||
## Telefonnummer via ratsit.se (Simon beslut 2026-09-07)
|
||||
|
||||
`ratsit_connector.py` — kör LOKALT från ASUS (residensiellt IP krävs; Hetzner-IP är
|
||||
CF-bannad på mrkoll, ratsit 403 från datacenter). Headless Chromium, endast
|
||||
företagssidor ratsit.se/<orgnr>, 2 workers á 4s väntan + 2.5-5s jitter.
|
||||
|
||||
- Extraktion: `Telefon</h2>`/`Telefon:</td>`-blocket, primärt `href="tel:..."`-länk
|
||||
- Kraschtålig: browser-omstart per worker vid EPIPE/fel, resume via done.txt
|
||||
- Mål: vehiq_bilbransch prioriterat (bilhandlare → verkstad → leasing → reservdelar → transportör, störst omsättning först)
|
||||
- Utdata: telefon_ratsit.jsonl {orgnr, telefon, status, kalla, hamtat}
|
||||
- Hittills: ~57% telefonträff, 0 blockeringar; toppsegment ~1 dygn, hela listan ~3 dygn
|
||||
- hitta.se: telefon bakom "Visa nummer"-betallösning → ej använd; mrkoll.se: CF-bannad IP
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Ratsit-telefonkonnektor: företagsnummer för VehIQ/MarketMute-mål.
|
||||
Kör från residensiellt IP (ASUS). Endast företagssidor — inga personuppgifter."""
|
||||
import asyncio, json, re, random, os, time, sys
|
||||
from playwright.async_api import async_playwright
|
||||
from datetime import datetime, timezone
|
||||
|
||||
IN = sys.argv[2] if len(sys.argv) > 2 else "/home/simon/vehiq-telefon/targets.jsonl" # prioritet 1: bilbranschen
|
||||
OUT = "/home/simon/vehiq-telefon/telefon_ratsit.jsonl"
|
||||
DONE = "/home/simon/vehiq-telefon/done.txt"
|
||||
WORKERS = 2
|
||||
MIN_DELAY, MAX_DELAY = 2.5, 5.0
|
||||
|
||||
SEGM_PRIO = {"bilhandlare": 0, "bilverkstad": 1, "uthyrning_leasing": 2, "reservdelar_dack": 3, "entreprenad": 4, "motorcykel": 5, "transportor": 6}
|
||||
|
||||
def load_targets():
|
||||
rows = []
|
||||
for line in open(IN):
|
||||
t = json.loads(line)
|
||||
rows.append(t)
|
||||
# prioritet: segment-vikt sedan omsättning (störst först)
|
||||
rows.sort(key=lambda x: (SEGM_PRIO.get(x.get("segment"), 9), -(x.get("oms") or 0)))
|
||||
return rows
|
||||
|
||||
def norm_phone(s):
|
||||
d = re.sub(r"\D", "", s)
|
||||
if d.startswith("46") and len(d) >= 10: d = "0" + d[2:]
|
||||
return d if len(d) >= 8 else None
|
||||
|
||||
def extract_phone(html):
|
||||
"""Företagets egen telefon: Telefon</h2> eller Telefon:</td>-blocket."""
|
||||
for anchor in ("Telefon</h2>", "Telefon:</td>", ">Telefon<"):
|
||||
i = html.find(anchor)
|
||||
if i > 0:
|
||||
seg = html[i:i+1200]
|
||||
m = re.search(r'href="tel:([0-9+-]{8,15})"', seg)
|
||||
if not m:
|
||||
m = re.search(r"0[1-9]\d{1,2}[\s-]?\d{2}\s?\d{2,3}(?!\d)", re.sub(r"<[^>]+>", " ", seg))
|
||||
if m:
|
||||
p = norm_phone(m.group(1) if m.lastindex else m.group(0))
|
||||
if p: return p
|
||||
return None
|
||||
|
||||
async def worker(p, wid, queue, outf, done, lock):
|
||||
while not queue.empty():
|
||||
b = None
|
||||
try:
|
||||
b = await p.chromium.launch(headless=True, args=["--disable-blink-features=AutomationControlled"])
|
||||
pg = await b.new_page(user_agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36", viewport={"width":1440,"height":900}, locale="sv-SE")
|
||||
while True:
|
||||
try:
|
||||
t = queue.get_nowait()
|
||||
except asyncio.QueueEmpty:
|
||||
return
|
||||
orgnr = t["orgnr"]; digits = orgnr.replace("-", "")
|
||||
if digits in done:
|
||||
queue.task_done(); continue
|
||||
phone = None; status = "ok"
|
||||
try:
|
||||
r = await pg.goto(f"https://www.ratsit.se/{digits}", wait_until="domcontentloaded", timeout=30000)
|
||||
await pg.wait_for_timeout(4000)
|
||||
html = await pg.content()
|
||||
if r.status == 200:
|
||||
phone = extract_phone(html)
|
||||
if not phone: status = "ingen_telefon"
|
||||
elif r.status == 403:
|
||||
status = "blockerad"
|
||||
await asyncio.sleep(180)
|
||||
else:
|
||||
status = f"http_{r.status}"
|
||||
except Exception as e:
|
||||
status = f"fel_{str(e)[:40]}"
|
||||
raise # trigga omstart av browser
|
||||
async with lock:
|
||||
rec = {"orgnr": orgnr, "segment": t.get("segment"), "namn": t.get("namn"),
|
||||
"postort": t.get("postort"), "telefon": phone, "status": status,
|
||||
"kalla": f"ratsit.se/{digits}",
|
||||
"hamtat": datetime.now(timezone.utc).isoformat()}
|
||||
outf.write(json.dumps(rec, ensure_ascii=False) + "\n"); outf.flush()
|
||||
done.add(digits)
|
||||
with open(DONE, "a") as df: df.write(digits + "\n")
|
||||
queue.task_done()
|
||||
await asyncio.sleep(MIN_DELAY + random.random() * (MAX_DELAY - MIN_DELAY))
|
||||
except Exception as e:
|
||||
print(f"[worker {wid}] omstart efter fel: {str(e)[:60]}", flush=True)
|
||||
await asyncio.sleep(5)
|
||||
finally:
|
||||
if b:
|
||||
try: await b.close()
|
||||
except: pass
|
||||
|
||||
async def main():
|
||||
limit = int(sys.argv[1]) if len(sys.argv) > 1 else 999999
|
||||
targets = load_targets()[:limit]
|
||||
done = set(open(DONE).read().split()) if os.path.exists(DONE) else set()
|
||||
queue = asyncio.Queue()
|
||||
for t in targets:
|
||||
if t["orgnr"].replace("-", "") not in done:
|
||||
queue.put_nowait(t)
|
||||
print(f"[INFO] {queue.qsize():,} att hämta ({len(done):,} klara), {WORKERS} workers", flush=True)
|
||||
lock = asyncio.Lock()
|
||||
outf = open(OUT, "a")
|
||||
async with async_playwright() as p:
|
||||
await asyncio.gather(*[worker(p, i, queue, outf, done, lock) for i in range(WORKERS)])
|
||||
outf.close()
|
||||
print("[KLAR]", flush=True)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user