Wie man ZoomInfo im großen Maßstab scraped

Erfahren Sie, wie Sie ZoomInfo-Firmenseiten scrapen, CRM-Domains mit Profilen abgleichen und Datensätze mit Python und Bright Datas ZoomInfo Scraper API validieren.
3 min lesen
How to Scrape - ZoomInfo

ZoomInfo zu scrapen, eine B2B-Datenplattform, bedeutet, die Firmenprofile zu sammeln, die ohne Login angezeigt werden. Vertriebs- und Datenteams nutzen ZoomInfo-Unternehmensdaten, um CRM-Konten mit Branche, Mitarbeiterzahl, Umsatz und Technologiefeldern anzureichern.

Ohne Referrer von einer anderen Seite erhielten alle 32 ersten Anfragen, die wir von neuen Residential-IPs sendeten, ZoomInfos “Press & Hold”-Challenge-Seite. In unseren Tests wurden IPs, die eine erste Seite erhielten, nach einem Median von 3 Seiten blockiert.

Dieser Leitfaden zeigt Ihnen, wie Sie Firmenseiten im großen Maßstab abrufen, Konten mit Profilen abgleichen, Daten extrahieren und entscheiden, wann Sie einen ZoomInfo Scraper kaufen sollten.

Kurzfassung

  • Wir maßen etwa 3 Seiten pro IP, daher würde das Sammeln von 100.000 ZoomInfo-Seiten mehr als 33.000 Sitzungen erfordern. Bei der 5. Anfrage erhielten 12 von 14 Sitzungen einen Cloudflare-429-Fehler.
  • ZoomInfo enthielt doppelte Firmenprofile. Eine Suche nach einer Domain ergab 3 Firmen-IDs, und ein Duplikat zeigte eine andere Mitarbeiterzahl und einen anderen Umsatz an.
  • Das eingebettete JSON von ZoomInfo enthielt ungerundete Zahlen. Web Unlocker lieferte Aviagens Seite mit 6.776 Mitarbeitern im JSON, während das Profil 5K bis 10K anzeigte.
  • Die ZoomInfo Scraper API von Bright Data übernimmt die Entsperrung für Sie. Aus 1 Verzeichnis-URL lieferte sie 45 strukturierte Firmendatensätze.

Was ZoomInfo ohne Login anzeigt

ZoomInfos öffentliche Seiten lieferten detaillierte Firmendaten und grundlegende Personendaten. E-Mails, direkte Telefonnummern, vollständige Kontaktlisten und die vollständige Liste der Technologien erforderten ein Konto. Unser ausgeloggter Scraper sammelte firmographische Daten wie Branche und Mitarbeiterzahl. Die öffentliche Seite hatte diese Haupttypen von Seiten:

Seitentyp URL-Muster Was sie anzeigen kann
Firmenprofil /c/{slug}/{id} Branche, Mitarbeiterzahl, Umsatzschätzung, Hauptsitz, Telefon, Website, SIC- und NAICS-Codes, einige Technologien, Finanzierung und Mitarbeiterzahlen nach Managementebene
Firmenverzeichnis /companies-search/{filters} 9 Unternehmen pro Seite, gefiltert nach Standort und Branche
Personenprofil /p/{name}/{id} Ein Name, eine Berufsbezeichnung und ein Arbeitgeber
Andere Seiten /pic/, /hq/, /tech/, /executives/, /competitors/ Mitarbeiterverzeichnisse, Hauptsitzdetails, Unternehmen, die eine Technologie nutzen, Führungskräfte und Wettbewerber

Personenseiten, Mitarbeiterverzeichnisse und die Listen von Personen auf Firmenseiten enthalten personenbezogene Daten, die unter die GDPR-Konformität fallen können. Eingeloggte Seiten unterliegen den Nutzungsbedingungen von ZoomInfo, denen Kontoinhaber zustimmen. Unsere Skripte sammeln nur öffentliche Firmendatensätze und lassen Personenfelder weg.

Warum Ihre erste Anfrage an ZoomInfo blockiert wird

ZoomInfo betrieb 2 Anti-Bot-Systeme vor seinen Seiten, und jede blockierte Antwort zeigte, welches System sie gesendet hatte. Ein Proxy zwischen Ihrem Scraper und ZoomInfo kann auch den TLS-Fingerabdruck ändern, den diese Systeme sehen. In unseren Tests benötigte die erste Anfrage einer Sitzung einen externen Referrer, aber spätere Anfragen, die die Cookies der Sitzung trugen, benötigten keinen.

Jedes Anti-Bot-System hat seine eigene Signatur

Anfragen an zoominfo.com-Seiten, einschließlich Verzeichnisseiten, wurden über Cloudflare geleitet und von HUMAN Security überprüft, das 2022 mit PerimeterX fusionierte. Beide Systeme markierten ihre blockierten Antworten mit eindeutigen Signaturen, und eine echte Firmenseite hatte ebenfalls eine Signatur:

Antwort von Status und Seite Signal zur Unterscheidung
HUMAN Security 403, “Access to this page has been denied” mit einer “Press & Hold”-Schaltfläche Der 403-Status und px-captcha im Body
Cloudflare 429, “Just a moment…” Der cf-mitigated: challenge Response-Header
ZoomInfo-Ursprung 200, die Firmenseite <script id="ng-state"> mit Firmendaten

Unterscheiden Sie anhand dieser Signale, nicht anhand von Seitentiteln. Firmenseiten enthielten auch _pxAppId, daher bedeutet dessen Vorhandensein nicht, dass HUMAN die Anfrage blockiert hat. Die Cloudflare-Dokumentation besagt, dass jeder Typ von Challenge-Seite den cf-mitigated: challenge Header trägt.

Das eingebettete JSON im ng-state-Skript markierte echte Firmenseiten am zuverlässigsten. Behandeln Sie eine Antwort nur dann als erfolgreich, wenn sie dieses JSON trägt.

Was Ihr Proxy mit Ihrem TLS-Fingerabdruck macht

Wir verwendeten eine Bright Data Residential-Proxy-Zone, eine benannte Proxy-Konfiguration. Die Zone öffnete eine eigene TLS-Verbindung zur Seite. Wir sendeten Anfragen von 3 Clients an tls.peet.ws, einen TLS-Fingerabdruck-Checker, zunächst ohne Proxy und dann über diese Zone:

Client JA4 ohne Proxy JA4 über den Proxy
curl_cffi als Chrome 150 t13d1516h2_8daaf6152771_806a8c22fdea t13d1516h2_8daaf6152771_d8a2da3f94cd
curl_cffi als Firefox 147 t13d1717h2_5b57614c22b0_3cbfd9057e0d t13d1516h2_8daaf6152771_02713d6af862
System curl 8.7.1 (LibreSSL) t13d497h2_0d8feac7bc37_7395dae3b2f3 t13d1516h2_8daaf6152771_02713d6af862

