차단되지 않고 LinkedIn을 긁는 방법

차단되지 않고 LinkedIn을 긁는 방법

사용자 프로필
Pandada 게시일: 2025-11-21
0

LinkedIn blocks thousands of scraping attempts every single day. In 2024-2025, the platform has significantly upgraded its anti-bot detection systems, making it one of the most challenging websites to scrape at scale. This isn't just about avoiding a simple rate limit anymore—LinkedIn now employs AI-driven behavioral analysis, TLS fingerprinting, and sophisticated browser detection that can identify automation even when you think you've covered all your tracks.

This guide will reveal the technical architecture behind LinkedIn's defense system, walk you through three DIY solutions with actual working code, analyze their real-world performance, and introduce a professional alternative that handles all the complexity for you. Whether you're building a lead generation tool, conducting market research, or enriching your CRM data, you'll understand exactly what it takes to scrape LinkedIn successfully in 2025.

Understanding LinkedIn's Multi-Layer Anti-Scraping Defense System

Before attempting to scrape LinkedIn, you need to understand the sophisticated defense mechanisms you're facing. LinkedIn doesn't rely on a single protection method—instead, it employs a multi-layered approach where each layer catches what the previous ones might have missed. This architecture has evolved significantly in 2024-2025, incorporating machine learning models trained on millions of real user sessions.

Rate Limiting and Request Patterns

At the most basic level, LinkedIn monitors request frequency across different user types. Authenticated users typically get around 80-100 profile visits per hour, while unauthenticated requests are limited to roughly 40 page views per hour. Search queries face even stricter limits at about 20-30 searches per hour. These aren't just arbitrary numbers—they're calculated based on normal human browsing behavior.

What makes this interesting is how LinkedIn tracks these patterns. The system doesn't just count requests; it analyzes the rhythm and timing. A human browsing LinkedIn typically has irregular intervals between actions—maybe 3 seconds here, 15 seconds there, occasionally a minute when they're reading something interesting. A bot, even with random delays, often exhibits subtle patterns that statistical analysis can detect.

Real-World Test Results: In testing, exceeding these thresholds triggers HTTP 429 (Too Many Requests) errors within 2-5 minutes. More importantly, persistent violations lead to temporary IP bans lasting 6-24 hours, and repeated offenses can result in permanent account suspension.

Account Behavior and Trust Scoring

LinkedIn's AI systems continuously analyze behavioral patterns to build a trust score for each account and session. New accounts face much stricter scrutiny compared to established accounts that are older than six months. The platform tracks not just what you do, but how you do it—the depth of your interactions, how long you stay on each page, whether you scroll naturally, and even the patterns in your mouse movements.

Consider this real case study: A developer reported that their account was restricted after visiting 150 profiles in one hour, despite using a legitimate Chrome browser with no automation tools. LinkedIn's behavioral analysis detected that the profile visits were too systematic—each lasting approximately the same amount of time, with similar scrolling patterns, and no meaningful interactions like sending messages or reacting to posts. The AI recognized this as abnormal behavior, even though it technically came from a real browser.

IP Fingerprinting and Reputation Systems

LinkedIn maintains extensive IP reputation databases that go far beyond simple blacklists. The system tracks Autonomous System Numbers (ASN) to identify datacenter IP ranges, which are automatically flagged as high-risk. More sophisticated is the ability to distinguish between residential, datacenter, and mobile IPs based on their behavioral characteristics and routing patterns.

Geographic consistency plays a crucial role here. If your session shows you accessing LinkedIn from San Francisco in one minute and London the next, that's an immediate red flag. The system also analyzes whether your IP's geographic location matches the regional settings in your browser, the timezone in your requests, and even the language preferences you're sending in HTTP headers.

Test Results: In controlled testing, IP addresses from AWS, GCP, and DigitalOcean have a survival time of less than 5 minutes on LinkedIn before being blocked. Datacenter proxies show a 30-50% blocking rate on their very first request, regardless of how carefully you configure the rest of your scraper.

TLS Fingerprinting: The Silent Detector

This is where scraping gets technically sophisticated. Before your HTTP request even reaches LinkedIn's servers, your TLS handshake has already revealed whether you're using a real browser or an automation tool. Every HTTP client—whether it's Chrome, Firefox, Python's requests library, or curl—has a unique TLS "fingerprint" based on how it negotiates the encrypted connection.

The JA3 fingerprinting technique analyzes parameters from your ClientHello packet: the TLS version, ordered list of cipher suites, extensions, elliptic curves, and elliptic curve formats. These create a hash that's remarkably consistent for each client type. Python's requests library, for instance, produces the hash 579ccef312d18482fc42e2b822ca2430, while a real Chrome browser generates 773906b0efdefa24a7f2b8eb6985bf37. LinkedIn knows these signatures and can instantly identify automation tools.

