Avviso importante: Questo articolo ha esclusivamente scopo educativo e non costituisce consulenza legale. Lo scraping di dati pubblici si colloca in un'area grigia dal punto di vista legale. Prima di intraprendere qualsiasi attività di scraping, consultate un avvocato specializzato in diritto tecnologico e rispettate i Termini di Servizio di LinkedIn, il robots.txt, e le leggi applicabili come il CFAA (Computer Fraud and Abuse Act) negli Stati Uniti e il GDPR nell'Unione Europea.
Il Contesto Legale: Cosa Significa il Caso hiQ Labs v. LinkedIn
Prima di addentrarci negli aspetti tecnici, è fondamentale comprendere il quadro normativo. Nel 2017, LinkedIn ha citato in giudizio hiQ Labs, una startup che raccoglieva dati pubblici dai profili LinkedIn per analisi predittive sui dipendenti.
La controversia ha portato a due decisioni giudiziarie significative:
- 2019 (Nona Circuito): La corte ha stabilito che l'accesso a dati pubblicamente accessibili senza aggirare misure di autenticazione non viola il CFAA.
- 2022 (Corte Suprema): Rimandata alla corte d'appello con la direttiva di applicare un'interpretazione più restrittiva del CFAA.
- 2024 (Nona Circuito): La corte ha confermato che hiQ non ha violato il CFAA perché i dati erano pubblicamente accessibili, ma ha anche riconosciuto che LinkedIn può vietare l'accesso tramite misure tecniche.
Punto chiave: Lo scraping di dati pubblicamente accessibili senza autenticazione è legalmente diverso dall'accesso a dati dietro login. Tuttavia, LinkedIn può implementare misure tecniche per bloccare scraper, e aggirare tali misure potrebbe costituire violazione del CFAA.
Questo significa che dovete limitarvi strettamente ai dati accessibili senza account LinkedIn e senza superare barrieri tecnici progettate per bloccare l'accesso automatizzato.
Quali Dati LinkedIn Sono Pubblicamente Accessibili
LinkedIn distingue tra dati pubblici e privati. Solo una parte limitata dei profili è visibile ai visitatori non autenticati:
1. Profili Pubblici (linkedin.com/in/username)
Quando un utente imposta il proprio profilo come "pubblico", alcune informazioni diventano accessibili senza login:
- Nome e foto profilo
- Titolo professionale attuale
- Posizione attuale e azienda
- Formazione (in alcuni casi)
- Numero di connessioni (approssimativo)
Non sono accessibili senza login:
- Elenco completo delle esperienze lavorative
- Competenze e endorsement
- Recommendazioni
- Attività e post
- Dettagli di contatto (a meno che non siano stati resi pubblici dall'utente)
2. Pagine Aziendali Pubbliche (linkedin.com/company/nome-azienda)
Le pagine aziendali offrono dati generalmente accessibili:
- Nome azienda e logo
- Dimensioni (range, non numero esatto)
- Settore
- Descrizione aziendale
- Sede principale
- Tipologia di azienda
- Pagina delle carriere (se attiva)
3. Annunci di Lavoro Pubblici (linkedin.com/jobs/)
Gli annunci di lavoro sono la categoria più accessibile per lo scraping:
- Titolo della posizione
- Azienda e località
- Descrizione completa
- Requisiti e competenze
- Tipo di impiego (full-time, part-time, contratto)
- Livello di esperienza
Perché i Proxy Residenziali Sono Essenziali per LinkedIn
LinkedIn implementa uno dei sistemi anti-bot più sofisticati del web. Comprendere perché i proxy residenziali sono necessari richiede una comprensione delle tecniche di rilevamento utilizzate.
Il Problema degli IP Datacenter
Gli IP datacenter sono facilmente identificabili perché:
- Appartengono a blocchi ASN registrati come hosting provider (AWS, Google Cloud, DigitalOcean)
- Hanno pattern di traffico anomali (richieste concentrate, orari non umani)
- Non hanno storia di navigazione "normale"
LinkedIn mantiene liste di IP datacenter note e applica limiti molto più restrittivi:
| Tipo di IP | Limite Richieste (stimato) | Rischio Blocco |
|---|---|---|
| Datacenter | 10-50 richieste/giorno | Altissimo |
| Residenziale | 200-500 richieste/giorno | Moderato |
| Mobile | 300-600 richieste/giorno | Basso |
Fingerprinting del Browser
LinkedIn non si limita agli IP. Il sistema analizza:
- User-Agent: Deve corrispondere a browser reali e recenti
- TLS fingerprint: La "firma" della connessione HTTPS deve sembrare autentica
- Canvas fingerprint: Rendering canvas unico per ogni dispositivo
- WebGL: Informazioni sulla GPU e driver grafici
- Comportamento mouse/tastiera: Pattern di movimento e timing
- Cookie e localStorage: Presenza di cookie "normali" da navigazione precedente
Per questo motivo, strumenti come Playwright o Puppeteer con contesti browser persistenti sono preferibili a semplici richieste HTTP.
Perché i Proxy Residenziali ai ProxyHat
I proxy residenziali utilizzano IP assegnati a vere connessioni domestiche tramite ISP. Questo significa:
- L'IP appare come un normale utente domestico
- L'ASN corrisponde a provider come Telecom Italia, Vodafone, Fastweb
- Il traffico è mescolato con quello di milioni di utenti legittimi
Configurazione base ProxyHat per LinkedIn:
# HTTP Proxy (porta 8080)
http://USERNAME:PASSWORD@gate.proxyhat.com:8080
# Con targeting geografico (USA per test)
http://user-country-US:PASSWORD@gate.proxyhat.com:8080
# SOCKS5 Proxy (porta 1080)
socks5://USERNAME:PASSWORD@gate.proxyhat.com:1080
Esempio Pratico: Python + Playwright con Proxy Residenziali
Ecco un esempio completo che implementa scraping responsabile con rate limiting e rotazione IP.
import asyncio
import random
import time
from playwright.async_api import async_playwright
class LinkedInPublicScraper:
def __init__(self, proxy_country="US"):
# Configurazione proxy ProxyHat
self.proxy_config = {
"server": "http://gate.proxyhat.com:8080",
"username": f"user-country-{proxy_country}",
"password": "YOUR_PASSWORD"
}
self.min_delay = 3 # secondi tra richieste
self.max_delay = 8
async def scrape_public_profile(self, profile_url: str) -> dict:
"""
Scrape un profilo LinkedIn pubblico (senza login).
NOTA: Funziona solo per profili impostati come pubblici.
"""
async with async_playwright() as p:
# Avvia browser con proxy e contesto realistico
browser = await p.chromium.launch(
proxy=self.proxy_config,
headless=True,
args=[
'--disable-blink-features=AutomationControlled',
'--disable-dev-shm-usage'
]
)
# Crea contesto browser persistente per cookie "naturali"
context = await browser.new_context(
viewport={'width': 1920, 'height': 1080},
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
locale='it-IT',
timezone_id='Europe/Rome'
)
page = await context.new_page()
# Simula navigazione umana - visita homepage prima
await page.goto('https://www.linkedin.com/', wait_until='networkidle')
await self._random_delay()
# Scroll naturale
await self._human_scroll(page)
# Ora visita il profilo target
await page.goto(profile_url, wait_until='networkidle')
await self._random_delay()
# Estrai dati pubblici
profile_data = await self._extract_public_profile(page)
await browser.close()
return profile_data
async def _extract_public_profile(self, page) -> dict:
"""Estrae solo dati visibili pubblicamente."""
data = {}
try:
# Nome (se visibile)
name_el = await page.query_selector('h1')
if name_el:
data['name'] = await name_el.inner_text()
# Titolo professionale
title_el = await page.query_selector('.text-body-medium')
if title_el:
data['title'] = await title_el.inner_text()
# Località
location_el = await page.query_selector('.pv-text-details__left-panel')
if location_el:
data['location'] = await location_el.inner_text()
except Exception as e:
data['error'] = str(e)
return data
async def _human_scroll(self, page):
"""Simula scrolling umano."""
for _ in range(random.randint(2, 5)):
await page.evaluate('window.scrollBy(0, window.innerHeight * 0.8)')
await asyncio.sleep(random.uniform(0.5, 1.5))
async def _random_delay(self):
"""Ritardo casuale per sembrare umano."""
delay = random.uniform(self.min_delay, self.max_delay)
await asyncio.sleep(delay)
# Utilizzo
async def main():
scraper = LinkedInPublicScraper(proxy_country="IT")
# Esempio profilo pubblico
profile = await scraper.scrape_public_profile(
'https://www.linkedin.com/in/some-public-profile/'
)
print(profile)
asyncio.run(main())
Rate Limiting Responsabile
Per evitare blocchi e rispettare il servizio:
import time
from collections import deque
class RateLimiter:
def __init__(self, requests_per_hour=30, requests_per_day=200):
self.hourly_limit = requests_per_hour
self.daily_limit = requests_per_day
self.hourly_requests = deque()
self.daily_requests = deque()
def wait_if_needed(self):
"""Attende se necessario per rispettare i limiti."""
now = time.time()
# Pulisci richieste vecchie
hour_ago = now - 3600
day_ago = now - 86400
while self.hourly_requests and self.hourly_requests[0] < hour_ago:
self.hourly_requests.popleft()
while self.daily_requests and self.daily_requests[0] < day_ago:
self.daily_requests.popleft()
# Controlla limiti
if len(self.hourly_requests) >= self.hourly_limit:
wait_time = 3600 - (now - self.hourly_requests[0]) + 60
print(f"Limite orario raggiunto. Attendo {wait_time:.0f} secondi.")
time.sleep(wait_time)
if len(self.daily_requests) >= self.daily_limit:
wait_time = 86400 - (now - self.daily_requests[0]) + 60
print(f"Limite giornaliero raggiunto. Attendo {wait_time:.0f} secondi.")
time.sleep(wait_time)
# Registra nuova richiesta
self.hourly_requests.append(time.time())
self.daily_requests.append(time.time())
Scraping degli Annunci di Lavoro LinkedIn
Gli annunci di lavoro rappresentano il caso d'uso più comune e accessibile per lo scraping LinkedIn. La struttura URL base è:
https://www.linkedin.com/jobs/search/?keywords={query}&location={location}
Parametri di Ricerca Principali
| Parametro | Descrizione | Esempio |
|---|---|---|
| keywords | Termini di ricerca | python developer |
| location | Località geografica | Milano, Italy |
| f_JT | Tipo di lavoro | F (Full-time), P (Part-time), C (Contratto) |
| f_E | Livello esperienza | 1 (Entry), 2 (Associate), 3 (Mid-Senior) |
| f_WRA | Remoto | true (solo remoto) |
| start | Paginazione | 0, 25, 50, 75... |
Esempio Node.js per Job Scraping
const { chromium } = require('playwright');
class LinkedInJobsScraper {
constructor(proxyConfig) {
this.proxyServer = `http://${proxyConfig.username}:${proxyConfig.password}@gate.proxyhat.com:8080`;
this.jobs = [];
}
async scrapeJobs(keywords, location, maxJobs = 100) {
const browser = await chromium.launch({
headless: true,
proxy: { server: this.proxyServer }
});
const context = await browser.newContext({
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
viewport: { width: 1920, height: 1080 }
});
const page = await context.newPage();
let start = 0;
while (this.jobs.length < maxJobs) {
const url = this.buildSearchUrl(keywords, location, start);
await page.goto(url, { waitUntil: 'networkidle' });
await this.randomDelay(3000, 6000);
const jobCards = await page.$$('[data-entity-urn]');
if (jobCards.length === 0) break;
for (const card of jobCards) {
if (this.jobs.length >= maxJobs) break;
const job = await this.extractJobData(card);
if (job) this.jobs.push(job);
}
start += 25; // LinkedIn mostra 25 risultati per pagina
await this.rotateIP(); // Cambia IP ogni pagina
}
await browser.close();
return this.jobs;
}
buildSearchUrl(keywords, location, start) {
const params = new URLSearchParams({
keywords: keywords,
location: location,
start: start.toString()
});
return `https://www.linkedin.com/jobs/search/?${params}`;
}
async extractJobData(card) {
try {
return {
title: await card.$eval('h3', el => el.innerText).catch(() => null),
company: await card.$eval('h4', el => el.innerText).catch(() => null),
location: await card.$eval('.job-search-card__location', el => el.innerText).catch(() => null),
url: await card.$eval('a', el => el.href).catch(() => null)
};
} catch {
return null;
}
}
async randomDelay(min, max) {
const delay = Math.random() * (max - min) + min;
await new Promise(r => setTimeout(r, delay));
}
async rotateIP() {
// Con ProxyHat, ogni nuova connessione può usare IP diverso
// implementando rotazione tramite sessioni
console.log('Rotazione IP consigliata per prossima richiesta');
}
}
// Utilizzo
const scraper = new LinkedInJobsScraper({
username: 'user-country-IT',
password: 'YOUR_PASSWORD'
});
scraper.scrapeJobs('software engineer', 'Milano', 50)
.then(jobs => console.log(`Trovati ${jobs.length} annunci`));
Quando NON Effettuare Scraping: Limiti Etici e Tecnici
Esistono categorie di dati che non dovrebbero mai essere oggetto di scraping:
1. Dati dietro Autenticazione
Qualsiasi dato visibile solo dopo il login è off-limits:
- Profilo completo dei membri (esperienze, competenze, raccomandazioni)
- Connessioni e network
- Messaggi e InMail
- Notifiche e feed personalizzato
Ragionamento legale: Il CFAA si applica all'accesso non autorizzato a sistemi protetti. Se un dato richiede autenticazione, accedervi tramite scraping (anche con credenziali proprie) potrebbe violare i Termini di Servizio.
2. Sales Navigator e Recruiter Lite
Questi prodotti premium hanno dati aggiuntivi protetti da:
- Paywall e autenticazione specifica
- Termini di servizio separati che vietano esplicitamente lo scraping
- Misure anti-bot più aggressive
Lo scraping di Sales Navigator è particolarmente rischioso dal punto di vista legale perché implica l'aggiramento di misure di protezione a pagamento.
3. Dati Personali Sensibili
Anche se tecnicamente accessibili, alcuni dati non dovrebbero essere raccolti:
- Indirizzi email privati
- Numeri di telefono
- Indirizzi fisici
- Informazioni su minori
Il GDPR nell'UE richiede consenso esplicito per il trattamento di dati personali. Lo scraping automatico non può ottenere tale consenso.
4. Dati di Terze Parti
LinkedIn ospita contenuti di terze parti:
- Post e articoli degli utenti
- Commenti e interazioni
- Contenuti multimediali
Questi contenuti sono protetti da copyright e il loro scraping potrebbe violare i diritti d'autore.
Alternative: Le API Ufficiali LinkedIn
Per molti casi d'uso commerciali, le API ufficiali sono l'opzione più sicura e sostenibile:
LinkedIn Marketing API
Per gestire campagne pubblicitarie e accedere a dati aggregati:
- Analytics delle pagine aziendali
- Dati demografici del pubblico
- Metriche di engagement
Requisiti: Account advertiser attivo, processo di approvazione.
LinkedIn Recruiter System Connect
Per integrare sistemi ATS con Recruiter:
- Sincronizzazione candidati
- Gestione pipeline
- Reporting
Requisiti: Licenza Recruiter o Recruiter Lite.
LinkedIn Share API
Per pubblicare contenuti su pagine aziendali:
- Post automatici
- Condivisione articoli
- Analytics di base
Requisiti: Applicazione approvata, permessi specifici.
Confronto: API vs Scraping
| Aspetto | API Ufficiali | Scraping (solo pubblico) |
|---|---|---|
| Stabilità | Alta (contratti SLA) | Bassa (HTML cambia) |
| Rischio legale | Nessuno | Presente |
| Costo | Da gratuito a costoso | Proxy + sviluppo |
| Dati disponibili | Limitati ma garantiti | Pubblici non garantiti |
| Rate limits | Documentati | Imprevisti |
| Supporto | Ufficiale | Nessuno |
Best Practices per lo Scraping Etico
- Rispetta robots.txt: Verifica sempre
https://www.linkedin.com/robots.txtprima di iniziare. - Implementa rate limiting conservativo: Massimo 30-50 richieste/ora per IP.
- Usa proxy residenziali: Gli IP datacenter vengono bloccati rapidamente.
- Simula comportamento umano: Delay casuali, scrolling naturale, cursori realistici.
- Limitati ai dati pubblici: Mai accedere a contenuti dietro login.
- Non aggirare CAPTCHA: Se appare un CAPTCHA, fermati.
- Documenta le tue attività: Conserva log per dimostrare intento legittimo.
- Considera l'impatto: Il tuo scraping non deve degradare il servizio per altri utenti.
Punti Chiave da Ricordare
Key Takeaways:
- Lo scraping di dati LinkedIn pubblicamente accessibili senza login è tecnicamente e potenzialmente legalmente diverso dall'accesso a dati privati.
- Il caso hiQ Labs v. LinkedIn non è un "carta bianca" per lo scraping indiscriminato - LinkedIn può bloccare scraper tecnicamente.
- I proxy residenziali sono essenziali perché LinkedIn ha sistemi avanzati di fingerprinting e blocca aggressivamente gli IP datacenter.
- Limitati rigorosamente a: profili pubblici, pagine aziendali, annunci di lavoro - mai dati dietro autenticazione.
- Per uso commerciale, valuta seriamente le API ufficiali LinkedIn - più costose ma legalmente sicure.
- Il GDPR si applica ai dati personali europei, indipendentemente da dove risiede lo scraper.
Conclusione
Lo scraping di dati pubblici LinkedIn può essere uno strumento valido per ricerca di mercato, analisi recruiting e intelligence competitiva, ma richiede un approccio estremamente cauto. La linea tra accesso legittimo a dati pubblici e violazione del CFAA è sottile e in evoluzione.
Prima di implementare qualsiasi soluzione di scraping:
- Consultate un avvocato specializzato in diritto tecnologico
- Valutate se le API ufficiali soddisfano le vostre esigenze
- Se procedete con lo scraping, limitatevi rigorosamente ai dati pubblici
- Implementate rate limiting conservativo e proxy residenziali di qualità
- Monitorate costantemente per rilevare blocchi e adattarvi
Per proxy residenziali affidabili con rotazione automatica e targeting geografico, visitate ProxyHat Pricing per scoprire i piani disponibili. I nostri proxy residenziali sono ottimizzati per piattaforme con anti-bot avanzati come LinkedIn.