Über den Proxy begannen alle 3 JA4-Fingerabdrücke mit t13d1516h2_8daaf6152771, denselben ersten 2 Teilen wie curl_cffi, das Chrome imitiert. Diese Teile kodieren TLS 1.3, 15 Cipher-Suiten, 16 Erweiterungen und HTTP/2. Der Proxy präsentierte also für alle 3 Clients einen Chrome-ähnlichen TLS-Fingerabdruck. Die Chrome-Imitation behielt über den Proxy auch ihren HTTP/2-Fingerabdruck bei, nutzen Sie diese also, um sowohl den TLS- als auch den HTTP/2-Fingerabdruck wie den von Chrome aussehen zu lassen.

Prüfen Sie, welcher Hop TLS terminiert, bevor Sie Zeit in TLS-Imitation investieren. Wenn der Proxy Ihre Verbindung nur tunnelt, sieht die Seite den eigenen Handshake Ihres Clients. In beiden Fällen beginnt die TCP-Verbindung am Exit-Node, dessen SYN-Paket seine eigene TTL, Fenstergröße und Optionsreihenfolge trägt. Ein macOS-User-Agent kann über einen Windows-TCP-Stack ankommen, und ein Anti-Bot-System, das TCP-Fingerabdrücke liest, kann diese Diskrepanz melden.

Wie ZoomInfo die erste Anfrage bewertet

HUMANs Challenge-Seite führte ein Sensor-Skript im Browser aus, und das Skript setzte Risiko-Cookies wie _px3. Die erste Anfrage eines Scrapers trägt keines dieser Cookies, daher kann die Seite nur die Anfrage und die Verbindung, über die sie ankommt, bewerten.

Wir sendeten 32 cookielose Firmenseiten-Anfragen ohne Referrer von neuen US-Residential-IPs, wobei jeder der 4 Clients 8 sendete. Die Clients waren System-curl, curl mit einem vollständigen Satz von Chrome-Headern, curl_cffi als Chrome-Imitation und httpx über HTTP/2. Alle 32 erhielten HUMANs 403-Fehler und diese “Press & Hold”-Seite:

ZoomInfos HUMAN Security Challenge-Seite mit dem ZoomInfo-Logo, dem Text "Press & Hold to confirm you are a human (and not a bot)," und einer leeren Halteschaltfläche.

ZoomInfo gab diese Challenge-Seite als Antwort auf eine cookielose Anfrage ohne Referrer zurück.

Über die Proxy-Zone sah Cloudflare den TLS-Handshake des Proxys für alle 4 Clients. Von einer Heimverbindung aus sah Cloudflare den eigenen Handshake jedes Clients. Die ersten Anfragen von allen 6 Clients, die wir bei dieser Verbindung testeten, erhielten denselben 403-Fehler. Bei unseren ersten Anfragen ohne Referrer veränderte der TLS-Handshake also nicht das Ergebnis.

Ein externer Referrer veränderte das Ergebnis. Cookielose Anfragen von curl_cffi mit Chrome-Imitation waren in 16 von 18 Fällen erfolgreich, wenn ihr Referer-Header eine andere Seite benannte. Die Referrer waren Google, Bing, DuckDuckGo, LinkedIn und example.com. Von den 2 fehlgeschlagenen Anfragen erhielt 1 einen HTTP-502-Fehler.

Cookielose Anfragen mit einem sec-fetch-site: cross-site-Header, aber ohne Referer-Header, wurden in 3 von 3 Fällen blockiert. Cookielose Anfragen mit einem Same-Origin-Referrer wurden ebenfalls in 3 von 3 Fällen blockiert. In unseren Tests musste eine erste Anfrage wie ein Klick von einer anderen Seite aussehen, zum Beispiel aus einem Suchergebnis.

Mit einem Google-Referrer änderte sich die Erfolgsrate auch mit der Parallelität und dem User-Agent:

  • Bei 6 gleichzeitigen Anfragen über rotierende IPs waren 14 von 45 curl_cffi-Anfragen erfolgreich.
  • Bei 1 Anfrage gleichzeitig mit einer 6-Sekunden-Pause nach jeder waren 13 von 20 Anfragen erfolgreich, und 3 erhielten eine Weiterleitung.
  • Anfragen mit einem curl– oder python-requests-User-Agent wurden in 29 von 29 Fällen blockiert.
  • Bei 6 gleichzeitigen Anfragen waren 4 von 15 System-curl-Anfragen mit einem Chrome-User-Agent erfolgreich.

Selbst bei 1 Anfrage gleichzeitig schlugen 7 von 20 Anfragen fehl, daher benötigt ein selbst erstellter Scraper dennoch Wiederholungsversuche, neue Sitzungen und Überwachung.

Wie viele ZoomInfo-Seiten jede IP erhält

Sobald eine Sitzung ihre erste Seite erhielt, lieferte ZoomInfo nur noch wenige weitere Seiten an dieselbe IP. Jede Sitzung behielt 1 Residential-IP durch die Sticky-Session-Einstellung des Proxys bei und sendete bei der ersten Anfrage einen Google-Referrer. Dann zählten wir Firmenseiten bis zur ersten blockierten Anfrage.

Über 14 Sitzungen, die eine erste Seite erhielten, lud die mittlere Sitzung 3 Firmenseiten, bevor ihre erste blockierte Anfrage auftrat. Bei der 5. Anfrage hatten 12 der 14 einen Cloudflare-429-Fehler erhalten. Nach dem ersten 429-Fehler erhielt auch jede spätere Anfrage in dieser Sitzung einen 429-Fehler. Keiner der untersuchten 429-Fehler trug einen Retry-After-Header.

In den untenstehenden Sitzungen veränderte eine Verzögerung von 2 oder 15 Sekunden zwischen den Seiten die Zählung nicht:

Client Verzögerung zwischen Seiten Ergebnis pro Anfrage
HTTP-Client 15 Sekunden OK, OK, dann 429 bei den nächsten 8
HTTP-Client 15 Sekunden OK, OK, OK, dann 429 bei den nächsten 7
HTTP-Client 2 Sekunden OK, OK, OK, dann 429 bei den nächsten 7
Echtes Chrome Seitenladezeit OK, OK, OK, dann “Just a moment…” bei den nächsten 5

In der Chrome-Sitzung lief ein sichtbares Chrome mit verstecktem navigator.webdriver, über eine separate Sticky-Session. Es lud die ersten 3 Seiten jeweils in 6 bis 9 Sekunden. Bei jeder der nächsten 5 Seiten zeigte Cloudflares Challenge nach etwa 15 Sekunden immer noch an, obwohl der Browser ein Clearance-Cookie, cf_clearance, besaß.

