Wenn Sie in großem Maßstab crawlen, ist der langsamste Teil die Wartezeit. Ein synchroner Scraper sendet eine Anfrage, blockiert, bis eine stark gesicherte Seite gerendert wird und zurückkommt, parst sie und bewegt sich erst dann zur nächsten URL. Stellen Sie Tausende von URLs dahinter und Sie verbringen den Großteil Ihrer Laufzeit untätig. Das asynchrone Muster dreht es um: Sie übergeben einen Stapel URLs an einen Crawler, er erledigt die langsame Rendering-Arbeit auf seiner eigenen Infrastruktur und postet jedes fertige Ergebnis zurück an einen Webhook, den Sie kontrollieren. Diese Anleitung baut genau das: einen Flask-Callback-Server, der Ergebnisse vom asynchronen Crawlbase Crawler empfängt und sie in MySQL speichert.
Das Beispielziel ist LinkedIn, aber lesen Sie zuerst den direkt darunter stehenden Hinweis, denn was Sie speichern, ist wichtiger als wie Sie es speichern. Diese Anleitung ist bewusst auf öffentliche, nicht personenbezogene Daten beschränkt: Unternehmensseiten-Felder und öffentlicher Stellenangebots-Text, keine individuellen Mitgliederprofile. Der eigentliche Lernwert liegt hier in der Architektur, asynchrones Crawling plus Callback-Server plus Datenbank, und dieses Muster funktioniert gleich, unabhängig davon, auf welche öffentliche Quelle Sie es richten.
LinkedIns Nutzungsbedingungen schränken den automatisierten Zugriff stark ein, und die meisten LinkedIn-Daten sind sensible personenbezogene Daten über identifizierbare Personen. Dieses Tutorial sammelt nur ÖFFENTLICHE, nicht personenbezogene Felder (Unternehmensname, öffentliche Unternehmensbeschreibung, öffentlicher Stellenangebots-Text), niemals Mitgliederprofile, Verbindungen oder irgendetwas hinter einem Login. Wenn personenbezogene Daten beteiligt sind, gelten DSGVO und CCPA: Sie benötigen eine Rechtsgrundlage und müssen Löschungsanfragen nachkommen. Für jede reale oder kommerzielle Nutzung ist der sanktionierte Weg LinkedIns offizielle APIs und Partnerprogramme, kein Scraper. Lesen Sie den vollständigen Rechtsabschnitt gegen Ende, bevor Sie dies auf irgendetwas Reales richten.
Was Sie bauen werden
Ein kleines asynchrones Crawling-System in Python mit drei beweglichen Teilen und einer gemeinsamen MySQL-Datenbank. Anstatt eines blockierenden Skripts wird die Arbeit aufgeteilt, damit das langsame Crawlen außerhalb Ihres Rechners stattfindet und Ergebnisse ankommen, wenn sie fertig sind:
- Ein asynchrones Crawler-Anfrage-Skript, das eine Liste öffentlicher URLs an den asynchronen Crawlbase Crawler sendet und für jede eine Anfrage-ID (RID) aufzeichnet.
- Ein Flask-Callback-Server, der jeden fertigen Crawl als HTTP-POST empfängt, ihn dekomprimiert und die rohe Nutzlast speichert.
- Ein Prozessor, der gespeicherte Nutzlasten nach einem Zeitplan liest, die öffentlichen Felder extrahiert und strukturierte Zeilen schreibt.
- Ein MySQL-Schema mit einer Anfrage-Tracking-Tabelle und Tabellen für die öffentlichen Felder, die Sie behalten.
Beachten Sie, was absichtlich im Schema fehlt: keine Personennamen, keine Überschriften, keine individuellen Profilzusammenfassungen, keine Verbindungsdaten. Wir speichern die öffentliche Identität eines Unternehmens und öffentliche Stellenangebote, also unpersönliche Informationen, die ein Unternehmen über sich selbst veröffentlicht.
Warum asynchron, und warum ein Callback-Server
Eine LinkedIn-Unternehmensseite oder ein öffentliches Stellenangebot rendert clientseitig und sitzt hinter aggressiven Bot-Abwehrmechanismen, sodass ein einzelner Abruf langsam und oft herausgefordert ist. Das synchron für eine lange Liste von URLs zu tun bedeutet, dass Ihr Skript bei jeder Seite blockiert. Der asynchrone Crawler akzeptiert Ihre URL, gibt sofort eine Anfrage-ID zurück und erledigt dann das langsame Rendering und die Wiederholungsversuche auf seiner eigenen Infrastruktur. Wenn er schließlich eine saubere Antwort erhält, sendet er dieses Ergebnis als POST an Ihren Webhook.
Dieses Push-Modell ist der Grund, warum Sie einen Callback-Server benötigen. Ihr Endpunkt fragt nicht ab und wartet nicht; er sitzt einfach bereit, und jedes Ergebnis landet, wenn es fertig ist, in beliebiger Reihenfolge, in der die Crawls abgeschlossen werden. Die Crawler-Engine sendet den Body GZIP-komprimiert, sodass Ihr Endpunkt vor dem Lesen dekomprimieren muss. Das Entkoppeln von Anfrage, Empfang und Verarbeitung in drei Skripte ist es, was das System ermöglicht, einen großen Stapel aufzunehmen, ohne dass ein einzelner Schritt die anderen blockiert. Wenn Sie den breiteren Hintergrund zur Engine selbst möchten, lesen Sie unsere Anleitung zum Extrahieren von Daten mit dem Crawlbase Crawler.
Voraussetzungen
Einige Dinge müssen vorhanden sein. Keine davon nimmt viel Zeit in Anspruch.
Python 3.8 oder höher. Bestätigen Sie mit python3 --version. Falls Sie es nicht haben, installieren Sie es von python.org.
MySQL 8. Ein laufender MySQL-Server, mit dem Sie lokal verbinden können. Das offizielle Installationshandbuch deckt jede Plattform ab.
Ein Crawlbase-Konto und ein Normal-Token (TCP). Registrieren Sie sich, öffnen Sie Ihr Dashboard und kopieren Sie Ihr Token. LinkedIn wird vom Normal-Anfrage-Crawler bedient, verwenden Sie also hier das TCP-Token, nicht das JavaScript-Token. Behandeln Sie das Token wie ein Passwort und halten Sie es aus der Versionskontrolle heraus.
Eine Möglichkeit, localhost zu exponieren. Der Crawler postet an eine öffentliche URL, daher benötigen Sie während der Entwicklung einen Tunnel wie ngrok, um Ihre lokale Flask-App zu erreichen.
Das Projekt einrichten
Erstellen Sie eine isolierte virtuelle Umgebung und installieren Sie dann die Bibliotheken, die das System benötigt.
python3 -m venv .venv source .venv/bin/activate pip install Flask mysql-connector-python pyyaml requests SQLAlchemy
Unter Windows aktivieren Sie mit .venv\Scripts\activate anstelle der source-Zeile. Vier Bibliotheken erledigen die Arbeit: Flask ist der Webhook-Server, SQLAlchemy mit mysql-connector-python übernimmt die Datenbank, requests sendet die Crawl-Anfragen und pyyaml liest Ihr Token aus einer Einstellungsdatei. Erstellen Sie eine settings.yml neben Ihren Skripten, um das Token und Ihren Crawler-Namen zu speichern.
token: YOUR_CRAWLBASE_TOKEN crawler: linkedin-public-crawler
Schritt 1: Das MySQL-Schema entwerfen
Das Schema hat zwei Aufgaben: jeden Crawl-Anfrage durch ihren Lebenszyklus zu verfolgen und die öffentlichen Felder zu speichern, die Sie behalten. Erstellen Sie einen Benutzer, eine Datenbank und die Tabellen. Führen Sie diese im MySQL-Kommandozeilenclient aus.
CREATE USER 'linkedincrawler'@'localhost' IDENTIFIED BY 'linked1nS3cret'; CREATE DATABASE linkedin_crawler_db; GRANT ALL PRIVILEGES ON linkedin_crawler_db.* TO 'linkedincrawler'@'localhost'; USE linkedin_crawler_db;
Jetzt die Tabellen. Die Tabelle crawl_requests ist die Kontrolltabelle für den gesamten asynchronen Prozess: Jede URL, die Sie senden, erhält eine Zeile, verfolgt durch ihren status, wenn sie durch waiting, dann received und dann processed läuft. Die Spalte crawlbase_rid verknüpft eine Zeile mit der Anfrage-ID, die der Crawler zurückgibt, dem einzigen Schlüssel, den Sie haben, um einen eingehenden Callback mit der Anfrage zuzuordnen, die ihn ausgelöst hat.
CREATE TABLE IF NOT EXISTS `crawl_requests` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `url` TEXT NOT NULL, `status` VARCHAR(30) NOT NULL, `crawlbase_rid` VARCHAR(255) NOT NULL ); CREATE INDEX `idx_crawl_requests_status` ON `crawl_requests` (`status`); CREATE INDEX `idx_crawl_requests_rid` ON `crawl_requests` (`crawlbase_rid`);
Die Zieltabellen enthalten nur öffentliche, nicht personenbezogene Unternehmensdaten. Eine Zeile pro Unternehmensseite, plus eine Kindtabelle für die öffentlichen Stellenangebote, auf die diese Seite verlinkt. Es gibt nirgendwo eine Spalte für den Namen, den Titel oder den Profiltext einer Person. Das ist die Datenschutzgrenze, die im Schema selbst konkretisiert wird.
CREATE TABLE IF NOT EXISTS `company_pages` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `crawl_request_id` INT NOT NULL, `company_name` VARCHAR(255), `industry` VARCHAR(255), `description` TEXT, FOREIGN KEY (`crawl_request_id`) REFERENCES `crawl_requests`(`id`) ); CREATE TABLE IF NOT EXISTS `company_job_postings` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `company_page_id` INT NOT NULL, `title` VARCHAR(255), `location` VARCHAR(255), `description` TEXT, FOREIGN KEY (`company_page_id`) REFERENCES `company_pages`(`id`) );
Schritt 2: Das ORM definieren
Ordnen Sie diese Tabellen Python-Klassen mit SQLAlchemy zu, damit der Rest des Codes mit Objekten statt mit rohem SQL arbeitet. Speichern Sie dies als lib/database.py. Die Klassen spiegeln das Schema genau wider: ein CrawlRequest für die Verfolgung, eine CompanyPage für die öffentlichen Unternehmensfelder und ein JobPosting-Kind für jedes öffentliche Stellenangebot.
from typing import List from sqlalchemy import ForeignKey, create_engine from sqlalchemy.orm import DeclarativeBase, Session, Mapped, mapped_column, relationship class Base(DeclarativeBase): pass class CrawlRequest(Base): __tablename__ = 'crawl_requests' id: Mapped[int] = mapped_column(primary_key=True) url: Mapped[str] status: Mapped[str] crawlbase_rid: Mapped[str] company_page: Mapped['CompanyPage'] = relationship(back_populates='crawl_request') class CompanyPage(Base): __tablename__ = 'company_pages' id: Mapped[int] = mapped_column(primary_key=True) company_name: Mapped[str] industry: Mapped[str] description: Mapped[str] crawl_request_id: Mapped[int] = mapped_column(ForeignKey('crawl_requests.id')) crawl_request: Mapped['CrawlRequest'] = relationship(back_populates='company_page') job_postings: Mapped[List['JobPosting']] = relationship(back_populates='company_page') class JobPosting(Base): __tablename__ = 'company_job_postings' id: Mapped[int] = mapped_column(primary_key=True) title: Mapped[str] location: Mapped[str] description: Mapped[str] company_page_id: Mapped[int] = mapped_column(ForeignKey('company_pages.id')) company_page: Mapped['CompanyPage'] = relationship(back_populates='job_postings') def create_database_session(): url = 'mysql+mysqlconnector://linkedincrawler:linked1nS3cret@localhost:3306/linkedin_crawler_db' engine = create_engine(url, echo=True) return Session(engine)
create_database_session gibt eine Session zurück, die jedes andere Skript importiert. Der Verbindungsstring enthält Benutzer, Passwort, Host und Datenbank, die Sie in Schritt 1 eingerichtet haben; ändern Sie sie hier, wenn Ihre abweichen.
Schritt 3: URLs an den asynchronen Crawler senden
Dieses Skript liest eine Liste öffentlicher URLs, sendet jede an den asynchronen Crawler und zeichnet die zurückgegebene RID mit einem waiting-Status auf. Die Schlüsselparameter sind callback=true, was den Crawler anweist, Ergebnisse zurückzuposten, anstatt sie inline zurückzugeben, und crawler=, was den Crawler benennt, den Sie im Dashboard erstellen. Speichern Sie es als crawl.py und tragen Sie Ihre öffentlichen Unternehmens- und Stellenangebots-URLs eine pro Zeile in urls.txt ein.
import requests import urllib.parse import json import yaml from json import JSONDecodeError from lib.database import CrawlRequest, create_database_session settings = yaml.safe_load(open('settings.yml')) token = settings.get('token') crawler = settings.get('crawler') if not token or not crawler: print('Set your token and crawler name in settings.yml') exit() urls = open('urls.txt', 'r').readlines() api = 'https://api.crawlbase.com?token={0}&callback=true&crawler={1}&url={2}&autoparse=true' session = create_database_session() for url in urls: url = url.strip() if not url: continue encoded = urllib.parse.quote(url, safe='') api_url = api.format(token, crawler, encoded) print(f'Requesting crawl for {url}') try: response = requests.get(api_url) rid = json.loads(response.text)['rid'] request_row = CrawlRequest(url=url, crawlbase_rid=str(rid), status='waiting') session.add(request_row) session.commit() except JSONDecodeError: print(f'Could not decode response for {url}') print('Done pushing crawl requests.')
Jeder Aufruf gibt einen kleinen JSON-Body wie {"rid": 12341234} zurück. Das Skript speichert diese RID in crawl_requests mit Status waiting und bewegt sich sofort zur nächsten URL, ohne auf das eigentliche Crawlen zu blockieren. Der Parameter autoparse=true weist den Crawler an, strukturierte Felder statt rohem HTML zurückzugeben, was der Prozessor in Schritt 6 liest. Das ist der ganze Sinn des asynchronen Modells: Hundert URLs zu senden dauert Sekunden, und die langsame Arbeit findet anderswo statt.
Der Push, den Sie gerade geschrieben haben, gibt in Sekunden eine RID zurück, weil der langsame Teil, das Rendering einer gesicherten LinkedIn-Seite hinter einer vertrauenswürdigen Residential-IP und Wiederholungsversuche bis zu einem sauberen 200, auf Crawlbase-Infrastruktur stattfindet, nicht auf Ihrer. Der asynchrone Crawler reiht Ihren Stapel ein, erledigt das Rendering und die Rotation serverseitig und postet jedes fertige Ergebnis an Ihren Webhook, sodass Sie nie eine Headless-Browser-Flotte oder einen Proxy-Pool betreiben müssen. Starten Sie im kostenlosen Kontingent.
Schritt 4: Den Flask-Callback-Server bauen
Das ist das Herzstück des Systems. Der Crawler postet jedes fertige Ergebnis an eine einzige Route. Ihre Aufgabe ist es, die Anfrage zu validieren, den Body zu dekomprimieren und die Nutzlast zu speichern, damit der Prozessor sie abholen kann. Der Crawler sendet die RID in einem Header namens rid und zwei Status-Header: PC-Status (den Crawlbase-Status) und Original-Status (den Status der Zielseite). Sie behalten nur Ergebnisse, bei denen beide 200 sind. Speichern Sie dies als callback_server.py.
import gzip import os from flask import Flask, request from lib.database import CrawlRequest, create_database_session app = Flask(__name__) session = create_database_session() os.makedirs('./data', exist_ok=True) def header_status(name): value = request.headers.get(name) return int(value.split(',')[0]) if value else None @app.route('/crawlbase_crawler_callback', methods=['POST']) def crawlbase_crawler_callback(): rid = request.headers.get('rid') encoding = request.headers.get('Content-Encoding') if rid is None: return ('', 204) if rid == 'dummyrequest': print('Callback server is working') return ('', 204) if header_status('PC-Status') != 200 or header_status('Original-Status') != 200: return ('', 204) crawl_request = session.query(CrawlRequest).filter_by(crawlbase_rid=rid, status='waiting').first() if crawl_request is None: print(f'No waiting request for rid {rid}') return ('', 204) body = request.data if encoding == 'gzip': try: body = gzip.decompress(body) except OSError: pass with open(f'./data/{rid}.json', 'wb') as f: f.write(body) crawl_request.status = 'received' session.commit() print(f'Received rid {rid}') return ('', 201) if __name__ == '__main__': app.run(port=5000)
Gehen Sie die Prüfungen durch, denn jede zählt. Eine fehlende rid bedeutet, dass die Anfrage nicht vom Crawler kam; sie wird verworfen. Eine dummyrequest-RID ist der Test-Ping, den die Plattform sendet, um zu bestätigen, dass Ihr Endpunkt erreichbar ist; Sie protokollieren ihn und kehren früh zurück. Die Statusprüfung ignoriert alles, was nicht ein sauberes 200 auf beiden Seiten ist. Dann schlagen Sie die RID in crawl_requests mit Status waiting nach: Wenn keine solche Zeile existiert, entspricht der Callback keiner Anfrage, die Sie gestellt haben, und er wird ignoriert. Erst nach alledem dekomprimieren und speichern Sie den Body und kippen dann die Zeile auf received. Der Endpunkt blockiert nie; er schreibt die Datei und kehrt sofort zurück, was ihn auch unter einem Schwall von Callbacks responsiv hält.
Ihre Callback-URL ist öffentlich, solange der Tunnel offen ist. Sichern Sie sie: Akzeptieren Sie nur POST, verlangen Sie ein geheimes Token in einem benutzerdefinierten Header oder URL-Parameter, das Sie bei jeder Anfrage verifizieren, und bestätigen Sie, dass die erwarteten Header rid, PC-Status und Original-Status vorhanden sind. Vermeiden Sie IP-Allowlisting, da die Quelladressen rotieren und sich ohne Vorankündigung ändern können.
Schritt 5: Den Server exponieren und den Crawler registrieren
Der Crawler benötigt eine öffentliche URL, an die er posten kann. Wenn die Flask-App auf Port 5000 läuft, öffnen Sie einen Tunnel.
python callback_server.py ngrok http 5000
ngrok gibt eine öffentliche HTTPS-URL aus. Ihre vollständige Callback-Route ist diese URL plus der Pfad, zum Beispiel https://your-subdomain.ngrok.io/crawlbase_crawler_callback. Bestätigen Sie, dass der Endpunkt aktiv ist, mit einem Test-Ping, bevor Sie überhaupt den Crawler einbeziehen.
curl -i -X POST 'http://localhost:5000/crawlbase_crawler_callback' \ -H 'rid: dummyrequest' \ -H 'Content-Type: gzip/json' \ -H 'Content-Encoding: gzip'
Sie sollten Callback server is working im Flask-Log sehen. Gehen Sie jetzt in Ihr Crawlbase-Dashboard, öffnen Sie die Seite zum Erstellen von Crawlern, geben Sie dem Crawler denselben Namen, den Sie in settings.yml eingetragen haben, und fügen Sie Ihre vollständige ngrok-Callback-URL ein. LinkedIn wird vom Normal-Anfrage-Crawler (TCP) bedient, wählen Sie also diesen Typ. Einmal gespeichert, weiß der Crawler, wohin er Ergebnisse posten soll.
Schritt 6: Empfangene Nutzlasten in strukturierte Zeilen verarbeiten
Der Callback-Server speichert nur rohe Nutzlasten. Ein separater Prozessor läuft nach einem Zeitplan, holt alles mit Status received ab, extrahiert die öffentlichen Felder, schreibt die strukturierten Zeilen und markiert die Anfrage als processed. Das Trennen von Empfang und Verarbeitung bedeutet, dass ein langsamer Datenbankschreibvorgang den Webhook nie blockiert. Speichern Sie dies als process.py.
import json import sched import time from lib.database import CrawlRequest, CompanyPage, JobPosting, create_database_session INTERVAL_SECONDS = 60 BATCH_LIMIT = 10 def process(): session = create_database_session() received = session.query(CrawlRequest).filter_by(status='received').limit(BATCH_LIMIT).all() if not received: print('No received requests to process.') return for req in received: with open(f'./data/{req.crawlbase_rid}.json') as f: data = json.load(f) page = CompanyPage( company_name=data.get('name'), industry=data.get('industry'), description=data.get('description'), ) page.crawl_request_id = req.id session.add(page) for job in data.get('jobs', []): posting = JobPosting( title=job.get('title'), location=job.get('location'), description=job.get('description'), ) posting.company_page = page session.add(posting) req.status = 'processed' session.commit() def process_and_reschedule(): process() scheduler.enter(INTERVAL_SECONDS, 1, process_and_reschedule) if __name__ == '__main__': scheduler = sched.scheduler(time.monotonic, time.sleep) process_and_reschedule() scheduler.run()
Der Prozessor liest nur die unpersönlichen Unternehmens- und Jobfelder aus der geparsten Nutzlast: den Unternehmensnamen, die Branche, die öffentliche Beschreibung und den Titel, den Standort und die Beschreibung jedes öffentlichen Stellenangebots. Er berührt nie ein Feld auf Personenebene, selbst wenn eines in der Nutzlast vorhanden wäre. Diese enge Extraktionsliste zu halten ist die zweite Hälfte der Datenschutzgrenze, nach dem Schema. Die sched-Schleife führt process alle 60 Sekunden erneut aus und leert höchstens zehn Anfragen pro Durchlauf, was den Speicher auch bei einem großen Rückstand flach hält.
Die gesamte Pipeline ausführen
Mit dem registrierten Crawler führen Sie die drei Teile aus, jedes in seinem eigenen Terminal mit aktiver virtueller Umgebung. Die Reihenfolge ist wichtig: Der Callback-Server und der Prozessor müssen laufen, bevor Sie Anfragen senden, sonst kommen frühe Callbacks ohne Zielort an.
# terminal 1: webhook (already running, plus ngrok) python callback_server.py # terminal 2: scheduled processor python process.py # terminal 3: push the batch python crawl.py
Während crawl.py läuft, erscheinen Zeilen in crawl_requests mit Status waiting. Minuten später, wenn der Crawler jede Seite abschließt, kippt der Callback-Server sie auf received und schreibt eine JSON-Datei unter ./data. Bei seinem nächsten Durchlauf liest der Prozessor diese Dateien, befüllt company_pages und company_job_postings und markiert die Anfragen als processed. Sie können das live über den Monitoring-Tab des Crawlers im Dashboard beobachten, der den Status jeder Anfrage in Echtzeit anzeigt.
Wie die gespeicherten Daten aussehen
Nach einem vollständigen Lauf enthalten die Zieltabellen saubere, unpersönliche Unternehmenseinträge. Eine einzelne verarbeitete Unternehmensseite sieht so aus, wenn Sie sie als JSON zurücklesen.
{ "company_name": "Example Robotics", "industry": "Industrial Automation", "description": "We design warehouse automation systems.", "job_postings": [ { "title": "Backend Engineer", "location": "Remote, EU", "description": "Build and operate our ingestion services." } ] }
Jedes Feld dort ist etwas, das das Unternehmen über sich selbst veröffentlicht. Es gibt keine Person, keinen Kontakt, kein Profil. Das ist by Design, und es ist das, was den Datensatz vertretbar macht.
Skalieren und zusätzlichen Kontext senden
Die Architektur skaliert ohne strukturelle Änderung: Eine größere urls.txt bedeutet mehr waiting-Zeilen, der Crawler absorbiert die Warteschlange und Callbacks landen, wenn Crawls abgeschlossen sind. Um Nutzlasten Ihrem eigenen Kontext zuzuordnen, hängen Sie Daten mit dem Parameter callback_headers an, wenn Sie eine Anfrage senden. Der Crawler gibt diese Header beim Callback zurück, sodass Sie zum Beispiel eine Stapel-ID ohne Speicherung in der URL mitführen können.
raw_headers = f'BATCH-ID:{batch_id}|SOURCE:public-company-page' encoded_headers = urllib.parse.quote(raw_headers, safe='') # append &callback_headers={encoded_headers} to the api url
Auf der Empfängerseite lesen Sie sie als gewöhnliche Request-Header zurück: request.headers.get('BATCH-ID'). Für eine tiefere Abdeckung, wie große Läufe gegen gesicherte Ziele gesund bleiben, lesen Sie unsere Anleitungen zum Scrapen von Websites ohne blockiert zu werden und zum Aufbau einer skalierbaren Web-Datenpipeline.
Ist es legal, LinkedIn zu scrapen?
Das ist der Abschnitt, den Sie klären sollten, bevor Sie Produktionscode schreiben, nicht danach. LinkedIns Nutzungsbedingungen und seine Richtlinie zu verbotener Software und Erweiterungen verbieten ausdrücklich Scraping und automatisierte Datenerfassung, und LinkedIn setzt diese Bedingungen durch. Diese Position gilt unabhängig davon, wie sorgfältig Ihre Werkzeuge sind. Der Code in dieser Anleitung lässt den technischen Teil funktionieren; er macht das Scrapen von LinkedIn nicht konform mit LinkedIns Bedingungen. Lesen Sie die Nutzungsbedingungen und LinkedIns robots.txt, und behandeln Sie beide als die Grenze dessen, was Sie tun.
Die Datendimension ist ebenso wichtig. Die meisten LinkedIn-Inhalte sind personenbezogene Daten über identifizierbare Personen: Namen, Berufsverläufe, Überschriften, Verbindungen und Beiträge. Unter der DSGVO in Europa und dem CCPA in Kalifornien benötigt die Verarbeitung personenbezogener Daten eine Rechtsgrundlage, und Personen haben Rechte, einschließlich des Rechts, ihre Daten löschen zu lassen. Es gibt auch echte Rechtsprechung: In hiQ Labs v. LinkedIn untersuchten US-Gerichte das Scrapen öffentlicher Profile unter dem Computer Fraud and Abuse Act, aber dieser Rechtsstreit war eng gefasst, jurisdiktionsspezifisch und segnete Scraping im Allgemeinen nicht und hob auch nicht LinkedIns Vertragsbedingungen oder Datenschutzrecht auf. Die Rechtmäßigkeit hängt von den Daten, der Methode, der Jurisdiktion und den Vereinbarungen ab, an die Sie gebunden sind; behandeln Sie also pauschale Behauptungen, dass "öffentlich gleich erlaubt" sei, mit Skepsis.
Deshalb ist dieses Tutorial so eingegrenzt, wie es ist. Es speichert nur öffentliche, nicht personenbezogene Unternehmensinformationen: Unternehmensnamen, Branchen, öffentliche Beschreibungen und öffentlichen Stellenangebots-Text, den ein Unternehmen über sich selbst veröffentlicht. Es erstellt nie Profile von Einzelpersonen, berührt nie etwas hinter einem Login und erfasst nie personenbezogene Mitgliederdaten. Für jeden realen oder kommerziellen Bedarf ist der richtige Weg LinkedIns offizielle APIs und Partnerprogramme, die sanktionierten, strukturierten Zugang innerhalb von LinkedIns Bedingungen bieten. Wenn Ihr Projekt Daten auf Mitgliederebene benötigt, ist dieser Weg oder eine formelle Datenvereinbarung die Antwort, kein ausgefeilterer Scraper. Wenn Sie unsicher über Ihren spezifischen Anwendungsfall sind, holen Sie sich Rat von einem qualifizierten Anwalt. Mehr zum allgemeinen Ansatz öffentlicher Daten finden Sie in unserem Überblick zu LinkedIn scrapen.
Wichtigste Erkenntnisse
- Asynchron schlägt synchron im großen Maßstab. URLs an den Crawler zu senden gibt in Sekunden eine RID zurück; das langsame Rendering findet außerhalb Ihres Rechners statt und Ergebnisse kommen an, wenn sie fertig sind.
- Der Callback-Server ist ein schlanker, gesicherter Empfänger. RID und beide Status-Header validieren, den GZIP-Body dekomprimieren, speichern und sofort zurückgeben, damit der Webhook nie blockiert.
-
Status in MySQL verfolgen. Die Tabelle
crawl_requestsführt jede Anfrage durchwaiting,receivedundprocessed, was dafür sorgt, dass Empfang und Verarbeitung entkoppelt bleiben. - Nur öffentliche, nicht personenbezogene Daten speichern. Schema und Prozessor halten beide an Unternehmensseiten- und öffentlichen Stellenangebots-Feldern fest, niemals Mitgliederprofile oder personenbezogene Daten.
- Für alles Reale den offiziellen Weg bevorzugen. LinkedIns Bedingungen schränken Scraping ein, und die meisten seiner Daten sind personenbezogen; verwenden Sie LinkedIns offizielle APIs und Partnerprogramme und respektieren Sie DSGVO und CCPA.
Häufig gestellte Fragen
Warum einen asynchronen Crawler statt eines synchronen Skripts verwenden?
Ein synchrones Skript blockiert bei jeder URL, während eine gesicherte Seite rendert und zurückkommt, sodass eine lange Liste größtenteils untätig läuft. Der asynchrone Crawler akzeptiert Ihre URL, gibt sofort eine Anfrage-ID zurück und erledigt das langsame Rendering und die Wiederholungsversuche auf seiner eigenen Infrastruktur und postet dann das fertige Ergebnis an Ihren Webhook. Einen großen Stapel zu senden dauert Sekunden, und Ergebnisse strömen zurück, wenn sie abgeschlossen sind, statt eine langsame Anfrage nach der anderen.
Was tut der Flask-Callback-Server eigentlich?
Er exponiert eine POST-Route, die der Crawler mit jedem fertigen Ergebnis aufruft. Der Handler liest den rid-Header, prüft, dass sowohl PC-Status als auch Original-Status 200 sind, bestätigt, dass die RID einer Anfrage noch mit Status waiting entspricht, dekomprimiert den GZIP-Body, speichert die Nutzlast auf der Festplatte und kippt die Anfrage auf received. Er gibt sofort zurück und blockiert nie, sodass er auch unter einem Schwall von Callbacks responsiv bleibt.
Warum Empfang und Verarbeitung in zwei Skripte aufteilen?
Damit ein langsamer Datenbankschreibvorgang den Webhook nie aufhält. Die einzige Aufgabe des Callback-Servers ist es, schnell zu empfangen und zu speichern. Ein separater geplanter Prozessor liest gespeicherte Nutzlasten in kleinen Stapeln, extrahiert die öffentlichen Felder, schreibt die strukturierten Zeilen und markiert jede Anfrage als processed. Das Entkoppeln der beiden lässt das System ein großes Volumen von Callbacks ohne Gegendruck auf einer Seite absorbieren.
Brauche ich das JavaScript-Token oder das normale Token?
Das normale Anfrage-Token (TCP). LinkedIn wird vom Normal-Anfrage-Crawler bedient, sodass Sie diesen Crawler-Typ im Dashboard auswählen und Ihr TCP-Token in settings.yml verwenden. Der asynchrone Crawler übernimmt trotzdem Rotation und Wiederholungsversuche im Hintergrund; der Token-Typ teilt ihm nur mit, welchen Anfragepfad er für das Ziel verwenden soll.
Wie halte ich den Webhook sicher?
Akzeptieren Sie nur POST-Anfragen, verlangen Sie ein geheimes Token in einem benutzerdefinierten Header oder URL-Parameter, das Sie bei jedem Aufruf verifizieren, und bestätigen Sie, dass die erwarteten Header rid, PC-Status und Original-Status vorhanden sind, bevor Sie einer Nutzlast vertrauen. Vermeiden Sie IP-Allowlisting, da die Quelladressen rotieren und sich ohne Vorankündigung ändern können. Die Status- und RID-Prüfungen im Beispiel sind ein Ausgangspunkt, nicht die ganze Geschichte.
Ist es sicher, LinkedIn-Daten auf diese Weise zu speichern?
Nur wenn Sie sich auf öffentliche, nicht personenbezogene Daten beschränken, wie es dieses Tutorial tut: Unternehmensnamen, Branchen, öffentliche Beschreibungen und öffentlichen Stellenangebots-Text. Das Speichern von Mitgliederprofilen, Namen, Verbindungen oder anderen personenbezogenen Daten zieht LinkedIns Nutzungsbedingungen und Gesetze wie DSGVO und CCPA in Mitleidenschaft und liegt außerhalb des Geltungsbereichs hier. Für Mitglieder-Ebene oder kommerzielle Nutzung verwenden Sie LinkedIns offizielle APIs und Partnerprogramme statt eines Scrapers.
Crawlen Sie jede Website im großen Maßstab, ohne gegen die Infrastruktur zu kämpfen.
Crawlbase übernimmt Proxys, Fingerprints und CAPTCHAs, damit Ihr Team Datenpipelines ausliefert, statt Crawl-Infrastruktur zu pflegen. 1.000 Anfragen kostenlos, keine Karte erforderlich.

