Quand vous crawlez en volume, la partie la plus lente du travail est l'attente. Un scraper synchrone envoie une requête, bloque jusqu'au retour d'une page fortement défendue rendue, l'analyse, et seulement alors passe à l'URL suivante. Empilez des milliers d'URL derrière cela et vous passez la plupart de votre temps d'exécution à l'arrêt. Le pattern asynchrone renverse cela : vous confiez un lot d'URL à un crawler, il effectue le travail de rendu lent sur sa propre infrastructure, et il poste chaque résultat terminé à un webhook que vous contrôlez. Ce guide construit exactement cela, un serveur de callback Flask qui reçoit les résultats du Crawler Crawlbase asynchrone et les stocke dans MySQL.
L'exemple de cible est LinkedIn, mais lisez d'abord la mise en garde directement ci-dessous, car ce que vous stockez importe plus que la façon dont vous le stockez. Ce tutoriel est délibérément limité aux données publiques et non personnelles : champs de pages entreprise et texte d'offres d'emploi publiques, jamais les profils de membres individuels. La vraie valeur pédagogique ici est l'architecture, crawl asynchrone plus serveur de callback plus base de données, et ce pattern fonctionne de la même manière quelle que soit la source publique sur laquelle vous le pointez.
L'Accord utilisateur de LinkedIn restreint fortement l'accès automatisé, et la plupart des données LinkedIn sont des données personnelles sensibles concernant des personnes identifiables. Ce tutoriel collecte uniquement des champs PUBLICS et non personnels (nom de l'entreprise, description publique de l'entreprise, texte d'offre d'emploi publique), jamais les profils de membres, les connexions ou quoi que ce soit derrière une connexion. Si des données personnelles sont impliquées, le RGPD et le CCPA s'appliquent : vous avez besoin d'une base légale et devez honorer les demandes de suppression. Pour tout usage réel ou commercial, la voie sanctionnée est les API officielles de LinkedIn et les programmes partenaires, pas un scraper. Consultez la section légalité complète vers la fin avant de pointer cela sur quoi que ce soit de réel.
Ce que vous allez construire
Un petit système de crawling asynchrone en Python avec trois éléments mobiles et une base de données MySQL partagée. Au lieu d'un script bloquant, le travail est divisé pour que le crawl lent se produise hors de votre machine et que les résultats arrivent au fur et à mesure qu'ils se terminent :
- Un script de requête async Crawler qui pousse une liste d'URL publiques vers le Crawler asynchrone Crawlbase et enregistre un identifiant de requête (RID) pour chacune.
- Un serveur de callback Flask qui reçoit chaque crawl terminé sous forme de HTTP POST, le décompresse et sauvegarde le payload brut.
- Un processeur qui lit les payloads sauvegardés selon un calendrier, extrait les champs publics et écrit des lignes structurées.
- Un schéma MySQL avec une table de suivi des requêtes et des tables pour les champs publics que vous conservez.
Remarquez ce qui est délibérément absent du schéma : pas de noms de personnes, pas de titres, pas de résumés de profils individuels, pas de données de connexions. Nous stockons l'identité publique d'une entreprise et ses offres publiques, qui sont des informations impersonnelles qu'une entreprise publie sur elle-même.
Pourquoi asynchrone, et pourquoi un serveur de callback
Une page entreprise LinkedIn ou une offre d'emploi publique se rend côté client et est protégée par des défenses anti-bot agressives, donc une seule récupération est lente et souvent challengée. Faire cela de manière synchrone pour une longue liste d'URL signifie que votre script bloque sur chacune à tour de rôle. Le Crawler async accepte votre URL, retourne immédiatement avec un identifiant de requête, puis effectue le rendu lent et les nouvelles tentatives sur sa propre infrastructure. Quand il obtient finalement une réponse propre, il pousse ce résultat à votre webhook avec un POST.
Ce modèle push est pourquoi vous avez besoin d'un serveur de callback. Votre endpoint ne fait pas de polling et n'attend pas ; il se tient simplement prêt, et chaque résultat arrive quand il est terminé, dans l'ordre où les crawls se complètent. Le moteur du Crawler envoie le corps compressé en gzip, votre endpoint doit donc décompresser avant de pouvoir lire quoi que ce soit. Découpler la requête, la réception et le traitement en trois scripts est ce qui permet au système d'absorber un grand lot sans qu'aucune étape ne bloque les autres. Si vous voulez plus de contexte sur le moteur lui-même, consultez notre guide sur comment extraire des données à l'aide du Crawlbase Crawler.
Prérequis
Quelques éléments à avoir en place. Aucun ne prend longtemps.
Python 3.8 ou ultérieur. Vérifiez avec python3 --version. Si vous ne l'avez pas, installez-le depuis python.org.
MySQL 8. Un serveur MySQL en cours d'exécution auquel vous pouvez vous connecter localement. Le manuel d'installation officiel couvre toutes les plateformes.
Un compte Crawlbase et un token Normal (TCP). Inscrivez-vous, ouvrez votre tableau de bord et copiez votre token. LinkedIn est servi par le Crawler de requête Normal, utilisez donc le token TCP ici, pas le token JavaScript. Traitez le token comme un mot de passe et gardez-le hors du contrôle de version.
Un moyen d'exposer localhost. Le Crawler poste vers une URL publique, donc pendant le développement vous avez besoin d'un tunnel tel que ngrok pour atteindre votre application Flask locale.
Configurer le projet
Créez un environnement virtuel isolé, puis installez les bibliothèques dont le système a besoin.
python3 -m venv .venv source .venv/bin/activate pip install Flask mysql-connector-python pyyaml requests SQLAlchemy
Sur Windows, activez avec .venv\Scripts\activate à la place de la ligne source. Quatre bibliothèques font le travail : Flask est le serveur webhook, SQLAlchemy avec mysql-connector-python gère la base de données, requests envoie les requêtes de crawl, et pyyaml lit votre token depuis un fichier de paramètres. Créez un settings.yml à côté de vos scripts pour contenir le token et le nom de votre Crawler.
token: YOUR_CRAWLBASE_TOKEN crawler: linkedin-public-crawler
Étape 1 : Concevoir le schéma MySQL
Le schéma a deux rôles : suivre chaque requête de crawl tout au long de son cycle de vie, et contenir les champs publics que vous conservez. Créez un utilisateur, une base de données et les tables. Exécutez ces commandes dans le client en ligne de commande MySQL.
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;
Maintenant les tables. La table crawl_requests est la table de contrôle de tout le processus asynchrone : chaque URL que vous poussez obtient une ligne, suivie par son status au fur et à mesure qu'elle passe de waiting à received puis processed. La colonne crawlbase_rid relie une ligne à l'identifiant de requête que le Crawler retourne, qui est la seule clé dont vous disposez pour faire correspondre un callback entrant à la requête qui l'a déclenché.
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`);
Les tables de destination contiennent uniquement des données d'entreprise publiques et non personnelles. Une ligne par page entreprise, plus une table enfant pour les offres d'emploi publiques que cette page référence. Il n'y a nulle part de colonne pour le nom, le titre ou le texte de profil d'une personne. C'est la frontière de confidentialité rendue concrète dans le schéma lui-même.
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`) );
Étape 2 : Définir l'ORM
Mappez ces tables sur des classes Python avec SQLAlchemy pour que le reste du code travaille avec des objets, pas du SQL brut. Enregistrez cela sous lib/database.py. Les classes reflètent exactement le schéma : un CrawlRequest pour le suivi, un CompanyPage pour les champs publics d'entreprise, et un enfant JobPosting pour chaque offre publique.
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 retourne une session que chaque autre script importe. La chaîne de connexion porte l'utilisateur, le mot de passe, l'hôte et la base de données que vous avez configurés à l'étape 1 ; modifiez-les ici si les vôtres diffèrent.
Étape 3 : Pousser des URL vers le Crawler async
Ce script lit une liste d'URL publiques, envoie chacune au Crawler async et enregistre le RID retourné avec un statut waiting. Les paramètres clés sont callback=true, qui indique au Crawler de POSTer les résultats en retour au lieu de les retourner en ligne, et crawler=, qui nomme le Crawler que vous créerez dans le tableau de bord. Enregistrez-le sous crawl.py, et mettez vos URL d'entreprises publiques et d'offres d'emploi une par ligne dans urls.txt.
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.')
Chaque appel retourne un petit corps JSON comme {"rid": 12341234}. Le script stocke ce RID dans crawl_requests avec le statut waiting et passe directement à l'URL suivante sans bloquer sur le crawl réel. Le paramètre autoparse=true demande au Crawler de retourner des champs structurés plutôt que du HTML brut, ce que le processeur à l'étape 6 lit. C'est tout le point du modèle async : pousser cent URL prend des secondes, et le travail lent se passe ailleurs.
Le push que vous venez d'écrire retourne un RID en secondes parce que la partie lente, rendre une page LinkedIn défendue derrière une IP résidentielle de confiance et réessayer jusqu'à obtenir un 200 propre, se passe sur l'infrastructure Crawlbase, pas la vôtre. Le Crawler async met votre lot en file d'attente, effectue le rendu et la rotation côté serveur, et poste chaque résultat terminé à votre webhook, vous n'avez donc jamais à gérer une flotte de navigateurs sans interface ou un pool de proxies. Commencez sur le niveau gratuit.
Étape 4 : Construire le serveur de callback Flask
C'est le cœur du système. Le Crawler poste chaque résultat terminé vers une seule route. Votre rôle est de valider la requête, décompresser le corps et sauvegarder le payload pour que le processeur puisse le récupérer. Le Crawler envoie le RID dans un en-tête nommé rid, et il envoie deux en-têtes de statut, PC-Status (le statut Crawlbase) et Original-Status (le statut du site cible). Vous ne conservez que les résultats où les deux sont 200. Enregistrez cela sous 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)
Parcourez les gardes, car chacune compte. Un rid manquant signifie que la requête ne vient pas du Crawler, elle est donc abandonnée. Un RID dummyrequest est le ping de test que la plateforme envoie pour confirmer que votre endpoint est accessible ; vous l'enregistrez et retournez tôt. La vérification de statut ignore tout ce qui n'est pas un 200 propre des deux côtés. Vous recherchez ensuite le RID dans crawl_requests avec le statut waiting : si aucune ligne de ce type n'existe, le callback ne correspond pas à une requête que vous avez effectuée et il est ignoré. Seulement après tout cela, vous décompressez et sauvegardez le corps, puis basculez la ligne à received. L'endpoint ne bloque jamais ; il écrit le fichier et retourne immédiatement, ce qui le maintient réactif même sous un déluge de callbacks.
Votre URL de callback est publique pendant que le tunnel est ouvert. Renforcez-la : acceptez seulement POST, exigez un token secret dans un en-tête personnalisé ou un paramètre URL que vous vérifiez sur chaque requête, et confirmez que les en-têtes rid, PC-Status et Original-Status attendus sont présents. Évitez la liste blanche d'IP, car les adresses source tournent et peuvent changer sans préavis.
Étape 5 : Exposer le serveur et enregistrer le Crawler
Le Crawler a besoin d'une URL publique vers laquelle poster. Avec l'application Flask tournant sur le port 5000, ouvrez un tunnel.
python callback_server.py ngrok http 5000
ngrok affiche une URL HTTPS publique. Votre route de callback complète est cette URL plus le chemin, par exemple https://your-subdomain.ngrok.io/crawlbase_crawler_callback. Confirmez que l'endpoint est actif avec un ping de test avant d'impliquer le Crawler.
curl -i -X POST 'http://localhost:5000/crawlbase_crawler_callback' \ -H 'rid: dummyrequest' \ -H 'Content-Type: gzip/json' \ -H 'Content-Encoding: gzip'
Vous devriez voir Callback server is working dans le journal Flask. Allez maintenant dans votre tableau de bord Crawlbase, ouvrez la page Créer un Crawler, donnez au Crawler le même nom que vous avez mis dans settings.yml, et collez votre URL de callback ngrok complète. LinkedIn est servi par le Crawler de requête Normal (TCP), donc sélectionnez ce type. Une fois enregistré, le Crawler sait où pousser les résultats.
Étape 6 : Traiter les payloads reçus en lignes structurées
Le serveur de callback ne sauvegarde que les payloads bruts. Un processeur séparé tourne selon un calendrier, récupère tout ce qui est en statut received, extrait les champs publics, écrit les lignes structurées et marque la requête processed. Séparer la réception du traitement signifie qu'une écriture lente en base de données ne bloque jamais le webhook. Enregistrez cela sous 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()
Le processeur lit uniquement les champs d'entreprise et d'emploi impersonnels du payload analysé : le nom de l'entreprise, le secteur, la description publique, et le titre, la localisation et la description de chaque offre publique. Il ne touche jamais à aucun champ de niveau personnel, même si l'un d'eux se trouvait dans le payload. Maintenir la liste d'extraction aussi restreinte est la deuxième moitié de la frontière de confidentialité, après le schéma. La boucle sched re-exécute process toutes les 60 secondes et traite au plus dix requêtes par passage, ce qui maintient la mémoire stable sous un grand arriéré.
Exécuter le pipeline complet
Avec le Crawler enregistré, exécutez les trois pièces, chacune dans son propre terminal avec l'environnement virtuel actif. L'ordre compte : le serveur de callback et le processeur doivent être en place avant que vous poussiez des requêtes, sinon les premiers callbacks arrivent sans endroit où atterrir.
# 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
Pendant que crawl.py s'exécute, des lignes apparaissent dans crawl_requests avec le statut waiting. Quelques minutes plus tard, au fur et à mesure que le Crawler termine chaque page, le serveur de callback les bascule à received et écrit un fichier JSON sous ./data. À son prochain passage, le processeur lit ces fichiers, peuple company_pages et company_job_postings, et marque les requêtes processed. Vous pouvez observer cela en direct depuis l'onglet de surveillance du Crawler dans le tableau de bord, qui affiche l'état de chaque requête en temps réel.
À quoi ressemblent les données stockées
Après une exécution complète, les tables de destination contiennent des enregistrements d'entreprise propres et impersonnels. Une seule page entreprise traitée ressemble à ceci quand vous la relisez en JSON.
{ "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." } ] }
Chaque champ là-dedans est quelque chose que l'entreprise publie sur elle-même. Il n'y a aucune personne, aucun contact, aucun profil. C'est délibéré, et c'est ce qui rend le jeu de données défendable.
Passer à l'échelle et envoyer du contexte supplémentaire
L'architecture passe à l'échelle sans changement structurel : un urls.txt plus grand signifie plus de lignes waiting, le Crawler absorbe la file d'attente, et les callbacks arrivent au fur et à mesure que les crawls se terminent. Pour garder les payloads associés à votre propre contexte, attachez des données avec le paramètre callback_headers quand vous poussez une requête. Le Crawler répercute ces en-têtes sur le callback, vous pouvez donc transporter, par exemple, un identifiant de lot sans le stocker dans l'URL.
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
Côté réception, lisez-les comme des en-têtes de requête ordinaires : request.headers.get('BATCH-ID'). Pour plus de détails sur le maintien de grandes exécutions saines contre des cibles défendues, consultez nos guides sur comment scraper des sites web sans être bloqué et sur la construction d'un pipeline de données web évolutif.
Est-il légal de scraper LinkedIn ?
C'est la section à régler avant d'écrire du code de production, pas après. L'Accord utilisateur de LinkedIn et sa politique sur les Logiciels et Extensions Interdits interdisent expressément le scraping et la collecte automatisée de données, et LinkedIn fait respecter ces conditions. Cette position reste valable quelle que soit la prudence de vos outils. Le code dans ce guide fait fonctionner la partie technique ; il ne rend pas le scraping de LinkedIn conforme aux conditions de LinkedIn. Lisez l'Accord utilisateur et le robots.txt de LinkedIn, et traitez les deux comme la limite de ce que vous faites.
La dimension des données est tout aussi importante. La plupart du contenu LinkedIn est des données personnelles concernant des personnes identifiables : noms, historiques professionnels, titres, connexions et publications. Sous le RGPD en Europe et le CCPA en Californie, le traitement de données personnelles nécessite une base légale, et les personnes ont des droits, notamment le droit à la suppression de leurs données. Il existe aussi une jurisprudence réelle : dans hiQ Labs c. LinkedIn, les tribunaux américains ont examiné le scraping de profils publics sous l'angle du Computer Fraud and Abuse Act, mais ce litige était limité, spécifique à une juridiction, et n'a pas sanctionné le scraping en général ni supplanté les conditions contractuelles de LinkedIn ou le droit à la protection des données. La légalité dépend des données, de la méthode, de la juridiction et des accords auxquels vous êtes lié, donc traitez avec méfiance les affirmations catégoriques selon lesquelles "public signifie jeu équitable".
C'est pourquoi ce tutoriel est cadré comme il l'est. Il ne stocke que des informations publiques et non personnelles d'entreprise : noms d'entreprises, secteurs, descriptions publiques et textes d'offres d'emploi publiques qu'une entreprise publie sur elle-même. Il ne construit jamais de profils d'individus, ne touche jamais à quoi que ce soit derrière une connexion, et ne collecte jamais de données personnelles de membres. Pour tout besoin réel ou commercial, la bonne voie est les API officielles de LinkedIn et les programmes partenaires, qui fournissent un accès structuré et sanctionné dans les conditions de LinkedIn. Si votre projet nécessite des données au niveau des membres, cette voie, ou un accord formel de données, est la réponse, pas un scraper. En cas de doute sur votre usage spécifique, obtenez conseil auprès d'un avocat qualifié. Pour plus d'informations sur l'approche des données publiques en général, consultez notre vue d'ensemble sur comment scraper LinkedIn.
Points clés
- L'async bat le synchrone en volume. Pousser des URL vers le Crawler retourne un RID en secondes ; le rendu lent se passe hors de votre machine et les résultats arrivent au fur et à mesure qu'ils se terminent.
- Le serveur de callback est un récepteur fin et gardé. Validez le RID et les deux en-têtes de statut, décompressez le corps gzip, sauvegardez-le et retournez immédiatement pour que le webhook ne bloque jamais.
-
Suivez l'état dans MySQL. La table
crawl_requestsfait avancer chaque requête dewaitingàreceivedpuisprocessed, ce qui permet de garder réception et traitement découplés. - Ne stockez que des données publiques et non personnelles. Le schéma et le processeur se limitent tous deux aux champs de pages entreprise et d'offres d'emploi publiques, jamais aux profils de membres ni aux données personnelles.
- Préférez la voie officielle pour tout usage réel. Les conditions de LinkedIn restreignent le scraping et la plupart de ses données sont personnelles ; utilisez les API officielles de LinkedIn et les programmes partenaires, et respectez le RGPD et le CCPA.
Foire aux questions
Pourquoi utiliser un crawler asynchrone plutôt qu'un script synchrone ?
Un script synchrone bloque sur chaque URL pendant qu'une page défendue se rend et retourne, donc une longue liste s'exécute principalement à l'arrêt. Le Crawler async accepte votre URL, retourne un identifiant de requête immédiatement, et effectue le rendu lent et les nouvelles tentatives sur sa propre infrastructure, puis poste le résultat terminé à votre webhook. Pousser un grand lot prend des secondes, et les résultats affluent au fur et à mesure qu'ils se complètent plutôt qu'une requête lente à la fois.
Que fait réellement le serveur de callback Flask ?
Il expose une route POST que le Crawler appelle avec chaque résultat terminé. Le gestionnaire lit l'en-tête rid, vérifie que PC-Status et Original-Status sont tous deux 200, confirme que le RID correspond à une requête encore en statut waiting, décompresse le corps gzip, sauvegarde le payload sur disque et bascule la requête à received. Il retourne immédiatement et ne bloque jamais, donc il reste réactif même sous une rafale de callbacks.
Pourquoi diviser la réception et le traitement en deux scripts ?
Pour qu'une écriture lente en base de données ne retienne jamais le webhook. Le seul rôle du serveur de callback est de recevoir et sauvegarder rapidement. Un processeur programmé séparé lit les payloads sauvegardés en petits lots, extrait les champs publics, écrit les lignes structurées et marque chaque requête processed. Découpler les deux permet au système d'absorber un grand volume de callbacks sans contre-pression d'aucun côté.
Ai-je besoin du token JavaScript ou du token Normal ?
Le token de requête Normal (TCP). LinkedIn est servi par le Crawler de requête Normal, donc vous sélectionnez ce type de Crawler dans le tableau de bord et utilisez votre token TCP dans settings.yml. Le Crawler async gère quand même rotation et nouvelles tentatives en coulisses ; le type de token indique simplement quel chemin de requête utiliser pour la cible.
Comment sécuriser le webhook ?
Acceptez uniquement les requêtes POST, exigez un token secret dans un en-tête personnalisé ou un paramètre URL que vous vérifiez sur chaque appel, et confirmez que les en-têtes rid, PC-Status et Original-Status attendus sont présents avant de faire confiance à un payload. Évitez la liste blanche d'IP, car les adresses source tournent et peuvent changer sans préavis. Les vérifications de statut et de RID dans l'exemple sont un point de départ, pas toute l'histoire.
Est-il sûr de stocker des données LinkedIn de cette manière ?
Seulement si vous vous en tenez à des données publiques et non personnelles, comme ce tutoriel le fait : noms d'entreprises, secteurs, descriptions publiques et textes d'offres d'emploi publiques. Stocker des profils de membres, des noms, des connexions ou d'autres données personnelles fait entrer en jeu l'Accord utilisateur de LinkedIn et des lois comme le RGPD et le CCPA, et dépasse le cadre de ce tutoriel. Pour un usage au niveau des membres ou commercial, utilisez les API officielles de LinkedIn et les programmes partenaires plutôt qu'un scraper.
Crawlez n'importe quel site à grande échelle, sans combattre l'infrastructure.
Crawlbase gère les proxies, les empreintes et les CAPTCHA afin que votre équipe livre des pipelines de données au lieu de maintenir la plomberie de crawl. 1 000 requêtes gratuites, sans carte requise.