In Sitzungen, die die Cookies der ersten Antwort beibehielten, benötigten spätere Anfragen keinen externen Referrer. Eine spätere Anfrage konnte die vorherige ZoomInfo-Seite als ihren Referrer benennen, wie es ein Browser tut.

Das Löschen der Cookies setzte die Zählung nicht zurück. Von 4 Sitzungen, die sie vor jeder Anfrage löschten und jedes Mal einen Google-Referrer sendeten, erhielten 3 nach 3 Seiten einen 429-Fehler. Das Limit war also an die IP gebunden, nicht an die Cookies.

Foren-Threads behaupten, Anti-Bot-Cookies seien an die Exit-IP gebunden, aber wir konnten dies mit den Cookies, die unser HTTP-Client erhielt, nicht reproduzieren. Wir verschoben ein Cookie-Jar mit _pxhd, _cfuvid und contextual_signup_test von einer IP auf eine neue, und 4 von 5 Anfragen waren erfolgreich. Cookielose Anfragen von anderen neuen IPs zur gleichen Zeit waren in 2 von 5 Fällen erfolgreich. Beide Gruppen sendeten einen Google-Referrer.

Dieses Diagramm zeigt den Pfad von einem Scraper zu einer Firmenseite und die Antwort jedes Anti-Bot-Systems:

Flussdiagramm des Pfades von einem Scraper zu einer ZoomInfo-Firmenseite. Anfragen laufen über einen optionalen Proxy-Exit-Node zu 2 Anti-Bot-Systemen. Cloudflare lieferte nach etwa 3 Seiten pro IP einen 429-Fehler, und HUMAN Security lieferte einen 403-Fehler, wenn die erste Anfrage keinen externen Referrer hatte. Der Ursprung von ZoomInfo lieferte einen 200-Status mit den Firmendaten im eingebetteten JSON.

Eine blockierte Anfrage liefert keine Firmendaten, daher bedeutet eine höhere Blockierungsrate mehr Anfragen pro Datensatz.

Bei etwa 3 Seiten pro IP würde das Sammeln von 100.000 Firmenseiten pro Monat mehr als 33.000 Sitzungen erfordern, die eine erste Seite erhalten. Jede blockierte erste Anfrage bedeutet eine weitere zu startende Sitzung. Bright Datas Web Unlocker übernimmt diese Sitzungen für Sie.

Firmendaten aus einer ZoomInfo-Seite extrahieren

ZoomInfos Firmenseiten waren serverseitig gerenderte Angular-Anwendungen. Der Server schrieb die Daten der Seite in <script id="ng-state" type="application/json">. Dieses Skript stammt von Angulars TransferState-API, die es dem Browser erlaubt, die Daten wiederzuverwenden, ohne sie erneut abzurufen. Für Walmart enthielt das Skript 84 KB JSON, und sein pageData-Objekt hatte 42 Schlüssel.

Das eingebettete JSON enthielt präzisere Zahlen als die sichtbare Seite. Aviagens Profil zeigte seine Mitarbeiterzahl als Bereich und seinen Umsatz auf 1 Nachkommastelle gerundet an:

ZoomInfo-Firmenprofil für Aviagen. Der Header zeigt Agriculture, Alabama, United States und 5K - 10K Employees, und die Übersicht zeigt Hauptsitz, Telefonnummer, Website, Umsatz von 2,4 Milliarden Dollar, Branchen-Tags und einen ZI Rank von 65.

Aviagens sichtbares Profil zeigte “5K – 10K” Mitarbeiter und “2,4 Milliarden Dollar” Umsatz.

Das JSON derselben Seite enthielt die ungerundeten Zahlen:

<script id="ng-state" type="application/json">
{"pageData": {
  "companyId": 9047889,
  "name": "Aviagen",
  "numberOfEmployees": "6776",
  "about": {"revenue": "2413885", ...},
  ...
}}
</script>

ZoomInfo speicherte den Umsatz in Tausenden von US-Dollar, daher bedeutet Aviagens 2413885 2.413.885.000 Dollar. Vergleichen Sie das eingebettete JSON eines Unternehmens mit seinem sichtbaren Profil, um den Multiplikator zu bestätigen.

Andere Firmenseiten zeigten ebenfalls Bereiche, während ihr eingebettetes JSON ungerundete Zahlen enthielt. Der FAQ-Block auf der Walmart-Seite nannte “10K+” Mitarbeiter, während das JSON 2.100.000 enthielt. Bei 5 kleinen Unternehmen, die als “<5 Millionen Dollar" Umsatz angezeigt wurden, reichten die JSON-Schätzungen von 366.000 bis 4.474.000 Dollar.

Einige Felder änderten sich zwischen den Abrufen, selbst wenn die Firmendaten gleich blieben. Bei 2 Abrufen der Walmart-Seite, Minuten voneinander entfernt, unterschieden sich genau 3 pageData-Schlüssel:

  • pic enthielt eine teilweise andere Gruppe von 9 Mitarbeitern.
  • copilotContent enthielt dieselben 10 Marketing-Vorlagen in neuer Reihenfolge.
  • intents enthielt 5 Einträge, von denen 2 einen randomLocation-Wert trugen, und beide Werte änderten sich.

Mit diesen Feldern in der Seite kann das Hashing ganzer Seiten unveränderte Unternehmen bei jedem Crawl als verändert erscheinen lassen. Dieser Parser, gespeichert als parse_zoominfo.py, liest das eingebettete JSON, konvertiert die Einheiten, behält IDs als Strings und hasht nur Firmenfelder:

import hashlib
import json
import re
from urllib.parse import urljoin

STATE = re.compile(r'<script[^>]*id="ng-state"[^>]*>(.*?)</script>', re.S)
LD_JSON = re.compile(
    r'<script[^>]*type="application/ld\+json"[^>]*>(.*?)</script>', re.S
)


def _state(html: str) -> dict | None:
    match = STATE.search(html)
    return json.loads(match.group(1)) if match else None


def _int(value) -> int | None:
    return int(value) if value and str(value).isdigit() else None


def _zi_rank(html: str) -> int | None:
    for block in LD_JSON.findall(html):
        data = json.loads(block)
        if isinstance(data, dict) and data.get("@type") == "Organization":
            for prop in data.get("additionalProperty", []):
                if prop.get("name") == "ZI Rank":
                    return prop.get("value")
    return None