# Python requests library TLS fingerprint (easily detected)
import requests
response = requests.get('https://www.linkedin.com')
# JA3 Hash: 579ccef312d18482fc42e2b822ca2430

# Real Chrome browser TLS fingerprint
# JA3 Hash: 773906b0efdefa24a7f2b8eb6985bf37
# LinkedIn can detect this difference instantly

What makes this particularly challenging is that you can have perfect residential proxies, rotate user agents all day long, and even simulate perfect mouse movements—but if your TLS fingerprint doesn't match a legitimate browser, you're still detectable. In 2025, LinkedIn has started implementing JA4 fingerprinting, which is even more sophisticated and covers modern TLS 1.3 and QUIC/HTTP/3 traffic.

JavaScript Challenges and Browser Environment Validation

Modern web scraping detection goes far beyond checking if JavaScript is enabled. LinkedIn employs multiple JavaScript-based fingerprinting techniques that create a unique signature for your browser environment. Canvas fingerprinting, for example, renders text and shapes on an HTML5 canvas element, then analyzes the exact pixel output. Each combination of browser, operating system, graphics card, and installed fonts produces a slightly different result—creating a fingerprint that's remarkably stable yet hard to forge.

WebGL fingerprinting takes this further by testing how your browser renders 3D graphics. The system also validates that all browser APIs behave consistently with each other. For instance, if your user agent claims to be Chrome on Windows, but your browser doesn't have the window.chrome object or your screen resolution doesn't match common Windows settings, that inconsistency gets flagged.


// LinkedIn's JavaScript detection can catch common automation indicators
if (navigator.webdriver) {
    // Selenium and Puppeteer set this to true by default
}

if (!window.chrome || !window.chrome.runtime) {
    // Chrome browsers should have this object
    // Many automation tools don't implement it correctly
}

// Canvas fingerprinting
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.textBaseline = 'top';
ctx.font = '14px Arial';
ctx.fillText('Browser fingerprint', 2, 2);
// Analyze pixel data - each browser/OS combination is unique

Behavioral Biometrics: The Human Factor

Perhaps the most difficult challenge to overcome is behavioral biometrics. LinkedIn tracks micro-behaviors that distinguish humans from bots in ways that are incredibly hard to simulate convincingly. Human mouse movements follow natural curves with subtle imperfections—we overshoot targets slightly, make small corrections, and have variable acceleration patterns. Scrolling behavior is similarly nuanced, with humans exhibiting natural reading pauses and variable scroll speeds.

Even keyboard timing can reveal automation. Humans have natural variations in the time between keystrokes, while automated form-filling often shows suspiciously consistent timing patterns. Touch interactions on mobile devices provide another layer of behavioral data that's extremely difficult for bots to replicate authentically.

The Voyager API Shield

LinkedIn's internal Voyager API, which powers both their web and mobile applications, has become significantly more protected in recent years. The API uses dynamically generated CSRF tokens that expire every 5-10 minutes, requiring constant re-authentication. Request signing mechanisms similar to AWS Signature v4 validate that each API call comes from a legitimate LinkedIn client. Perhaps most challenging for scrapers, the API endpoints themselves rotate every 4-8 weeks, requiring constant maintenance to keep any reverse-engineered solution working.

DIY Solutions: Technical Implementation & Real-World Analysis

Now that we understand the challenges, let's explore three approaches to building your own LinkedIn scraper. Each solution includes working code and an honest analysis of what works, what doesn't, and what it really costs to maintain.

Approach 1: Selenium with Human Behavior Simulation

The first approach uses Selenium WebDriver with sophisticated anti-detection measures and human behavior simulation. The core idea is to control a real Chrome browser instance that appears as legitimate as possible while automating the scraping process. This means not just visiting pages, but simulating realistic mouse movements, natural scrolling patterns, and appropriate timing between actions.

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains
import time
import random
import numpy as np

