Uwaga prawna: Ten artykuł dotyczy wyłącznie dostępu do danych publicznie dostępnych bez logowania. Scrapowanie danych za ścianami logowania, z sesji uwierzytelnionych lub z LinkedIn Sales Navigator może naruszać Warunki Korzystania z Usługi (ToS) oraz obowiązujące przepisy, w tym CFAA w USA i RODO w UE. Niniejszy artykuł nie stanowi porady prawnej — przed podjęciem jakichkolwiek działań skonsultuj się z prawnikiem.
Wprowadzenie: Publiczne Dane LinkedIn i Kontekst Prawny
LinkedIn to największa na świecie platforma profesjonalnych profili, z ponad miliardem użytkowników. Dla zespołów rekrutacyjnych, firm badawczych i analityków rynku, dane te są cennym zasobem. Ale jak legalnie i etycznie uzyskać dostęp do publicznie dostępnych informacji?
W sprawie hiQ Labs v. LinkedIn (2022), Sąd Apelacyjny Dziewiątego Okręgu w USA orzekł, że scrapowanie danych publicznie dostępnych na stronie internetowej nie narusza Computer Fraud and Abuse Act (CFAA). Sąd uznał, że hiQ Labs, firma analityczna, mogła legalnie scrapować publiczne profile LinkedIn, ponieważ dane te były dostępne dla każdego bez logowania.
Kluczowe orzeczenie: Dane publicznie dostępne bez uwierzytelnienia mogą podlegać scrapowaniu w kontekście CFAA, ale to nie oznacza, że jest to zawsze dozwolone — platformy mogą nadal blokować dostęp i egzekwować swoje ToS.
W Unii Europejskiej sytuacja jest bardziej skomplikowana. RODO (GDPR) nakłada rygorystyczne wymagania na przetwarzanie danych osobowych, nawet jeśli są publicznie dostępne. Scrapowanie profili zawodowych może wymagać podstawy prawnej, takiej jak uzasadniony interes, ale należy przeprowadzić ocenę wpływu na ochronę danych.
Jakie Dane LinkedIn Są Publicznie Dostępne?
Bez logowania do konta LinkedIn, możesz uzyskać dostęp do ograniczonego, ale znaczącego zbioru danych:
1. Publiczne Profile Osobiste
LinkedIn pozwala użytkownikom wybrać, czy ich profil ma być publicznie dostępny przez wyszukiwarki. Gdy profil jest ustawiony jako publiczny, dostępne są:
- Imię i nazwisko
- Stanowisko i nazwa firmy
- Lokalizacja (miasto/kraj)
- Branża
- Podsumowanie zawodowe (jeśli ustawione jako publiczne)
- Historia zatrudnienia (częściowo)
URL publicznego profilu ma format: https://www.linkedin.com/in/{username}
2. Strony Firmowe
Strony firm (Company Pages) są w dużej mierze publiczne i zawierają:
- Nazwa firmy i logo
- Rozmiar firmy (zakres pracowników)
- Branża
- Siedziba główna
- Rok założenia
- Typ firmy (publiczna/prywatna)
- Specjalizacje
URL: https://www.linkedin.com/company/{company-name}
3. Publiczne Oferty Pracy
LinkedIn publikuje miliony ofert pracy, z których wiele jest dostępnych bez logowania:
- Tytuł stanowiska
- Nazwa firmy
- Lokalizacja
- Typ zatrudnienia (pełny etat, kontrakt, zdalna)
- Opis stanowiska
- Wymagania kwalifikacyjne
URL wyszukiwania: https://www.linkedin.com/jobs/search/
Dlaczego Proxy Residential Są Niezbędne dla LinkedIn
LinkedIn stosuje jedne z najbardziej zaawansowanych systemów ochrony przed botami w sieci. Oto dlaczego proxy residential są krytyczne:
Fingerprinting IP Datacenter
LinkedIn utrzymuje bazy danych adresów IP z datacenter (AWS, Google Cloud, DigitalOcean itp.) i automatycznie oznacza ruch z tych zakresów jako podejrzany. Proxy datacenter są blokowane niemal natychmiast po wykryciu wzorca scrapowania.
Limity per-IP
LinkedIn stosuje agresywne ograniczenia szybkości (rate limiting) na poziomie adresu IP:
- ~50-100 żądań na sesję przed CAPTCHA
- Automatyczne blokady po wykryciu wzorców botowych
- Trwałe blokady IP dla powtarzających się naruszeń
Weryfikacja zachowania przeglądarki
Poza IP, LinkedIn analizuje:
- Podpis przeglądarki (canvas, WebGL, czcionki)
- Zachowanie myszy i scrollowania
- Kolejność żądań i nagłówki HTTP
- Czas spędzony na stronie
Proxy residential zapewniają adresy IP z prawdziwych sieci domowych, co sprawia, że Twój ruch wygląda jak ruch zwykłych użytkowników. To niezbędne dla każdego projektu scrapowania LinkedIn na większą skalę.
Konfiguracja Proxy Residential ProxyHat
ProxyHat oferuje proxy residential z rotacją IP i targetingiem geograficznym. Oto jak skonfigurować połączenie:
Format URL HTTP proxy:
http://USERNAME:PASSWORD@gate.proxyhat.com:8080
Przykłady konfiguracji z targetingiem:
# Proxy z targetingiem krajowym (USA)
http://user-country-US:PASSWORD@gate.proxyhat.com:8080
# Proxy z targetingiem miejskim (Berlin, Niemcy)
http://user-country-DE-city-berlin:PASSWORD@gate.proxyhat.com:8080
# Proxy z sesją sticky (stabilne IP przez 10 minut)
http://user-session-abc123-country-US:PASSWORD@gate.proxyhat.com:8080
Dla SOCKS5 użyj portu 1080:
socks5://USERNAME:PASSWORD@gate.proxyhat.com:1080
Python + Playwright: Scrapowanie Profili LinkedIn
Poniżej kompletny przykład scrapowania publicznych profili z użyciem Playwright i proxy residential:
import asyncio
from playwright.async_api import async_playwright
import random
import time
class LinkedInScraper:
def __init__(self, proxy_url):
self.proxy_url = proxy_url
self.base_url = "https://www.linkedin.com/in/"
async def get_browser_context(self, playwright):
"""Tworzy kontekst przeglądarki z proxy residential."""
browser = await playwright.chromium.launch(
headless=True,
args=['--disable-blink-features=AutomationControlled']
)
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',
proxy={
'server': self.proxy_url
} if self.proxy_url else None,
locale='en-US',
timezone_id='America/New_York'
)
# Dodaj skrypty anty-fingerprinting
await context.add_init_script("""
Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
Object.defineProperty(navigator, 'plugins', {get: () => [1, 2, 3, 4, 5]});
Object.defineProperty(navigator, 'languages', {get: () => ['en-US', 'en']});
""")
return browser, context
async def human_like_delay(self, min_seconds=2, max_seconds=5):
"""Symuluje naturalne opóźnienie między akcjami."""
await asyncio.sleep(random.uniform(min_seconds, max_seconds))
async def scroll_page(self, page):
"""Symuluje naturalne przewijanie strony."""
for _ in range(random.randint(2, 4)):
scroll_amount = random.randint(300, 800)
await page.evaluate(f'window.scrollBy(0, {scroll_amount})')
await self.human_like_delay(0.5, 1.5)
async def scrape_profile(self, username):
"""Scrapuje publiczny profil LinkedIn."""
async with async_playwright() as playwright:
browser, context = await self.get_browser_context(playwright)
page = await context.new_page()
try:
url = f"{self.base_url}{username}"
print(f"Scrapowanie: {url}")
await page.goto(url, wait_until='networkidle', timeout=30000)
await self.human_like_delay(2, 4)
# Sprawdź czy profil istnieje i jest publiczny
if await page.locator('text=Page not found').count() > 0:
return None
await self.scroll_page(page)
# Ekstrakcja danych
profile_data = await page.evaluate("""
() => {
const getText = (selector) => {
const el = document.querySelector(selector);
return el ? el.innerText.trim() : null;
};
return {
name: getText('h1'),
headline: getText('.text-body-medium'),
location: getText('.inline-show-more-text--is-collapsed'),
company: getText('button[aria-label*="Current company"]') ||
getText('.inline-show-more-text--is-collapsed'),
};
}
""")
profile_data['username'] = username
profile_data['url'] = url
profile_data['scraped_at'] = time.strftime('%Y-%m-%d %H:%M:%S')
return profile_data
except Exception as e:
print(f"Błąd scrapowania {username}: {e}")
return None
finally:
await browser.close()
# Przykład użycia
async def main():
proxy_url = "http://user-country-US:PASSWORD@gate.proxyhat.com:8080"
scraper = LinkedInScraper(proxy_url)
usernames = ['johndoe', 'janedoe', 'example-user']
results = []
for username in usernames:
profile = await scraper.scrape_profile(username)
if profile:
results.append(profile)
print(f"Scrapowano: {profile['name']}")
# Krytyczne: Ograniczenie szybkości między profilami
await asyncio.sleep(random.uniform(10, 20))
return results
if __name__ == "__main__":
asyncio.run(main())
Kluczowe elementy tego kodu:
- Proxy residential: Adres IP wygląda jak zwykły użytkownik domowy
- Anty-fingerprinting: Ukrywa cechy automatyzacji
- Naturalne opóźnienia: Symuluje ludzkie zachowanie
- Scrollowanie: Wymagane przez LinkedIn dla pełnego ładowania
- Rate limiting: 10-20 sekund między profilami
Scrapowanie Ofert Pracy LinkedIn
Oferty pracy są bardziej dostępne i mniej chronione niż profile. Oto jak scrapować z endpointu /jobs/search/:
import asyncio
from playwright.async_api import async_playwright
import urllib.parse
class LinkedInJobScraper:
def __init__(self, proxy_url=None):
self.proxy_url = proxy_url
self.base_url = "https://www.linkedin.com/jobs/search/"
async def search_jobs(self, keywords, location, page_num=0):
"""Wyszukuje oferty pracy z filtrami."""
async with async_playwright() as playwright:
browser = await playwright.chromium.launch(headless=True)
context = await browser.new_context(
viewport={'width': 1920, 'height': 1080},
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
proxy={'server': self.proxy_url} if self.proxy_url else None
)
page = await context.new_page()
try:
# Parametry wyszukiwania
params = {
'keywords': keywords,
'location': location,
'start': page_num * 25 # 25 wyników na stronę
}
url = f"{self.base_url}?{urllib.parse.urlencode(params)}"
await page.goto(url, wait_until='networkidle', timeout=45000)
await asyncio.sleep(3)
# Poczekaj na załadowanie wyników
await page.wait_for_selector('.jobs-search__results-list', timeout=15000)
# Ekstrakcja ofert
jobs = await page.evaluate("""
() => {
const jobCards = document.querySelectorAll('.job-search-card');
return Array.from(jobCards).map(card => ({
title: card.querySelector('.base-search-card__title')?.innerText.trim(),
company: card.querySelector('.base-search-card__subtitle')?.innerText.trim(),
location: card.querySelector('.job-search-card__location')?.innerText.trim(),
url: card.querySelector('a')?.href,
posted: card.querySelector('time')?.getAttribute('datetime')
}));
}
""")
return jobs
except Exception as e:
print(f"Błąd: {e}")
return []
finally:
await browser.close()
# Przykład użycia
async def scrape_jobs_example():
proxy = "http://user-country-US:PASSWORD@gate.proxyhat.com:8080"
scraper = LinkedInJobScraper(proxy)
all_jobs = []
for page in range(3): # 3 strony = ~75 ofert
jobs = await scraper.search_jobs(
keywords="Python Developer",
location="Remote",
page_num=page
)
all_jobs.extend(jobs)
print(f"Strona {page + 1}: {len(jobs)} ofert")
await asyncio.sleep(random.uniform(8, 15))
return all_jobs
Filtry wyszukiwania ofert pracy:
LinkedIn obsługuje wiele filtrów w URL:
| Parametr | Opis | Przykład |
|---|---|---|
keywords | Słowa kluczowe | Python Developer |
location | Lokalizacja | Warsaw, Poland |
f_JT | Typ pracy | F (pełny), C (kontrakt) |
f_WT | Praca zdalna | 2 (hybryda), 3 (zdalna) |
f_E | Doświadczenie | 1 (entry), 2 (mid), 3 (senior) |
start | Paginacja | 0, 25, 50... |
Przykład Node.js: Scrapowanie Stron Firmowych
const { chromium } = require('playwright');
class LinkedInCompanyScraper {
constructor(proxyUrl) {
this.proxyUrl = proxyUrl;
this.baseUrl = 'https://www.linkedin.com/company/';
}
async scrapeCompany(companySlug) {
const browser = await chromium.launch({
headless: true,
proxy: this.proxyUrl ? { server: this.proxyUrl } : undefined
});
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 },
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
});
const page = await context.newPage();
try {
const url = `${this.baseUrl}${companySlug}/about/`;
console.log(`Scrapowanie: ${url}`);
await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
await page.waitForTimeout(3000);
// Sprawdź czy strona istnieje
const notFound = await page.locator('text=Page not found').count();
if (notFound > 0) {
return null;
}
// Ekstrakcja danych firmy
const companyData = await page.evaluate(() => {
const getText = (selector) => {
const el = document.querySelector(selector);
return el ? el.innerText.trim() : null;
};
const getDefinitionValue = (term) => {
const dt = Array.from(document.querySelectorAll('dt'))
.find(el => el.innerText.includes(term));
return dt ? dt.nextElementSibling?.innerText.trim() : null;
};
return {
name: getText('h1'),
description: getText('.break-words'),
website: getDefinitionValue('Website'),
industry: getDefinitionValue('Industry'),
companySize: getDefinitionValue('Company size'),
headquarters: getDefinitionValue('Headquarters'),
founded: getDefinitionValue('Founded'),
specialties: getDefinitionValue('Specialties')
};
});
companyData.slug = companySlug;
companyData.url = url;
return companyData;
} catch (error) {
console.error(`Błąd: ${error.message}`);
return null;
} finally {
await browser.close();
}
}
}
// Użycie
async function main() {
const proxyUrl = 'http://user-country-US:PASSWORD@gate.proxyhat.com:8080';
const scraper = new LinkedInCompanyScraper(proxyUrl);
const companies = ['microsoft', 'google', 'apple'];
for (const company of companies) {
const data = await scraper.scrapeCompany(company);
if (data) {
console.log(`${data.name}: ${data.companySize}`);
}
await new Promise(r => setTimeout(r, 10000 + Math.random() * 5000));
}
}
main();
Czego NIE Scrapować: Granice Etyczne i Prawne
Istnieją wyraźne granice, których przekroczenie może prowadzić do konsekwencji prawnych:
1. Dane za ścianą logowania
Wszystko, co wymaga zalogowania, jest chronione:
- Pełne profile z historią zatrudnienia
- Rekomendacje i umiejętności
- Połączenia i sieć kontaktów
- Aktywność i posty
Scrapowanie tych danych wymaga konta i sesji uwierzytelnionej, co wyraźnie narusza ToS LinkedIn.
2. LinkedIn Sales Navigator
Sales Navigator to płatna usługa z zaawansowanymi danymi. Scrapowanie z Sales Navigator jest:
- Wyraźnie zabronione w ToS
- Chronione dodatkowymi zabezpieczeniami
- Podlegające ściganiu cywilnemu
3. Dane prywatne użytkowników
Nawet jeśli dane są publiczne, niektóre informacje wymagają szczególnej ostrożności:
- Adresy e-mail (jeśli widoczne)
- Numery telefonów
- Dane wrażliwe (stan zdrowia, wyznanie, poglądy polityczne)
Pod RODO, przetwarzanie tych danych bez zgody może być niezgodne z prawem.
4. Scrapowanie na skalę masową
Nawet publiczne dane, scrapowane masowo, mogą stanowić problem:
- Obciążenie infrastruktury LinkedIn
- Potencjalne naruszenie praw autorskich do baz danych
- Ryzyko dla prywatności w agregacji
Alternatywy: Oficjalne API LinkedIn
LinkedIn oferuje oficjalne API dla określonych zastosowań:
| API | Przeznaczenie | Dostęp | Ograniczenia |
|---|---|---|---|
| LinkedIn Recruiter System Connect | Integracja z ATS | Partnerstwo | Wymaga umowy |
| Marketing API | Zarządzanie reklamami | Wnioskowanie | Ograniczone dane |
| Share API | Publikowanie treści | Otwarte | Tylko posting |
| Profile API | Weryfikacja tożsamości | Ograniczony | Zgoda użytkownika |
Dla większości zastosowań rekrutacyjnych, oficjalne API LinkedIn są ograniczone i wymagają partnerstwa. Dlatego wiele firm wybiera scrapowanie publicznych danych — ale z zachowaniem ostrożności.
Najlepsze Praktyki Rate Limiting
LinkedIn jest niezwykle wrażliwy na wzorce automatycznego ruchu. Oto strategie minimalizacji ryzyka:
Rotacja IP
Z proxy residential ProxyHat, każde żądanie może pochodzić z nowego IP:
# Rotacja IP przy każdym żądaniu (sticky session OFF)
http://user-country-US:PASSWORD@gate.proxyhat.com:8080
# Sticky session dla sesji wieloetapowych
http://user-session-myid123-country-US:PASSWORD@gate.proxyhat.com:8080
Limity szybkości
- Profile: 1 profil na 10-20 sekund
- Oferty pracy: 1 strona wyników na 8-15 sekund
- Firmy: 1 firma na 15-25 sekund
Godziny szczytu
Scrapuj poza godzinami pracy w strefie czasowej targetu (np. 2-5 AM czasu lokalnego), gdy LinkedIn ma mniejszy ruch organiczny.
Kluczowe Wnioski
- Scrapowanie publicznych danych LinkedIn jest możliwe, ale wymaga proxy residential i ostrożnego rate limitingu.
- Sprawa hiQ Labs v. LinkedIn sugeruje, że publiczne dane nie są chronione przez CFAA, ale ToS i RODO nadal obowiązują.
- Proxy datacenter są niemal natychmiast blokowane przez LinkedIn — residential są niezbędne.
- Nigdy nie scrapuj danych za ścianą logowania, z Sales Navigator, ani danych prywatnych użytkowników.
- Rozważ oficjalne API LinkedIn dla zastosowań komercyjnych.
- Zawsze konsultuj się z prawnikiem przed rozpoczęciem projektu scrapowania.
Kiedy Użyć Oficjalnego API zamiast Scrapowania
Scrapowanie nie zawsze jest właściwym rozwiązaniem. Rozważ oficjalne API gdy:
- Budujesz produkt komercyjny: Ryzyko prawne jest zbyt duże
- Potrzebujesz danych w czasie rzeczywistym: API jest stabilniejsze
- Przetwarzasz dane osobowe w UE: RODO wymaga podstawy prawnej
- Integrujesz się z ATS: LinkedIn Recruiter API jest do tego stworzony
- Tworzysz narzędzie reklamowe: Marketing API jest dostępne
Scrapowanie publicznych danych może być uzasadnione dla:
- Badania rynku i analizy trendów
- Analizy konkurencji
- Monitorowania ofert pracy
- Weryfikacji danych firmowych
Decyzja zależy od konkretnego zastosowania, jurysdykcji i tolerancji ryzyka.
ProxyHat dostarcza proxy residential zoptymalizowane pod scrapowanie LinkedIn z targetingiem w ponad 195 krajach. Zobacz nasze plany cenowe lub sprawdź dostępne lokalizacje.