def parse_company(html: str) -> dict | None:
    state = _state(html)
    page = (state or {}).get("pageData") or {}
    if not page.get("companyId"):
        return None  # eine Challenge-Seite, eine leere Hülle oder eine andere Vorlage
    about = page.get("about") or {}
    overview = page.get("overview") or {}
    revenue_k = _int(about.get("revenue"))  # in Tausend USD gespeichert
    domain = (page.get("url") or "").lower().removeprefix("www.")
    return {
        "id": str(page["companyId"]),  # Strings: Personen-IDs überlaufen int32
        "name": page.get("name"),
        "domain": domain or None,
        "employees": _int(page.get("numberOfEmployees")),  # ungerundete Zahl
        "revenue_usd": revenue_k * 1000 if revenue_k is not None else None,
        "founded": _int(page.get("foundingYear")),
        "ticker": overview.get("ticker"),
        "sic": overview.get("sic") or [],
        "naics": overview.get("naics") or [],
        "industries": [i["name"] for i in about.get("industries") or []],
        "hq": page.get("address"),
        "phone": about.get("phone"),
        "tech_used": sorted(t["name"] for t in page.get("techUsed") or []),
        "management_levels": {
            m["title"]: m["value"] for m in page.get("mgtl") or []
        },
        "zi_rank": _zi_rank(html),
        # Personenfelder (executives, pic, hierarchyOrgChart) werden ausgeschlossen.
    }


# intents, pic und copilotContent änderten sich zwischen 2 Abrufen der
# gleichen Seite, daher schließt der Hash sie aus.
STABLE = ("name", "domain", "employees", "revenue_usd", "industries",
          "hq", "phone", "sic", "naics", "ticker", "tech_used")


def content_hash(record: dict) -> str:
    stable = json.dumps({k: record.get(k) for k in STABLE}, sort_keys=True)
    return hashlib.sha256(stable.encode()).hexdigest()[:16]

parse_company gibt None für jede Seite ohne Firmendaten zurück, etwa eine Challenge-Seite oder eine leere Seite. Bei den 2 Walmart-Abrufen lieferte content_hash denselben Wert, b4ffe1810bbb1c8c, während sich der SHA-256-Hash ihres Roh-HTML unterschied.

ZoomInfo-Firmen-URLs im großen Maßstab finden

ZoomInfos robots.txt listete 1 Sitemap auf, die nur Marketing-Seiten enthielt. Wir fanden keine Sitemap für Firmen- oder Personenseiten, daher können Sie die URL-Liste aus 3 anderen Quellen aufbauen.

Verzeichnisseiten

Jede /companies-search/-URL listete die Unternehmen auf, die ihren Standort- und Branchenfiltern entsprachen, und ein pageNum-Abfrageparameter wählte die Seite aus. Die Alabama-Landwirtschaftsliste zeigte insgesamt 1.962 Unternehmen. Die Seiten 1 bis 5 lieferten jeweils die angeforderte Seite zurück. Anfragen für die Seiten 6 und 10 lieferten HTTP 200 mit den Unternehmen von Seite 1 und pageNum: 1 im eingebetteten JSON zurück.

Hier ist die erste Seite dieser Liste:

ZoomInfo-Verzeichnisergebnisse für Landwirtschaftsunternehmen in Alabama. Eine Tabelle listet 9 Unternehmen auf, darunter Peco Foods und Aviagen, mit Spalten für Branche, Standort, Umsatz und Mitarbeiter, und die Seitennummerierung zeigt Seiten 1 bis 5.

Die Seitennummerierung zeigte 5 Seiten. Die Umsatzspalte war in Tausenden von Dollar angegeben, daher entsprechen Aviagens 2,4M den 2,4 Milliarden Dollar in seinem Profil.

Ein Paginator, der nur Statuscodes prüft, würde die Unternehmen von Seite 1 erneut als neue Ergebnisse sammeln. Fügen Sie diese Absicherung zu parse_zoominfo.py hinzu. Sie vergleicht die Seitennummer im eingebetteten JSON mit der angeforderten:

def directory_page(html: str, requested: int) -> list[str] | None:
    state = _state(html) or {}
    if state.get("pageNum") != requested:
        return None  # jenseits der erreichbaren Seiten liefert ZoomInfo erneut Seite 1
    rows = (state.get("aiSearchResults") or {}).get("data") or []
    return [urljoin("https://www.zoominfo.com", row["companyUrl"])
            for row in rows]

directory_page lieferte 9 Firmen-URLs pro Seite und gab für Seite 6 None zurück. Nur 5 Seiten dieses Filters waren also erreichbar, und sie enthielten etwa 45 der 1.962 Unternehmen. Um mehr Unternehmen zu erreichen, teilen Sie einen großen Filter nach US-Bundesstaat oder Teilbranche auf, bis jeder kleinere Filter 45 oder weniger Unternehmen enthält. Die ZoomInfo Scraper API von Bright Data kann auch ZoomInfo-Verzeichnisseiten crawlen und die darauf gelisteten Unternehmen als strukturierte Datensätze zurückgeben.

Suchergebnisse für bereits bekannte Unternehmen

Eine Abfrage wie site:zoominfo.com/c example.com, gesendet über Bright Datas SERP-API, kann Kandidatenprofile für eine Domain finden. Für brightdata.com lieferte die Abfrage 3 verschiedene Firmen-IDs. Behalten Sie den Kandidaten, dessen Website mit der gesuchten Domain übereinstimmt.

Viele Profile listeten ähnliche Unternehmen auf, und die ZoomInfo Scraper API von Bright Data lieferte diese in einem similar_companies-Feld zurück. Ein Crawler kann mit einer Liste von Seed-URLs beginnen und diesen Links folgen, um weitere Unternehmen zu finden.

Doppelte Profile und stabile IDs

Duplikate existierten in den eigenen Daten von ZoomInfo, sodass jede der 3 Quellen sie zurückgeben konnte. Für brightdata.com listete ein zusätzliches Profil die Straßenadresse des Hauptprofils, aber eine andere Website-Domain auf. Auch seine Schätzungen für Mitarbeiterzahl und Umsatz unterschieden sich vom Hauptprofil.

Ein Join auf Domain übersieht dieses Duplikat, und ein Join auf Name kann auch nicht verwandte Unternehmen zuordnen. Eine Suche nach minimax.com lieferte 9 Profile mit Minimax im Namen, und mehrere waren nicht verwandte Unternehmen. Gleichen Sie Entitäten anhand der Domain oder der Hauptsitz-Straßenadresse ab und überprüfen Sie nur die Profile, die bei einem, aber nicht beim anderen übereinstimmen. Behalten Sie die numerische ID von ZoomInfo als Primärschlüssel bei.

In einer Firmen-URL konnte sich der Slug ändern, aber die von uns verfolgte Weiterleitung behielt die numerische ID bei. In einer Stichprobe lieferten 3 von 20 Firmen-URLs eine 301-Weiterleitung, sodass jede eine weitere Anfrage über dieselben Anti-Bot-Systeme benötigte. Für die Eingabe /c/walmart-inc/155353090 lieferte die Scraper API die kanonische URL /c/walmart/155353090, die dieselbe ID beibehielt. Web Unlocker folgte derselben Weiterleitung und lieferte Walmarts Seite zurück.

3 Möglichkeiten, ZoomInfo-Daten im großen Maßstab zu sammeln