class LinkedInScraper:
    def __init__(self, proxy=None):
        options = webdriver.ChromeOptions()

        if proxy:
            options.add_argument(f'--proxy-server={proxy}')

        # Anti-detection measures
        options.add_argument('--disable-blink-features=AutomationControlled')
        options.add_experimental_option("excludeSwitches", ["enable-automation"])
        options.add_experimental_option('useAutomationExtension', False)

        self.driver = webdriver.Chrome(options=options)

        # Override navigator.webdriver property
        self.driver.execute_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})")

    def human_like_mouse_movement(self, element):
        """Simulate natural mouse movement using Bezier curves"""
        action = ActionChains(self.driver)
        element_location = element.location

        # Generate points along a cubic Bezier curve for natural movement
        points = self._generate_bezier_curve(
            (0, 0),
            (element_location['x'], element_location['y']),
            num_points=random.randint(10, 20)
        )

        for point in points:
            action.move_by_offset(point[0], point[1])
            action.pause(random.uniform(0.001, 0.01))

        action.perform()

    def _generate_bezier_curve(self, start, end, num_points=15):
        """Generate points along a cubic Bezier curve"""
        # Random control points create natural curve variation
        ctrl1 = (start[0] + random.randint(-50, 50), start[1] + random.randint(-50, 50))
        ctrl2 = (end[0] + random.randint(-50, 50), end[1] + random.randint(-50, 50))

        points = []
        for i in range(num_points):
            t = i / num_points
            x = (1-t)**3 * start[0] + 3*(1-t)**2*t * ctrl1[0] + \
                3*(1-t)*t**2 * ctrl2[0] + t**3 * end[0]
            y = (1-t)**3 * start[1] + 3*(1-t)**2*t * ctrl1[1] + \
                3*(1-t)*t**2 * ctrl2[1] + t**3 * end[1]
            points.append((int(x), int(y)))

        return points

    def human_like_scroll(self):
        """Simulate natural scrolling with easing and reading pauses"""
        total_height = self.driver.execute_script("return document.body.scrollHeight")
        current_position = 0

        while current_position < total_height:
            scroll_distance = random.randint(100, 400)

            # Apply easing function for natural acceleration/deceleration
            for step in range(10):
                ease_factor = self._ease_in_out_quad(step / 10)
                scroll_amount = scroll_distance * ease_factor / 10
                self.driver.execute_script(f"window.scrollBy(0, {scroll_amount});")
                time.sleep(random.uniform(0.01, 0.05))

            current_position += scroll_distance

            # Random pause simulating reading
            time.sleep(random.uniform(0.5, 2.0))

    def _ease_in_out_quad(self, t):
        """Quadratic easing function for smooth scrolling"""
        return 2*t*t if t < 0.5 else -1+(4-2*t)*t

    def scrape_profile(self, profile_url):
        """Scrape a LinkedIn profile with anti-detection measures"""
        self.driver.get(profile_url)
        time.sleep(random.uniform(2, 4))

        # Simulate human reading behavior
        self.human_like_scroll()

        try:
            name = WebDriverWait(self.driver, 10).until(
                EC.presence_of_element_located((By.CSS_SELECTOR, "h1.text-heading-xlarge"))
            ).text

            headline = self.driver.find_element(By.CSS_SELECTOR, "div.text-body-medium").text

            return {'name': name, 'headline': headline}
        except Exception as e:
            print(f"Error scraping profile: {e}")
            return None

    def close(self):
        self.driver.quit()

# Usage
scraper = LinkedInScraper(proxy="http://your-residential-proxy:port")
data = scraper.scrape_profile("https://www.linkedin.com/in/example/")
scraper.close()

This approach represents a significant engineering effort. The Bezier curve implementation for mouse movements creates paths that look more natural than straight lines. The easing functions for scrolling mimic how humans gradually accelerate and decelerate. Random timing variations between actions help avoid the suspiciously regular patterns that simple delays create.

However, the reality is sobering. Each profile takes 5-10 seconds minimum to scrape when you account for human behavior simulation. Running multiple instances in parallel requires substantial server resources—each Chrome instance consumes 300-500MB of RAM. LinkedIn updates its user interface every 4-8 weeks on average, which means your CSS selectors break regularly and need maintenance. Quality residential proxies cost $10-15 per gigabyte, and scraping 10,000 profiles typically requires 20-30GB of proxy bandwidth. Even with all these precautions, there's still a 30-40% chance of account suspension, because Selenium's ChromeDriver leaves detectable traces that advanced fingerprinting can identify.

Approach 2: Reverse Engineering the Voyager API

The second approach bypasses the browser entirely by directly calling LinkedIn's internal Voyager API. This is faster and more efficient than browser automation, but it requires understanding how LinkedIn's authentication and CSRF protection work.

import requests
import re

