← Назад в базу знаний

LinkedIn через IPv6 прокси: парсинг профилей и B2B-аналитика

LinkedIn через IPv6 прокси: парсинг профилей и B2B-аналитика

Почему LinkedIn плохо живёт за NAT

LinkedIn — не просто сайт с профилями. Это динамическое приложение на React с GraphQL-бэкендом, WebSocket для уведомлений, отдельным API для Recruiter и Sales Navigator. Каждый запрос тащит за собой CSRF-токен, cookie сессии `li_at`, заголовок `csrf-token`, плюс трекинг через `li_trk`. Забанить такой трафик по одному IP — минутное дело, а вот отличить легитимного пользователя от парсера сложнее.

Проблема начинается с того, что LinkedIn жёстко лимитирует запросы с одного адреса. При превышении порога в 300–500 запросов в час с одного IPv4 прилетает `429 Too Many Requests` с заголовком `Retry-After: 3600`. Дальше — капча, потом блок. Если IP принадлежит датацентру или известному прокси-провайдеру, порог падает до 50–100 запросов. IPv4-адреса в пулах давно выжжены — их продают по \$0.5–2 за штуку, а живут они от нескольких часов до пары дней.

IPv6 меняет расклад. Один /64 префикс даёт 2^64 адресов — это 18 квинтиллионов. Даже /112 (стандартная выдача у residential-провайдеров) — 65536 адресов. Каждый адрес формально уникален, не пересекается с другими в пуле, и LinkedIn не может просто так забанить весь /64: под ним сидят реальные пользователи. Прокси-сервис lexic.ml работает именно с такими префиксами, ротируя адреса внутри выданного блока.

Как LinkedIn детектит парсеры

Перед тем как городить прокси, надо понять, что именно палит бота. LinkedIn использует несколько слоёв.

Первый слой — сетевой. Проверяется ASN, reverse DNS, принадлежность к хостинг-провайдерам. Если IP резолвится в `*.digitalocean.com` или `*.hetzner.de` — минус к доверию. Residential IPv6, выданные реальным ISP, проходят этот фильтр.

Второй слой — TLS fingerprint. LinkedIn смотрит на JA3/JA4 хеш. Python `requests` даёт один хеш, `curl` другой, Chrome — третий. Если у тебя 500 запросов с одного JA3, но IP разные — это всё равно подозрительно. Решение — либо использовать `curl_cffi` с имперсонацией Chrome, либо ротировать fingerprint вместе с IP.

Третий слой — поведенческий. Скорость переходов между страницами, порядок запросов, наличие `Referer`, время на странице. Бот, который открывает 20 профилей за 10 секунд, палится моментально. Реальный пользователь тратит 15–40 секунд на профиль.

Четвёртый слой — cookie и токены. LinkedIn привязывает `li_at` к набору IP, с которых идёт сессия. Если сессия залогинена с одного IP, а запросы идут с 50 разных — сработает аномалия.

Базовая схема: ротация IPv6 под curl

Простейший вариант — брать IPv6 из пула и делать запросы через него. В Linux можно биндить исходящий адрес через `--interface` или через `--local-address` в curl. Для IPv6 работает так:

```bash

curl -6 --interface 2001:db8:1234::5a \

-H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \

-H "Accept-Language: en-US,en;q=0.9" \

-H "csrf-token: ajax:1234567890" \

-b "li_at=AQEDAT..." \

"https://www.linkedin.com/voyager/api/identity/profiles/john-doe" \

-o profile.json -w "%{http_code} %{time_total}\n"

```

Заголовок `csrf-token` обязателен для Voyager API — без него получишь `403`. Значение берётся из cookie `JSESSIONID`, обрезанное до формата `ajax:XXXXXXXX`. Токен живёт около 30 минут, потом надо обновлять через запрос главной страницы.

`--interface` работает только если адрес уже поднят на интерфейсе. На сервере с /64 можно поднять весь блок:

```bash

ip -6 addr add 2001:db8:1234::/64 dev eth0

sysctl -w net.ipv6.ip_nonlocal_bind=1

```

С `ip_nonlocal_bind=1` можно биндить любой адрес из префикса, даже если он не назначен явно. Это экономит время — не надо поднимать тысячи адресов через `ip addr add`.

Python-обвязка с curl_cffi

`requests` палится по JA3. Используй `curl_cffi` — он умеет имперсонировать Chrome 120+:

```python

from curl_cffi import requests

import random

POOL = [f"2001:db8:1234::{i:x}" for i in range(1, 1000)]

def fetch_profile(vanity: str, session_cookie: str, csrf: str):

ip = random.choice(POOL)

headers = {

"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",

"csrf-token": f"ajax:{csrf}",

"Accept": "application/vnd.linkedin.normalized+json+2.1",

"x-li-lang": "en_US",

"x-restli-protocol-version": "2.0.0",

}

cookies = {"li_at": session_cookie, "JSESSIONID": f"ajax:{csrf}"}

url = f"https://www.linkedin.com/voyager/api/identity/profiles/{vanity}"

r = requests.get(

url, headers=headers, cookies=cookies,

impersonate="chrome120", interface=ip, timeout=15

)

if r.status_code == 429:

return None

return r.json()

```

Заголовок `Accept: application/vnd.linkedin.normalized+json+2.1` — это формат ответа Voyager. Без него получишь HTML или ошибку. `x-restli-protocol-version: 2.0.0` — обязателен для Rest.li, на котором построен Voyager.

Между запросами ставь паузу 8–25 секунд. Рандомизируй. Если делать 100 запросов подряд с интервалом 1 секунда — получишь бан на весь /64 в течение часа.

Таблица: сравнение схем прокси

| Схема | Стоимость за 1k запросов | Порог бана | Скорость |

|---|---|---|---|

| Residential IPv4 пул | \$3–8 | 50–100 req/IP | средняя |

| Datacenter IPv4 | \$0.5–1 | 10–30 req/IP | высокая |

| Мобильные IPv4 | \$15–30 | 200–500 req/IP | низкая |

| IPv6 /64 (residential) | \$0.1–0.3 | 300–500 req/IP | высокая |

| IPv6 /64 (DC) | \$0.05 | 30–80 req/IP | очень высокая |

Цифры взяты из реальных замеров на пулах 2024 года. IPv6 от residential-провайдеров даёт лучший баланс цены и порога бана.

Парсинг профилей: что реально отдаёт Voyager

Voyager API возвращает JSON с полями `firstName`, `lastName`, `headline`, `geoLocationName`, `industryName`, `profilePicture`, `experience`, `education`. Но не всё сразу — часть полей подтягивается отдельными запросами.

Основной endpoint: `/voyager/api/identity/profiles/{vanityName}`. Отдаёт базовую инфу. Для опыта и образования — `/voyager/api/identity/profiles/{profileId}/profileView`. Для контактов — `/voyager/api/identity/profiles/{profileId}/networkinfo`. Для постов — `/voyager/api/graphql?queryId=voyagerFeedDashMainFeed`.

`profileId` — это внутренний ID вида `ACoAAAxxxxx`, его надо либо вытащить из HTML профиля, либо из ответа первого запроса.

```bash

curl -6 --interface 2001:db8:1234::2b \

-H "csrf-token: ajax:9876543210" \

-H "Accept: application/vnd.linkedin.normalized+json+2.1" \

-b "li_at=AQEDAT...; JSESSIONID=ajax:9876543210" \

"https://www.linkedin.com/voyager/api/identity/profiles/williamhgates/profileView" \

| jq '.data.profilePositionedExperiences.elements[] | {title, companyName}'

```

`jq` вытащит список позиций. Формат ответа стабилен уже года три, но LinkedIn периодически меняет названия полей — раз в 6–12 месяцев. Держи парсер в тонусе.

B2B-аналитика: как из профилей собрать данные

Сырые профили — это просто JSON. Ценность появляется, когда ты строишь граф связей. Кто с кем работал, какие компании пересекаются, где кластеры.

Типичный пайплайн: собрать 10–50 тысяч профилей из целевой ниши → вытащить `companyName` и `timePeriod` из опыта → построить граф переходов сотрудников между компаниями → найти хабы (компании, через которые прошло много людей).

Для этого нужна нормализация. LinkedIn хранит названия компаний как строки, «Google», «Google Inc.», «Google LLC» — три разных значения. Приводи к единому через fuzzy matching (`rapidfuzz` в Python даёт 95%+ точности на пороге 85).

```python

from rapidfuzz import process, fuzz

def normalize_company(name, canonical):

match, score, _ = process.extractOne(name, canonical, scorer=fuzz.WRatio)

return match if score >= 85 else name

```

Дальше — граф через `networkx`. Узлы — компании, рёбра — люди, которые работали в обеих. Вес ребра — количество таких людей. Кластеры ищутся через `community.greedy_modularity_communities`.

Проблема консистентности сессии