Bei 100.000 Firmenseiten pro Monat und etwa 3 Seiten pro IP würde eine selbst erstellte Pipeline mehr als 33.000 Sitzungen und laufende technische Arbeit erfordern. Bright Datas ZoomInfo-Datenextraktionsoptionen übernehmen unterschiedliche Mengen dieser Arbeit, von entsperrten Seiten bis zu fertigen Datensätzen:

Web Unlocker ZoomInfo Scraper API ZoomInfo-Datensatz
Sie senden Eine Seiten-URL Firmen- oder Verzeichnis-URLs Einen Filter
Sie erhalten Das vollständige Seiten-HTML Strukturierte JSON-Datensätze Fertige Datensätze in einer Datei
In unseren Tests Firmenseiten kamen mit intaktem eingebettetem JSON an Firmendatensätze kamen an, und Verzeichnis-URLs wurden für uns paginiert Die Beispieldatei zeigte das vollständige Schema
Lieferung Synchrone oder asynchrone Anfragen Snapshots über API, Cloud-Speicher oder Webhook Dateien bei geplanter Aktualisierung
Parsing Ihres Enthalten Enthalten

Die richtige Option hängt von der benötigten Ausgabe ab:

  • Strukturierte Datensätze für Ihre eigene URL-Liste. Nutzen Sie die Scraper API.
  • Ungerundete Mitarbeiterzahl und Umsatz. Nutzen Sie Web Unlocker mit parse_company.
  • Breite Abdeckung, bereits gesammelt. Nutzen Sie den Datensatz.

Für Seiten, die Klicks und Scrollen erfordern, betreibt Bright Datas Browser API ein gehostetes Chrome, das Sie mit Playwright oder Puppeteer steuern. Für Firmenseiten, die keine Klicks benötigen, ist Web Unlocker die einfachere Option, da es das HTML direkt zurückgibt, ohne Browser-Steuerung. Für KI-Agenten ermöglicht Bright Datas ZoomInfo MCP-Server Clients wie Claude Desktop und Cursor, dieselben öffentlichen Firmenprofile zu sammeln.

Um zu beginnen, erstellen Sie ein Bright Data-Konto und einen API-Schlüssel. Für Web Unlocker erstellen Sie auch eine Zone, eine benannte Web Unlocker-Konfiguration, im Kontrollfeld. Neue Konten erhalten kostenlose Credits, um Bright Datas APIs zu testen.

Die Skripte benötigen Python 3.10 oder höher. Die vollständigen Dateien befinden sich in Bright Datas zoominfo-scraper-Repository auf GitHub. Setzen Sie den Schlüssel und die Zone einmal:

pip install httpx
export BRIGHTDATA_API_KEY="your_api_key"
export BRIGHTDATA_UNLOCKER_ZONE="your_web_unlocker_zone"

Jedes Skript liest den Schlüssel beim Start ein, daher stoppt ein Skript mit fehlendem Schlüssel, bevor es irgendeine Anfrage sendet.

Web Unlocker liefert Seiten, die Sie parsen

Web Unlocker ist Bright Datas Entsperrungs-API. Sie senden eine URL, und Web Unlocker übernimmt Proxys, Browser-Rendering und Challenges, bevor es die Seite zurückgibt. In unseren Tests entsperrte es ZoomInfo-Firmenseiten und lieferte das HTML mit intaktem eingebettetem JSON zurück. parse_company funktioniert also mit diesem HTML ohne Änderungen.

Web Unlocker meldet den eigenen Status des Ziels im x-brd-status-code-Response-Header und Details in x-brd-error, sodass eine Pipeline anhand des Ergebnisses unterscheiden kann. Standardmäßig werden nur erfolgreiche Anfragen berechnet, und die Dokumentation listet die Einstellungen, die diesen Standard ändern.

Dieser Fetcher, gespeichert als fetch_pages.py, verarbeitet 5 URLs gleichzeitig, versucht jede bis zu 3 Mal und speichert die Datensätze in companies.jsonl:

import json
import os
import sys
import time
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timezone

import httpx

from parse_zoominfo import content_hash, parse_company

API = "https://api.brightdata.com/request"
KEY = os.environ["BRIGHTDATA_API_KEY"]
ZONE = os.environ["BRIGHTDATA_UNLOCKER_ZONE"]


def fetch(url: str, attempts: int = 3) -> dict:
    error = None
    for attempt in range(1, attempts + 1):
        try:
            resp = httpx.post(
                API,
                headers={"Authorization": f"Bearer {KEY}"},
                json={"zone": ZONE, "url": url, "format": "raw"},
                timeout=180,
            )
            # Die API meldet das eigene Ergebnis des Ziels in diesen 2 Headern.
            target = resp.headers.get("x-brd-status-code")
            record = parse_company(resp.text) if target == "200" else None
        except (httpx.HTTPError, ValueError) as exc:  # Timeout oder fehlerhaftes JSON
            error = f"{type(exc).__name__}: {exc}"
        else:
            if record:
                record["fetched_at"] = datetime.now(timezone.utc).isoformat()
                record["hash"] = content_hash(record)
                return {"url": url, "ok": True, "attempts": attempt,
                        "record": record}
            error = resp.headers.get("x-brd-error") or f"target {target}"
        if attempt < attempts:
            time.sleep(2 ** attempt)
    return {"url": url, "ok": False, "attempts": attempts, "error": error}


if __name__ == "__main__":
    urls = [line.strip() for line in open(sys.argv[1]) if line.strip()]
    with ThreadPoolExecutor(max_workers=5) as pool, \
            open("companies.jsonl", "a") as good, \
            open("failures.jsonl", "a") as bad:
        for result in pool.map(fetch, urls):
            (good if result["ok"] else bad).write(json.dumps(result) + "\n")
            print("saved" if result["ok"] else "not saved", result["url"])

Mit 3 Firmen-URLs, je eine pro Zeile, in urls.txt gab python fetch_pages.py urls.txt aus:

saved https://www.zoominfo.com/c/bright-data/430599389
saved https://www.zoominfo.com/c/peco-foods-inc/82428957
saved https://www.zoominfo.com/c/aviagen-group/9047889

Alle 3 Unternehmen wurden gespeichert. Hier sind die Firmenfelder des Aviagen-Eintrags in companies.jsonl:

{
  "id": "9047889",
  "name": "Aviagen",
  "domain": "aviagen.com",
  "employees": 6776,
  "revenue_usd": 2413885000,
  "founded": 1923,
  "ticker": null,
  "sic": ["02", "025"],
  "naics": ["11", "112"],
  "industries": ["Agriculture", "Animals & Livestock"],
  "hq": {
    "street": "920 Explorer Blvd NW",
    "city": "Huntsville",
    "state": "Alabama",
    "country": "United States",
    "zip": "35806"
  },
  "phone": "(256) 890-3800",
  "tech_used": ["ClickDimensions", "Freshservice", "PHP", "Zingfit"],
  "management_levels": {
    "C-Level": 14,
    "VP-Level": 28,
    "Director": 108,
    "Manager": 998,
    "Non-Manager": 1229,
    "Top Contacts": 1316
  },
  "zi_rank": 65
}