class VoyagerAPIScraper:
    def __init__(self, li_at_cookie):
        """
        Initialize with your LinkedIn session cookie

        To get li_at cookie:
        1. Login to LinkedIn in your browser
        2. Open DevTools > Application > Cookies
        3. Copy the 'li_at' cookie value
        """
        self.session = requests.Session()

        self.session.headers.update({
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
            'Accept': 'application/vnd.linkedin.normalized+json+2.1',
            'Accept-Language': 'en-US,en;q=0.9',
            'x-li-lang': 'en_US',
            'x-restli-protocol-version': '2.0.0',
        })

        self.session.cookies.set('li_at', li_at_cookie, domain='.linkedin.com')
        self._get_csrf_token()

    def _get_csrf_token(self):
        """Extract CSRF token from session"""
        response = self.session.get('https://www.linkedin.com/feed/')

        jsessionid = self.session.cookies.get('JSESSIONID', domain='.linkedin.com')

        if jsessionid:
            csrf_token = jsessionid.strip('"')
            self.session.headers.update({'csrf-token': csrf_token})
        else:
            raise Exception("Failed to obtain CSRF token")

    def get_profile(self, profile_id):
        """Fetch profile data using Voyager API"""
        url = f'https://www.linkedin.com/voyager/api/identity/profiles/{profile_id}/profileView'

        try:
            response = self.session.get(url)

            if response.status_code == 200:
                data = response.json()
                return self._parse_profile_data(data)
            elif response.status_code == 429:
                print("Rate limited! Wait before retrying.")
                return None
            else:
                print(f"Error {response.status_code}")
                return None

        except Exception as e:
            print(f"Request failed: {e}")
            return None

    def _parse_profile_data(self, raw_data):
        """Parse the complex Voyager API response"""
        try:
            profile = raw_data.get('profile', {})

            first_name = profile.get('firstName', '')
            last_name = profile.get('lastName', '')
            headline = profile.get('headline', '')
            location = profile.get('locationName', '')

            experience = []
            positions = raw_data.get('positionView', {}).get('elements', [])

            for position in positions:
                exp = {
                    'title': position.get('title', ''),
                    'company': position.get('companyName', ''),
                    'duration': position.get('timePeriod', {}),
                }
                experience.append(exp)

            return {
                'name': f"{first_name} {last_name}",
                'headline': headline,
                'location': location,
                'experience': experience,
            }
        except Exception as e:
            print(f"Parsing error: {e}")
            return None

# Usage
scraper = VoyagerAPIScraper(li_at_cookie="YOUR_LI_AT_COOKIE_HERE")
profile = scraper.get_profile("williamhgates")
print(profile)

The Voyager API approach is technically elegant but practically fragile. The CSRF token extraction from JSESSIONID cookies works, but tokens expire every 5-10 minutes and must be refreshed constantly. The API's response structure is deeply nested and changes without notice—a field that was at profile.data.elements[0].value might suddenly move to profile.included[2].attributes.value after an update. More critically, LinkedIn detects abnormal API usage patterns. Your personal account making hundreds of API calls per hour is a red flag that often leads to immediate suspension.

The endpoint structure itself is a moving target. On average, Voyager endpoints change every 4-8 weeks. Sometimes it's just a version number increment in the URL; other times, the entire endpoint structure gets reorganized. This means your scraper will break regularly, requiring reverse engineering work each time to figure out the new structure. The same rate limits apply as browser-based scraping—you're still limited to around 80-100 requests per hour per account—so the speed advantage only helps with execution time, not overall throughput.

Approach 3: Playwright with Advanced Stealth

The third approach uses Playwright, a more modern browser automation framework, combined with stealth techniques to hide automation signals. Playwright has some advantages over Selenium in terms of its architecture and default behavior, but it still faces the fundamental challenge of browser fingerprinting.

from playwright.sync_api import sync_playwright
import random