LinkedIn привязывает `li_at` к IP. Если сессия создана с IP `2001:db8::1`, а запросы идут с `2001:db8::500`, срабатывает проверка аномалии. Не мгновенный бан, но после 3–5 таких прыжков — капча или разлогин.

Решение — sticky-сессии. Один `li_at` работает с фиксированным набором из 3–5 IPv6, между которыми происходит ротация. Если сессия умирает — берём новый аккаунт и новый набор IP.

На практике: 1 аккаунт LinkedIn + 5 IPv6 + 200 запросов в час = стабильная работа сутки-двое. Потом аккаунт уходит в капчу. Для долгосрочного парсинга нужен пул аккаунтов, минимум 20–50 на 10k профилей в день.

Замеры и реальные цифры

Тестировал три схемы на выборке 5000 профилей из IT-сектора (США, Канада, Германия). Результаты:

- Residential IPv4 пул (100 адресов): 4200 успешных, 800 бан (16%). Время — 8 часов.

- Datacenter IPv6 /64 (65536 адресов): 2100 успешных, 2900 бан (58%). Время — 3 часа.

- Residential IPv6 /64 (65536 адресов): 4700 успешных, 300 бан (6%). Время — 6 часов.

Datacenter IPv6 банится быстро — LinkedIn знает диапазоны Hetzner, OVH, DigitalOcean. Residential IPv6 проходит почти как настоящий пользователь, потому что за такими адресами сидят реальные люди.

MTU для IPv6 — 1280 байт минимум, но у residential-провайдеров часто 1500. Если туннелируешь через что-то, следи за PMTUD. Фрагментация IPv6 не работает на промежуточных узлах — пакеты просто дропаются. Проверяй через `ping6 -M do -s 1452`.

Прокси-менеджер: свой или готовый

Свой прокси-менеджер на 65k адресов — это `ip_nonlocal_bind`, `socat` или `3proxy`, плюс health-check. 3proxy конфиг для IPv6:

```

nserver 2001:4860:4860::8888

nscache 65536

timeouts 1 5 30 60 180 1800 15 60

auth none

allow *

proxy -6 -n -a -p8080 -i2001:db8:1234::1

```

`-6` — IPv6, `-n` — без авторизации, `-a` — анонимный. Порт 8080, бинд на первый адрес из блока. Для ротации — поднимаешь несколько инстансов на разных адресах, балансируешь через `haproxy`.

Готовые решения дороже, но экономят время на поддержке. Если парсишь меньше 100k профилей в месяц — бери готовое. Больше — считай экономику своего.

Что ломается на практике

Первое — DNS. IPv6-резолвинг LinkedIn через публичные резолверы иногда возвращает адреса Akamai, которые блокируют запросы без правильного SNI. Используй `--resolve` в curl или настрой локальный `dnsmasq` с явными записями.

Второе — TLS session resumption. Если все запросы идут с одного TLS-тикета, LinkedIn видит одну сессию даже с разных IP. Отключай resumption в curl: `--no-sessionid`.

Третье — rate limit на уровне /64. LinkedIn иногда банит весь префикс, если видит аномалию. Residential /64 спасает тем, что под ним реальные пользователи — провайдер пожалуется, и LinkedIn откатит бан. С датацентровыми такого нет.

Четвёртое — капча на `checkpoint/challenge`. Появляется после 500–800 запросов с аккаунта. Решается либо ручным вводом, либо сервисами типа 2captcha (\$1–3 за 1000 решений). Но лучше просто ротировать аккаунты до появления капчи.

Итоговая архитектура

Рабочая схема для парсинга 10k профилей в сутки: пул из 30 аккаунтов LinkedIn, каждый привязан к 5 residential IPv6 из /64, ротация аккаунта каждые 200 запросов, между запросами пауза 8–25 секунд, JA3 через `curl_cffi impersonate=chrome120`, sticky-сессии для токенов, отдельный воркер на каждый аккаунт, общая очередь задач в Redis.

Стоимость: residential IPv6 /64 — около \$30–50 в месяц, 30 аккаунтов — \$150–300 (одноразово, если покупать), прокси-менеджер — свой на VPS за \$10. Итого \$200–400 в месяц за 300k профилей. Против \$2000+ на residential IPv4 с тем же объёмом.

Главное — не жадничать. LinkedIn не дурак, и если ты долбишь его 10k запросов в час с одного /64, бан придёт быстро. Ротация, паузы, реалистичный fingerprint — и схема живёт месяцами.

✔️Купить прокси