Aviagens Profil zeigte diese employees– und revenue_usd-Werte als “5K – 10K” und “2,4 Milliarden Dollar” in seinem Header und seiner Übersicht an.

Die ZoomInfo Scraper API liefert Datensätze

Bright Datas ZoomInfo Scraper API übernimmt sowohl das Entsperren als auch das Parsen. Ihr Collect by URL-Endpunkt nimmt Firmen-URLs entgegen, und Discover by search_url nimmt Verzeichnis-URLs entgegen und sammelt die auf deren Seiten gelisteten Unternehmen. Das Kontrollfeld listete beide Endpunkte auf und bot synchrone und asynchrone Modi an:

Bright Data-Kontrollfeld für den Zoominfo-Unternehmensinformationen-Scraper. Discover by search_url ist unter Verfügbare Endpunkte ausgewählt, mit 4 ZoomInfo-Verzeichnis-URLs als Eingaben und den synchronen und asynchronen Scraper-Modi darunter.

Discover by search_url nimmt Verzeichnis-URLs wie diese entgegen und liefert die auf deren Seiten gelisteten Unternehmen zurück.

Für Batches nutzen Sie den asynchronen /trigger-Endpunkt, der den Job im Hintergrund ausführt und eine Snapshot-ID zurückgibt. Der synchrone /scrape-Endpunkt liefert Datensätze direkt für kleine Tests zurück und verschiebt längere Jobs nach seinem Antwortzeitlimit in einen Snapshot.

In unserem 40-URL-Batch trafen die ersten Datensätze innerhalb einer Minute ein. Lassen Sie die API bei großen Batches Ergebnisse an Cloud-Speicher oder einen Webhook liefern. Sie sendet jeden Snapshot, wenn die letzte Eingabe abgeschlossen ist, sodass Sie die API nicht abfragen müssen.

Discover by search_url übernimmt die Seitennummerierung für Sie. Allein aus der Alabama-Verzeichnis-URL lieferte es 45 Firmendatensätze zurück. Jeder Datensatz trug die Verzeichnis-URL, aus der er stammte. Für einen größeren Filter senden Sie eine Verzeichnis-URL für jeden US-Bundesstaat oder jede Teilbranche.

Der scraper_api.py-Client löst einen Job aus und fragt ab, bis der Snapshot bereit ist, und lädt ihn dann herunter. Seine clean-Funktion wandelt den Mitarbeiterbereich in employees_min und employees_max um und markiert Umsatzbereiche wie “<5 Millionen Dollar". Sie entfernt auch die Felder für Führungsebene, Organigramm und CEO, die Personendaten enthalten. Mit –discover sendet der Client Verzeichnis-URLs an Discover by search_url:

import json
import os
import re
import sys
import time

import httpx

BASE = "https://api.brightdata.com/datasets/v3"
DATASET_ID = "gd_m0ci4a4ivx3j5l6nx"  # Zoominfo-Unternehmensinformationen
HEADERS = {"Authorization": f"Bearer {os.environ['BRIGHTDATA_API_KEY']}"}


def trigger(urls: list[str], discover: bool = False, limit: int = 50) -> str:
    params = {"dataset_id": DATASET_ID, "include_errors": "true"}
    if discover:  # Verzeichnis-URLs: der Job übernimmt deren Seitennummerierung
        params |= {"type": "discover_new", "discover_by": "search_url",
                   "limit_per_input": limit}
    resp = httpx.post(f"{BASE}/trigger", params=params, headers=HEADERS,
                      json=[{"url": u} for u in urls], timeout=60)
    resp.raise_for_status()
    return resp.json()["snapshot_id"]


def wait(snapshot_id: str, every: int = 30) -> dict:
    while True:
        try:
            progress = httpx.get(f"{BASE}/progress/{snapshot_id}",
                                 headers=HEADERS, timeout=60).json()
        except (httpx.HTTPError, ValueError):  # Wiederholung nach Netzwerkfehler
            progress = {}
        if progress.get("status") in ("ready", "failed"):
            return progress
        time.sleep(every)


def download(snapshot_id: str, tries: int = 30) -> list[dict]:
    for _ in range(tries):
        try:
            resp = httpx.get(f"{BASE}/snapshot/{snapshot_id}",
                             params={"format": "json"}, headers=HEADERS,
                             timeout=300)
            if resp.status_code == 200 and resp.content.strip():
                return resp.json()
        except (httpx.HTTPError, ValueError):  # Timeout oder unvollständige Datei
            pass
        time.sleep(10)  # warten, bis die Snapshot-Datei verfügbar ist
    raise RuntimeError(f"snapshot {snapshot_id} is not available yet")


def employee_range(text: str | None) -> tuple[int | None, int | None]:
    # "11-50" -> (11, 50), "1K - 5K" -> (1000, 5000), "10K+" -> (10000, None)
    if not text:
        return None, None
    nums = [int(float(n) * (1000 if k else 1)) for n, k
            in re.findall(r"([\d.]+)\s*(K?)", text.replace(",", ""))]
    if not nums:
        return None, None
    if len(nums) == 1:
        return nums[0], None if "+" in text else nums[0]
    return nums[0], nums[1]


def clean(row: dict) -> dict:
    row["employees_min"], row["employees_max"] = employee_range(
        row.get("employees_text"))
    # Die Mitarbeiterzahl stammt aus dem employees_text-Bereich.
    row["employees"] = row["total_employees"] = None
    row["revenue_is_range"] = (row.get("revenue_text") or "").startswith("<")
    row["stock_symbol"] = (row.get("stock_symbol") or "").strip() or None
    row["id"] = str(row["id"])
    for field in ("leadership", "org_chart", "ceo"):  # Personendaten
        row.pop(field, None)
    return row


if __name__ == "__main__":
    urls = [a for a in sys.argv[1:] if a.startswith("http")]
    snapshot = trigger(urls, discover="--discover" in sys.argv)
    progress = wait(snapshot)
    summary = {k: progress.get(k) for k in ("status", "records", "errors")}
    print(json.dumps(summary))
    rows = download(snapshot)
    # Zeilen mit einer ID und ohne Fehler behalten.
    good = [clean(r) for r in rows if r.get("id") and not r.get("error")]
    bad = [r for r in rows if r.get("error") or not r.get("id")]
    json.dump(good, open("records.json", "w"), indent=2)
    json.dump(bad, open("errors.json", "w"), indent=2)
    print(f"{len(good)} records saved to records.json")

Mit den 3 Firmen-URLs aus urls.txt als Argumente gab der Client aus:

{"status": "ready", "records": 3, "errors": 0}
3 records saved to records.json

Der Snapshot enthielt alle 3 Datensätze, und der Client speicherte sie in records.json. Jeder Datensatz trug Firmenfelder wie Umsatz, Branche, Technologien, Website, Hauptsitz und Telefonnummer.

Der ZoomInfo-Datensatz liefert bereits gesammelte Datensätze

Der ZoomInfo-Datensatz liefert Datensätze mit denselben Feldern, bereits gesammelt und nach Zeitplan aktualisiert. Prüfen Sie die Datensatzseite auf die aktuelle Datensatzanzahl, Aktualisierungshäufigkeit und Preise, und sehen Sie sich das Beispiel an, bevor Sie kaufen. Um zwischen diesem und einem ZoomInfo-Abonnement zu entscheiden, lesen Sie unseren Vergleich von Bright Data und ZoomInfo.

Filtern Sie vor dem Kauf. Die Filter des Datensatzes lassen Sie nur die benötigten Datensätze auswählen, etwa Firmenseiten mit URLs, die /c/ enthalten. Lassen Sie die Spalten für Führungsebene und Organigramm weg, die Personendaten enthalten, es sei denn, Sie haben eine rechtmäßige Grundlage dafür.

Ein Datensatz enthält Datensätze, wie sie zum Zeitpunkt der Erfassung waren, und jede Aktualisierung bringt sie auf den neuesten Stand. Um eine Aktualisierungshäufigkeit festzulegen, sammeln Sie dieselbe Stichprobe von Unternehmen zweimal im Abstand von einem Monat und zählen Sie die veränderten Datensätze.

CRM-Domains mit ZoomInfo-Profilen abgleichen

Ein CRM enthält Domains statt ZoomInfo-URLs. Dieses Skript, gespeichert als find_profiles.py, sucht bei Google für jede Domain über Bright Data. Es ruft die Top-3-Profile mit fetch aus fetch_pages.py ab und behält den ersten Datensatz, dessen domain mit der CRM-Domain übereinstimmt. Findet die Suche ohne Anführungszeichen keine Übereinstimmung, sucht das Skript erneut mit der Domain in Anführungszeichen:

import json
import os
import re
import sys
from concurrent.futures import ThreadPoolExecutor
from urllib.parse import quote_plus

import httpx

from fetch_pages import fetch

API = "https://api.brightdata.com/request"
KEY = os.environ["BRIGHTDATA_API_KEY"]
# Eine SERP-API-Zone, falls vorhanden, ansonsten die Web Unlocker-Zone.
ZONE = (os.environ.get("BRIGHTDATA_SERP_ZONE")
        or os.environ["BRIGHTDATA_UNLOCKER_ZONE"])
PROFILE = re.compile(r"https://www\.zoominfo\.com/c/[^/?#]+/\d+")
# Jede Abfrage fand Profile, die die andere übersah, daher beide versuchen.
QUERIES = ("site:zoominfo.com/c {}", 'site:zoominfo.com/c "{}"')


def candidates(query: str, top: int = 3) -> list[str]:
    url = f"https://www.google.com/search?q={quote_plus(query)}&brd_json=1"
    for _ in range(3):
        try:
            resp = httpx.post(
                API,
                headers={"Authorization": f"Bearer {KEY}"},
                json={"zone": ZONE, "url": url, "format": "raw"},
                timeout=120,
            )
            results = resp.json().get("organic", [])  # geparste Ergebnisse
        except (httpx.HTTPError, ValueError):  # Timeout oder leerer Body
            continue
        links = [row.get("link", "") for row in results]
        found = (PROFILE.match(link) for link in links)
        return list(dict.fromkeys(m.group(0) for m in found if m))[:top]
    return []


def find_profile(domain: str) -> dict | None:
    for query in QUERIES:
        for url in candidates(query.format(domain)):
            result = fetch(url)
            # Ein Profil nur behalten, wenn seine Website der CRM-Domain entspricht.
            if result["ok"] and result["record"]["domain"] == domain:
                return result["record"]
    return None


if __name__ == "__main__":
    domains = [d.strip().lower().removeprefix("www.") for d in sys.argv[1:]]
    with ThreadPoolExecutor(max_workers=5) as pool, \
            open("matches.jsonl", "a") as out:
        records = pool.map(find_profile, domains)
        for domain, record in zip(domains, records, strict=True):
            out.write(json.dumps({"domain": domain, "record": record}) + "\n")
            print(domain, record["id"] if record else "no match")

Für 4 CRM-Domains gab python find_profiles.py aviagen.com pecofoods.com brightdata.com walmart.com aus:

aviagen.com 9047889
pecofoods.com 82428957
brightdata.com 430599389
walmart.com no match

Für jede Übereinstimmung lieferte das Skript einen vollständigen Firmendatensatz zurück. Die Suche ohne Anführungszeichen fand Aviagen und Bright Data, und die Suche mit Anführungszeichen fand Peco Foods. Walmarts Profil erschien in keiner der beiden Suchergebnisse, daher lieferte das Skript keine Übereinstimmung statt eines falschen Unternehmens zurück. Senden Sie nicht übereinstimmende Domains an eine Namenssuche oder eine Überprüfungswarteschlange.

Jeden Datensatz validieren, bevor er Ihr CRM erreicht

Jede Antwort, die wir von ZoomInfo und von Bright Datas APIs sahen, erfordert eine eigene Aktion:

Antwort Bedeutung Aktion
200 mit Firmendaten in ng-state Eine echte Firmenseite Parsen und speichern
301-Weiterleitung Die URL hat sich geändert Die neue URL speichern und die ID als Schlüssel verwenden
403 mit px-captcha im Body HUMAN hat die Anfrage blockiert Mit neuer Sitzung wiederholen
429 mit cf-mitigated: challenge Das Cloudflare-Anfragelimit pro IP wurde erreicht Die IP rotieren und dort nicht mehr wiederholen
429 ohne Cloudflare-Header Bright Datas Auto-Drosselung, ausgelöst durch niedrige Erfolgsrate Anfragerate senken und fehlschlagende Anfragen beheben
502 ohne Anti-Bot-Signatur Ein vorübergehender Upstream-Fehler Mit Backoff wiederholen
Web Unlocker x-brd-status-code-Header Der eigene Status der Zielseite 200-Ergebnisse speichern und die anderen eine begrenzte Anzahl von Malen wiederholen
Scraper API 202 von /scrape Der Job wurde in einen Snapshot verschoben Die Snapshot-ID abfragen
Verzeichnisseite, deren pageNum niedriger als angefordert ist Jenseits der letzten erreichbaren Seite Seitennummerierung stoppen und Filter eingrenzen