class StealthLinkedInScraper:
    def __init__(self, proxy_config=None):
        self.playwright = sync_playwright().start()

        self.browser = self.playwright.chromium.launch(
            headless=True,
            args=[
                '--disable-blink-features=AutomationControlled',
                '--disable-dev-shm-usage',
                '--no-sandbox',
            ]
        )

        context_options = {
            'viewport': {'width': 1920, 'height': 1080},
            'user_agent': self._get_random_user_agent(),
            'locale': 'en-US',
            'timezone_id': 'America/New_York',
            'permissions': ['geolocation'],
            'geolocation': {'latitude': 40.7128, 'longitude': -74.0060},
        }

        if proxy_config:
            context_options['proxy'] = proxy_config

        self.context = self.browser.new_context(**context_options)

        # Inject stealth scripts to hide automation
        self.context.add_init_script("""
            // Override navigator.webdriver
            Object.defineProperty(navigator, 'webdriver', {
                get: () => undefined,
            });

            // Mock plugins array
            Object.defineProperty(navigator, 'plugins', {
                get: () => [1, 2, 3, 4, 5],
            });

            // Override languages
            Object.defineProperty(navigator, 'languages', {
                get: () => ['en-US', 'en'],
            });

            // Add Chrome runtime object
            window.chrome = {
                runtime: {},
            };

            // Fix permissions API
            const originalQuery = window.navigator.permissions.query;
            window.navigator.permissions.query = (parameters) => (
                parameters.name === 'notifications' ?
                    Promise.resolve({ state: Notification.permission }) :
                    originalQuery(parameters)
            );
        """)

        self.page = self.context.new_page()

    def _get_random_user_agent(self):
        """Return a random realistic User-Agent"""
        user_agents = [
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
            'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
        ]
        return random.choice(user_agents)

    def scrape_profile(self, profile_url):
        """Scrape profile with stealth mode"""
        self.page.goto(profile_url, wait_until='networkidle')
        self.page.wait_for_timeout(random.randint(2000, 4000))

        # Natural scrolling simulation
        for _ in range(random.randint(3, 6)):
            self.page.evaluate(f'window.scrollBy(0, {random.randint(200, 500)})')
            self.page.wait_for_timeout(random.randint(500, 1500))

        try:
            name = self.page.query_selector('h1.text-heading-xlarge').inner_text()
            headline = self.page.query_selector('div.text-body-medium').inner_text()

            return {'name': name, 'headline': headline}
        except Exception as e:
            print(f"Error: {e}")
            return None

    def close(self):
        self.browser.close()
        self.playwright.stop()

# Usage
proxy = {
    'server': 'http://proxy.example.com:8080',
    'username': 'user',
    'password': 'pass'
}

scraper = StealthLinkedInScraper(proxy_config=proxy)
data = scraper.scrape_profile("https://www.linkedin.com/in/example/")
scraper.close()

While the stealth scripts successfully hide basic automation signals like navigator.webdriver, they don't address the deeper fingerprinting issues. LinkedIn's 2025 detection systems analyze TLS handshakes before your JavaScript even runs, making these client-side patches insufficient. The approach still requires premium residential proxy pools to avoid IP-based detection, which typically cost $500-1000 per month for meaningful scale. Each browser instance consumes 200-400MB of RAM, so running dozens in parallel requires expensive server infrastructure. Most importantly, behavioral analysis can still identify automation through subtle patterns in timing and interaction sequences that are extremely difficult to replicate convincingly.

The Professional Alternative: Bright Data LinkedIn Scraper API

After exploring the technical complexity and maintenance burden of DIY solutions, it's worth considering the fundamental question: should you be building a LinkedIn scraper at all? If your core business is data-driven sales, market research, or talent intelligence—rather than web scraping infrastructure—there's a compelling case for using a professional API that handles all this complexity for you

Bright Data's LinkedIn Scraper API represents a fundamentally different approach. Instead of fighting LinkedIn's anti-scraping systems yourself, you're using infrastructure specifically built for this purpose. The service operates through a network of over 72 million residential IPs from real devices worldwide, with each request using browser fingerprints that perfectly match the IP's geographic location and device type. Behavioral simulation happens automatically, including natural scrolling, mouse movements, and reading time. All the technical challenges we've discussed—TLS fingerprinting, JavaScript challenges, behavioral biometrics—are handled transparently.

ChallengeDIY SolutionBright Data API
IP BlockingMaintain your own proxy pool, handle rotation logic, deal with bans 72M+ residential IPs with automatic rotation
Account BansManage account pools, risk your personal accounts, handle suspensions No LinkedIn account needed
CAPTCHAIntegrate third-party CAPTCHA services, handle API failures Built-in CAPTCHA solving
Rate LimitingManual throttling, monitoring, retry logic Automatic rate management
TLS FingerprintingComplex libraries, constant updates, testing Browser-identical fingerprints
API ChangesFix code every 4-8 weeks, reverse engineer changes Bright Data team handles updates
Data ParsingWrite and maintain parsers, handle format changes Returns structured JSON
ScalabilityLimited by infrastructure, requires capacity planning Supports thousands of requests/hour
Success Rate75-85% with excellent setup 95%+ success rate

The technical architecture behind this is sophisticated. When you make an API call, the request gets distributed across Bright Data's residential IP network with intelligent selection based on the target profile's location. Each request uses a real browser fingerprint that's been validated to match the IP's characteristics. The system automatically simulates human behavior patterns, handles any CAPTCHAs that appear, and retries failed requests with different IPs and configurations. Failed requests are automatically identified and retried at no charge to you.

The data extraction itself is handled by parsers that are continuously maintained and updated. When LinkedIn changes their HTML structure or API responses, Bright Data's team updates the parsers—usually within hours—and your code continues working without any changes. The API returns clean, structured JSON with comprehensive data points including profile information, work experience, education, skills, recommendations, recent activity, and network metrics.

Quick Start Guide with Bright Data

Getting started with Bright Data's API is remarkably straightforward compared to building your own scraper. The entire process from signup to your first successful scrape typically takes under 10 minutes.

Initial Setup

First, visit the LinkedIn Scraper product page and start a free trial. You'll get 100 free records with no credit card required, which is enough to test the API and validate the data quality for your use case. After signing up, navigate to your dashboard and copy your API key—that's all the setup you need on the Bright Data side.

Basic Profile Scraping

Here's a complete working example that scrapes a LinkedIn profile and returns structured data:

import requests
import json
import time

class BrightDataLinkedIn:
    def __init__(self, api_key):
        self.api_key = api_key
        self.base_url = "https://api.brightdata.com/datasets/v3"
        self.headers = {
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json"
        }

    def scrape_profile(self, profile_url):
        """Scrape a single LinkedIn profile"""
        endpoint = f"{self.base_url}/trigger"

        payload = [{
            "url": profile_url,
            "dataset_id": "gd_l7q7dkf244hwjntr0",
            "format": "json"
        }]

        # Trigger the scraping job
        response = requests.post(endpoint, headers=self.headers, json=payload)

        if response.status_code != 200:
            raise Exception(f"API Error: {response.status_code}")

        result = response.json()
        snapshot_id = result.get('snapshot_id')

        # Wait for results
        return self._wait_for_results(snapshot_id)

    def _wait_for_results(self, snapshot_id, max_wait=60):
        """Poll for scraping job completion"""
        endpoint = f"{self.base_url}/snapshot/{snapshot_id}"

        for _ in range(max_wait):
            response = requests.get(endpoint, headers=self.headers)

            if response.status_code == 200:
                data = response.json()
                if data.get('status') == 'ready':
                    return data.get('data', [])

            time.sleep(2)

        raise Exception("Timeout waiting for results")

# Usage
scraper = BrightDataLinkedIn(api_key="YOUR_API_KEY_HERE")
profile = scraper.scrape_profile("https://www.linkedin.com/in/satyanadella/")
print(json.dumps(profile, indent=2))

The API follows a trigger-and-poll pattern. You first trigger a scraping job by posting the target URLs, which returns a snapshot ID. Then you poll that snapshot ID until the results are ready. This asynchronous approach allows the system to optimize request timing and handle rate limiting transparently.

Batch Scraping for Scale

One of the API's most powerful features is batch processing. Instead of making individual requests for each profile, you can submit up to 1000 URLs in a single batch request:

def scrape_multiple_profiles(self, profile_urls):
    """Scrape up to 1000 profiles in one batch"""
    endpoint = f"{self.base_url}/trigger"

    payload = [
        {"url": url, "dataset_id": "gd_l7q7dkf244hwjntr0"}
        for url in profile_urls
    ]

    response = requests.post(endpoint, headers=self.headers, json=payload)
    snapshot_id = response.json()['snapshot_id']

    return self._wait_for_results(snapshot_id, max_wait=300)

# Scrape 100 profiles at once
urls = [
    "https://www.linkedin.com/in/profile1/",
    "https://www.linkedin.com/in/profile2/",
    # ... up to 1000 URLs
]

results = scraper.scrape_multiple_profiles(urls)

for profile in results:
    print(f"{profile['name']} - {profile['headline']}")

Batch processing is dramatically more efficient than individual requests. The system processes profiles in parallel while managing rate limits, IP rotation, and retries automatically. A batch of 100 profiles typically completes in 2-3 minutes, compared to the hours it might take with a DIY scraper that needs to carefully throttle requests.

Search-Based Discovery

Beyond scraping known profile URLs, the API supports discovery through keyword search. This is particularly valuable for lead generation or market research where you're looking for people matching specific criteria: 

def search_profiles(self, keyword, location=None, limit=100):
    """Search for LinkedIn profiles by keyword and location"""
    endpoint = f"{self.base_url}/trigger"

    payload = [{
        "dataset_id": "gd_l7q7dkf244hwjntr0",
        "discover_by": "keyword",
        "keyword": keyword,
        "limit": limit
    }]

    if location:
        payload[0]["location"] = location

    response = requests.post(endpoint, headers=self.headers, json=payload)
    snapshot_id = response.json()['snapshot_id']

    return self._wait_for_results(snapshot_id, max_wait=180)

# Find AI Engineers in San Francisco
profiles = scraper.search_profiles(
    keyword="AI Engineer",
    location="San Francisco Bay Area",
    limit=50
)

for profile in profiles:
    print(f"{profile['name']} at {profile.get('current_company', 'N/A')}")

Real-World Integration: CRM Enrichment