Statusprüfungen fangen fehlgeschlagene Seiten ab, und Feldprüfungen fangen fehlerhafte Datensätze ab:

  • IDs als Strings beibehalten. Die von uns untersuchten ZoomInfo-Personen-IDs reichten von -2.135.143.530 bis 17.773.369.573, sodass sie weder in einen vorzeichenbehafteten noch in einen vorzeichenlosen 32-Bit-Integer passen.
  • Mitarbeiterzahl aus Bereichen oder dem eingebetteten JSON lesen. Nutzen Sie employees_min und employees_max aus clean, oder numberOfEmployees aus dem JSON.
  • Umsatzbereiche markieren. Ein Wert wie “<5 Millionen Dollar" ist ein Bereich, und das JSON enthielt die Schätzung.
  • Branchencodes aufteilen. Werte wie “54,541” trugen einen Sektorcode und einen Teilsektorcode.
  • Leere CEO-Felder erwarten. ZoomInfo ließ das CEO-Feld auf jeder von uns geprüften öffentlichen Seite leer.
  • Füllraten pro Batch verfolgen. Ein plötzlicher Rückgang der Füllrate eines Felds kann bedeuten, dass ZoomInfo eine Vorlage geändert hat.

Unser Leitfaden zum Beheben ungenauer Web-Scraping-Daten erklärt die Datenbereinigung ausführlicher.

Was ZoomInfo-Scraping im großen Maßstab kostet

Die Optionen haben unterschiedliche Abrechnungseinheiten, und jede Preisseite listet die aktuellen Tarife auf:

Option Abgerechnet nach Weitere Kosten
Web Unlocker und Ihr Parser Erfolgreiche Anfragen, mit Standardeinstellungen URL-Erkennung, Parsing, Deduplizierung und Hosting
ZoomInfo Scraper API Gelieferte Datensätze Deduplizierung und Speicherung
ZoomInfo-Datensatz Datensätze in der Datei Deduplizierung und Speicherung
Residential-Proxys und eigener Stack Bandbreite Sitzungen, Wiederholungsversuche, Parsing, Deduplizierung und die Entwicklungsarbeit, um sie zu betreiben

Die von uns gemessenen Firmenseiten waren 42 bis 55 KB komprimiert, sodass 100.000 Seiten etwa 4 bis 6 GB Bandbreite nutzen. Bei größeren Mengen prüfen Sie jede Preisseite auf Mengentarife.

Residential-Proxys geben Ihnen die meiste Kontrolle. Bei dieser Seitengröße ist ihre Bandbreitenrechnung im Vergleich zu den technischen Kosten gering. Bei einem selbst erstellten Stack zahlen Sie auch für blockierte erste Anfragen, Wiederholungsversuche und die zusätzlichen Sitzungen bei etwa 3 Seiten pro IP. Sie benötigen auch einen Entwickler, der den Stack pflegt, wenn ZoomInfo seine Anti-Bot-Regeln oder Seitenstruktur ändert.

In einem IsraelTech-Podcast-Interview sagte Bright Data CEO Or Lenchner: “Sie brauchen uns, wenn Sie wiederholt und ständig Skalierung benötigen.” Für kleine Datenmengen riet Lenchner Teams, sie selbst zu sammeln. Unser Vergleich von Managed- und DIY-Scraping listet die Kosten jedes Ansatzes auf.

Abschließende Gedanken

ZoomInfos Seiten enthielten ungerundete firmographische Daten als eingebettetes JSON, aber in unseren Tests prüften Anti-Bot-Systeme jede erste Anfrage und blockierten IPs nach etwa 3 Seiten. Das JSON machte die Extraktion zur kleineren Aufgabe, daher benötigen im großen Maßstab Sitzungen, URL-Erkennung und Deduplizierung die meiste Arbeit. Unsere Skripte sammeln nur öffentliche Firmendaten, da Kontaktdetails ein Konto erforderten und Personendaten eine rechtmäßige Grundlage erfordern können.

Bright Datas Web Unlocker und ZoomInfo Scraper API übernehmen die Sitzungen, und der ZoomInfo-Datensatz liefert Datensätze, die Bright Data bereits gesammelt hat. Testen Sie die ZoomInfo Scraper API an einer Stichprobe Ihrer eigenen Konten und vergleichen Sie die Datensätze mit Ihrem CRM.

FAQ

Kann man ZoomInfo scrapen?

Ja, für die Firmenseiten, die ohne Login geladen werden. Kontaktdetails erforderten ein Konto. Die öffentlichen Firmenseiten trugen ihre Daten als eingebettetes JSON. In unseren Tests blockierte ZoomInfo alle 32 ersten Anfragen ohne Referrer von einer anderen Seite, starten Sie also jede neue Sitzung mit einem solchen.

Wie scrapt man Daten von ZoomInfo?

Finden Sie die Profil-URL jedes Unternehmens, rufen Sie die öffentliche Seite ab und lesen Sie die Daten aus ihrem eingebetteten JSON. IPs erhielten in unseren Tests einen Median von 3 Seiten, planen Sie also viele Sitzungen ein. Bright Datas ZoomInfo Scraper API übernimmt die Sitzungen und das Parsing für Sie.

Gibt es eine ZoomInfo-API?

Ja. ZoomInfo verkauft Zugang zur eigenen Datenbank über eine API. Für öffentliche Firmenprofile sammelt Bright Datas ZoomInfo Scraper API diese Seiten und liefert strukturierte Datensätze zurück. Bright Datas ZoomInfo-Datensatz liefert Datensätze mit denselben Feldern, bereits gesammelt und nach Zeitplan aktualisiert.

Ist ZoomInfo veraltet?

Einige seiner Datensätze können veraltet sein, prüfen Sie sie also, bevor sie ein CRM erreichen. Unsere Suche nach einer Domain lieferte 3 ZoomInfo-IDs zurück, und eine war ein Duplikat mit anderer Mitarbeiterzahl. Deduplizieren Sie, vergleichen Sie Füllraten und aktualisieren Sie so oft, wie sich Ihre Felder ändern.

Kann ich ZoomInfo kostenlos nutzen?

Teilweise. Firmenprofilseiten auf zoominfo.com luden ohne Konto, obwohl ihr HTML Kontaktdetails und einige Abschnitte als kostenpflichtig markierte. ZoomInfo bot auch einen kostenlosen Lite-Plan mit begrenzten monatlichen Credits an. Neue Bright Data-Konten erhalten kostenlose Credits, um die ZoomInfo Scraper API an diesen Seiten zu testen.

Gibt es einen kostenlosen ZoomInfo-Scraper?

Ja. Bright Datas Open-Source-Repository zoominfo-scraper auf GitHub enthält Python-Skripte, die Bright Datas APIs nutzen. In unseren Tests wurden Anfragen mit einem curl- oder python-requests-User-Agent in 29 von 29 Fällen blockiert, selbst mit einem Google-Referrer. Bright Datas APIs übernehmen das Entsperren für diese Skripte.