Here's a practical example showing how to integrate LinkedIn data into Salesforce for lead enrichment. This pattern works similarly with other CRMs like HubSpot, Pipedrive, or custom systems:

from simple_salesforce import Salesforce

class LinkedInCRMEnrichment:
    def __init__(self, linkedin_api_key, sf_username, sf_password, sf_token):
        self.linkedin = BrightDataLinkedIn(linkedin_api_key)
        self.sf = Salesforce(
            username=sf_username,
            password=sf_password,
            security_token=sf_token
        )

    def enrich_leads(self, linkedin_urls):
        """Enrich Salesforce leads with LinkedIn data"""
        print(f"Scraping {len(linkedin_urls)} LinkedIn profiles...")
        profiles = self.linkedin.scrape_multiple_profiles(linkedin_urls)

        updated_count = 0

        for profile in profiles:
            try:
                # Find existing lead by LinkedIn URL
                lead_query = f"SELECT Id FROM Lead WHERE LinkedIn_URL__c = '{profile['url']}'"
                result = self.sf.query(lead_query)

                if result['totalSize'] > 0:
                    lead_id = result['records'][0]['Id']

                    # Update with enriched data
                    update_data = {
                        'Title': profile.get('headline', ''),
                        'Company': profile.get('current_company', ''),
                        'Industry': profile.get('industry', ''),
                        'LinkedIn_Headline__c': profile.get('headline', ''),
                        'LinkedIn_Summary__c': profile.get('about', '')[:255],
                    }

                    self.sf.Lead.update(lead_id, update_data)
                    updated_count += 1
                    print(f"Updated: {profile['name']}")

            except Exception as e:
                print(f"Error updating {profile.get('name', 'Unknown')}: {e}")

        print(f"\nSuccessfully updated {updated_count} leads")
        return updated_count

# Usage
enricher = LinkedInCRMEnrichment(
    linkedin_api_key="YOUR_BRIGHT_DATA_KEY",
    sf_username="[email protected]",
    sf_password="yourpassword",
    sf_token="yourtoken"
)

linkedin_urls = [
    "https://www.linkedin.com/in/lead1/",
    "https://www.linkedin.com/in/lead2/",
]

enricher.enrich_leads(linkedin_urls)

Understanding the Response Structure

The API returns comprehensive, structured data for each profile. Here's an example of what a complete response looks like:

{
  "name": "Satya Nadella",
  "url": "https://www.linkedin.com/in/satyanadella/",
  "headline": "Chairman and CEO at Microsoft",
  "location": "Redmond, Washington, United States",
  "country_code": "US",
  "followers": 12500000,
  "connections": "500+",
  "about": "I'm Chairman and CEO of Microsoft...",
  "current_company": "Microsoft",
  "current_position": "Chairman and CEO",
  "experience": [
    {
      "title": "Chairman and CEO",
      "company": "Microsoft",
      "company_url": "https://www.linkedin.com/company/microsoft/",
      "location": "Redmond, WA",
      "start_date": "Feb 2014",
      "end_date": "Present",
      "duration": "10 yrs 11 mos",
      "description": "Leading Microsoft's transformation..."
    }
  ],
  "education": [
    {
      "school": "University of Chicago Booth School of Business",
      "degree": "MBA",
      "field_of_study": "Business Administration",
      "start_year": 1996,
      "end_year": 1997
    }
  ],
  "skills": [
    {"name": "Cloud Computing", "endorsements": 99},
    {"name": "Enterprise Software", "endorsements": 87}
  ],
  "languages": [
    {"name": "English", "proficiency": "Native or bilingual"}
  ]
}

The data is real-time, meaning you get current information as it appears on LinkedIn now, not stale data from a database. Failed requests—whether because a profile doesn't exist, is private, or temporarily unavailable—are not charged, and temporary failures are automatically retried at no additional cost.

상인 제품 가격 점수
Bright Data 데이터센터 프록시(공유) $ 0.20/proxy/month
 4.87

차단되지 않고 LinkedIn을 긁는 방법 (1개 업체)

평가:4.87 / 5
Bright Data
$ 0.20/proxy/month

데이터센터 프록시(공유)

 
Alipay
 
Credit card
 
Paypal

결론

LinkedIn 스크래퍼를 구축하는 것은 기술적으로 달성 가능합니다. 우리는 작업 코드를 사용하여 세 가지 다른 접근 방식을 살펴보았습니다. 적절하게 구현되면 각각 성공적으로 프로필 데이터를 추출할 수 있습니다. 그러나 실제 질문은 스크레이퍼를 구축할 수 있는지 여부가 아니라 스크래핑 인프라 구축 및 유지 관리에 엔지니어링 리소스를 할당해야 하는지 여부입니다.

DIY 솔루션은 특정 시나리오에 적합합니다. 매월 1,000개 미만의 프로필을 스크래핑하는 경우, 지속적인 유지 관리를 위한 여유 엔지니어링 용량이 있는 경우, 스크래핑 전문 지식 구축이 비즈니스의 핵심 역량인 경우 또는 API가 해결할 수 없는 고유한 요구 사항이 있는 경우입니다. 하지만 대부분의 기업에서는 경제적 측면에서 전문 서비스 사용을 강력히 선호합니다.

TLS 지문 인식부터 행동 생체 인식까지 우리가 탐구한 기술적 과제는 정교하고 끊임없이 진화하고 있습니다. LinkedIn은 데이터에 대한 액세스를 제어할 수 있는 강력한 인센티브가 있기 때문에 스크래핑 방지 기술에 막대한 투자를 하고 있습니다. 이러한 시스템과 싸우려면 지속적인 엔지니어링 노력, 심층적인 기술 전문 지식 및 지속적인 유지 관리가 필요합니다. 핵심 비즈니스가 웹 스크래핑이 아닌 데이터 중심 영업, 시장 조사 또는 인재 인텔리전스인 경우 엔지니어링 투자는 경제적으로 거의 의미가 없습니다.

Bright Data과 같은 전문 API는 다른 절충안을 나타냅니다. 안정성을 위한 제어권, 예측 가능한 운영을 위한 유지 관리 부담, 간단한 API 호출을 위한 인프라 복잡성을 교환하고 있습니다.가동 시간이 중요하고 안정적으로 확장해야 하며 엔지니어링 팀이 제품 기능에 집중해야 하는 생산 시스템의 경우 API 접근 방식은 더 낮은 총 비용으로 더 나은 결과를 제공합니다.

차단되지 않고 LinkedIn을 긁는 방법 리뷰 FAQ

아니요. Bright Data의 API에서는 LinkedIn 계정 자격 증명을 전혀 제공하지 않아도 되기 때문입니다. 서비스는 자체 인프라와 주거용 IP 네트워크를 통해 독립적으로 운영됩니다. 개인 계정으로 로그인할 필요가 없으므로 계정이 정지될 위험이 없습니다. 이는 금지될 수 있는 LinkedIn 계정을 사용해야 하는 DIY 솔루션과 근본적으로 다릅니다.

CAPTCHA 해결 기능은 서비스에 내장되어 있으며 가격에 포함되어 있습니다. 타사 CAPTCHA 서비스를 통합하거나 이러한 문제를 직접 처리할 필요가 없습니다. 시스템은 고급 해결 기술을 사용하여 CAPTCHA가 나타나면 자동으로 해결합니다. 마찬가지로 속도 제한은 인프라에 의해 자동으로 처리됩니다. 일괄 요청으로 수천 개의 URL을 제출할 수 있으며, 시스템은 이를 대규모 IP 네트워크 전체에서 최적의 속도로 지능적으로 처리하여 탐지를 피하는 동시에 처리량을 최대화합니다. 지연을 수동으로 구성하거나 제한 논리를 구현할 필요가 없습니다.

이는 DIY 솔루션에 비해 가장 큰 장점 중 하나입니다. LinkedIn이 HTML 구조, CSS 클래스 또는 내부 API를 업데이트할 때(몇 주마다 발생) Bright Data 팀은 이러한 변경 사항을 모니터링하고 스크래퍼 파서를 자동으로 업데이트합니다. 귀하의 코드에는 수정이 필요하지 않습니다. API는 LinkedIn 측의 변경 사항에 관계없이 동일한 구조의 JSON 형식을 계속 반환합니다. 이는 DIY 스크레이퍼를 장기간 작동하는 데 비용이 많이 드는 유지 관리 부담을 제거합니다. DIY 솔루션은 일반적으로 LinkedIn이 업데이트될 때마다 중단되고 즉각적인 엔지니어링 주의가 필요하지만 API는 이러한 변경을 통해 일관된 가동 시간을 유지합니다.
이전 기사 Bright Data을 사용하여 엔터프라이즈 AI 데이터 파이프라인을 구축하는 방법 오늘날의 경쟁이 치열한 디지털 환경에서 인공 지능(AI...
다음 기사 더 이상 게시물이 없습니다
블로그
차단되지 않고 LinkedIn을 긁는...

LinkedIn은 매일 수천 건의 스크래핑 시도를 차단...

블로그
최고의 LinkedIn 프록시 서버

LinkedIn은 전 세계 전문가들에게 없어서는 안 될...

이메일을 통해 직접 문의해 주세요 [email protected]

추천 가맹점