Парсинг (web scraping) на JavaScript: полное руководство от простого к сложному

Веб-скрапинг на JavaScript и Node.js: axios, cheerio, Puppeteer и Playwright — от простых страниц до сложной динамики.

КP
Команда Parsing.agency
Сбор данных под задачи бизнеса
Опубликовано: 28 марта 2025

Обзорная статья о том, как на Node.js забирать страницы, доставать из них данные и масштабировать всё это до боевого скрапера: кодировки, многопоточность, прокси, TOR, SSL, cookie, заголовки, очереди URL и подводные камни. С ссылками на официальные библиотеки.


Оглавление

  1. Что такое парсинг и из чего он состоит
  2. Как забираем страницу: HTTP-клиенты
  3. Библиотеки для парсинга содержимого
  4. Получение статуса ответа и других заголовков
  5. Решение проблем с парсингом кириллицы
  6. Работа с cookie
  7. Работа с HTTPS / SSL
  8. Использование прокси
  9. Парсинг через TOR
  10. Multi / многопоточность и конкурентность
  11. Хранение URL и очередей (обзорно)
  12. Готовые фреймворки
  13. Антибот, robots.txt, ретраи (то, что часто забывают)
  14. Основные плюсы и минусы реализации на JavaScript

1. Что такое парсинг и из чего он состоит

Парсинг сайта почти всегда раскладывается на три независимых слоя, и проектировать скрапер удобно именно по этим слоям:

  1. Транспорт — как получить байты страницы (HTTP-клиент или headless-браузер).
  2. Извлечение — как из HTML/JSON достать нужные поля (парсер DOM, селекторы).
  3. Оркестрация — как обойти много URL, не получив бан: очереди, конкурентность, прокси, ретраи, дедупликация.

Всё руководство построено по нарастанию: сначала «забрать одну страницу», в конце — «устойчивый распределённый краулер».

Важная развилка с самого начала:

  • Статический сайт (данные уже в HTML) → достаточно HTTP-клиент + парсер DOM. Быстро, дёшево, тысячи страниц в минуту.
  • Динамический сайт (данные подгружает JavaScript) → нужен либо headless-браузер (Playwright / Puppeteer), либо реверс внутреннего API сайта (часто данные лежат в JSON-эндпоинте, и браузер не нужен).

Прежде чем тянуть тяжёлый браузер, всегда проверьте вкладку Network в DevTools: если страница ходит за данными в /api/... и возвращает JSON — парсить надо его, а не отрисованный DOM.


2. Как забираем страницу: HTTP-клиенты

2.1. Нативный fetch (Node 18+) — выбор по умолчанию

Начиная с Node.js 18 fetch встроен глобально, стабилен с Node 21 и поддержан в LTS-ветках 22 и 24. Под капотом он работает на undici, так что отдельные пакеты вроде node-fetch для базовых задач больше не нужны.

javascript
const res = await fetch('https://example.com');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const html = await res.text();

Две вещи, на которых спотыкаются все новички:

  • fetch не бросает исключение на 404/500 — нужно самому проверять res.ok.
  • У fetch нет таймаута по умолчанию — повисший сокет может держаться вечно. Ставьте AbortSignal.timeout():
javascript
const res = await fetch(url, { signal: AbortSignal.timeout(15_000) });

2.2. undici напрямую — когда нужна максимальная скорость

undici — это и есть «движок» нативного fetch, но его низкоуровневый API (request, пулы соединений, pipelining) в бенчмарках обгоняет fetch, axios и got в несколько раз. Имеет смысл, когда вы упёрлись в пропускную способность.

javascript
import { request } from 'undici';
const { statusCode, headers, body } = await request('https://example.com');
const html = await body.text();

2.3. got и got-scraping — удобство + «маскировка под браузер»

got — зрелый клиент с встроенными ретраями, хуками, поддержкой cookie-jar и HTTP/2.

Для скрапинга интереснее форк got-scraping от Apify: он автоматически генерирует правдоподобные заголовки браузера и порядок заголовков, что снижает вероятность бана. Именно его использует CheerioCrawler в Crawlee.

javascript
import { gotScraping } from 'got-scraping';
const { body } = await gotScraping({ url: 'https://example.com' });

2.4. axios — если нужны интерсепторы и привычный API

axios (репозиторий) остаётся самым популярным клиентом из-за интерсепторов, удобной работы с прокси и автоматического парсинга JSON. Для скрапинга он не быстрее fetch, но его экосистема (например, axios-retry) экономит время.

2.5. Прочее

  • ky — тонкая обёртка над fetch с разумными дефолтами (ретраи, таймауты).
  • node-fetchlegacy, нужен только на очень старых Node; на современных используйте встроенный fetch.
  • Встроенные модули http/https — максимум контроля, но много ручной обвязки; обычно нужны лишь под капотом агентов и прокси.

Что выбрать

Сценарий Рекомендация
Большинство задач, Node 18+ нативный fetch
Тысячи запросов, упор в производительность undici (request/Pool)
Нужна маскировка заголовков из коробки got-scraping
Интерсепторы, привычный API, легаси-кодовая база axios
Динамический сайт с JS-рендерингом Playwright / Puppeteer (см. §3.5)

3. Библиотеки для парсинга содержимого

Получив HTML-строку, её нужно превратить в данные. Регулярными выражениями HTML не парсят — это хрупко и ломается на первом же вложенном теге. Используйте настоящий парсер.

3.1. Cheerio — стандарт для статики

Cheerio (репозиторий) — быстрый серверный парсер с jQuery-подобным API. Не исполняет JS, не рендерит — просто строит дерево и даёт по нему ходить селекторами. Идеален в связке с fetch/got.

javascript
import * as cheerio from 'cheerio';

const html = await (await fetch('https://example.com/products')).text();
const $ = cheerio.load(html);

const items = $('.product-card').map((_, el) => ({
  title: $(el).find('.title').text().trim(),
  price: $(el).find('.price').text().trim(),
  url: new URL($(el).find('a').attr('href'), 'https://example.com').href,
})).get();

3.2. jsdom — почти настоящий DOM

jsdom реализует значительную часть браузерного DOM и даже может исполнять скрипты на странице. Тяжелее Cheerio, но даёт привычные querySelectorAll, document, и пригождается, когда нужен более «честный» DOM API.

3.3. Лёгкие и быстрые альтернативы

  • node-html-parser — очень быстрый, с CSS-селекторами.
  • htmlparser2 — потоковый низкоуровневый парсер (на нём построен Cheerio).
  • parse5 — спецификационно-точный HTML5-парсер.
  • linkedom — лёгкая альтернатива jsdom с DOM API.

3.4. Извлечение по «рецептам»

x-ray позволяет описывать извлечение декларативно (селектор → поле) и сразу ходить по пагинации. Удобно для прототипов.

3.5. Динамика: Playwright и Puppeteer

Когда контент рисует JS, нужен headless-браузер:

  • Playwright (репозиторий) — современный фаворит: Chromium, Firefox и WebKit одним API, авто-ожидания элементов, перехват сетевых запросов, контексты для изоляции cookie.
  • Puppeteer (репозиторий) — де-факто стандарт для Chrome/Chromium, чуть проще, огромная экосистема.
javascript
import { chromium } from 'playwright';

const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle' });
const titles = await page.$$eval('h2', els => els.map(e => e.textContent.trim()));
await browser.close();

Браузер — самый дорогой по ресурсам способ: десятки-сотни МБ RAM на вкладку. Используйте его только когда реально нет статического HTML или внутреннего API.

Совет по гибридному подходу: часто оптимально открыть страницу в браузере один раз, вытащить готовый HTML через page.content() и дальше разбирать его быстрым Cheerio — так вы совмещаете JS-рендеринг и удобные селекторы.


4. Получение статуса ответа и других заголовков

Статус и заголовки — это половина диагностики скрапера (бан, редирект, лимит, кодировка).

С нативным fetch:

javascript
const res = await fetch(url, { redirect: 'follow' });

res.status;        // 200, 404, 429, 503 ...
res.statusText;    // 'OK', 'Too Many Requests'
res.ok;            // true при 2xx
res.redirected;    // были ли редиректы
res.url;           // итоговый URL после редиректов

res.headers.get('content-type');   // text/html; charset=windows-1251
res.headers.get('set-cookie');     // печеньки
res.headers.get('retry-after');    // сколько ждать при 429/503
[...res.headers];                  // все заголовки парами

Полезные привычки:

  • 429 / 503 → читайте Retry-After и делайте backoff, а не долбите дальше.
  • 301/302/308 → решите, идти ли по редиректу (redirect: 'manual' даёт ручной контроль).
  • Content-Type с charset= → первый и главный источник правды о кодировке (см. §5).
  • Управлять исходящими заголовками (User-Agent, Accept-Language, Referer) не менее важно — многие сайты режут запросы без правдоподобного User-Agent.

В got/axios всё то же доступно как response.statusCode, response.headers. В браузере — через перехват ответа: page.on('response', res => res.status()).


5. Решение проблем с парсингом кириллицы

Классическая боль рунета: страница в windows-1251 (или koi8-r), а вы получаете «крокозябры» вроде привет. Причина — res.text() всегда декодирует байты как UTF-8, а сайт отдал их в другой кодировке.

Правило: для не-UTF-8 страниц нельзя брать res.text(). Берите сырые байты (arrayBuffer) и декодируйте нужной кодировкой через iconv-lite.

javascript
import iconv from 'iconv-lite';

const res = await fetch('https://old-site.ru/');
const buf = Buffer.from(await res.arrayBuffer());

// 1) пытаемся узнать кодировку из заголовка Content-Type
let charset = (res.headers.get('content-type') || '').match(/charset=([^;]+)/i)?.[1];

// 2) если в заголовке нет — ищем в <meta> (декодируем кусок как latin1, чтобы прочитать тег)
if (!charset) {
  const head = iconv.decode(buf, 'latin1');
  charset = head.match(/<meta[^>]+charset=["']?([\w-]+)/i)?.[1]
         || head.match(/charset=([\w-]+)/i)?.[1];
}

charset = (charset || 'utf-8').toLowerCase().replace('windows-', 'win');

const html = iconv.decode(buf, charset); // корректная кириллица

Если кодировка нигде не объявлена, её можно определить эвристически:

  • jschardet — порт Mozilla Universal Charset Detector.
  • chardet — альтернативный детектор.
javascript
import jschardet from 'jschardet';
const guess = jschardet.detect(buf); // { encoding: 'windows-1251', confidence: 0.99 }

Дополнительно:

  • Cheerio умеет декодировать сам, если передать буфер и подсказку: cheerio.load(buf, { decodeEntities: true }) — но явный iconv.decode надёжнее.
  • В headless-браузере проблемы кодировки обычно нет: браузер декодирует страницу сам, page.content() вернёт корректный UTF-8.
  • Не забывайте про HTML-сущности (&nbsp;, &#1055;) — нормальные парсеры (Cheerio, parse5) раскодируют их за вас.

Cookie нужны для авторизованных зон, сессий, корзин и обхода «первого визита». Есть три уровня.

6.1. Вручную через заголовки

javascript
const res = await fetch(url, { headers: { cookie: 'sid=abc123; lang=ru' } });
const setCookie = res.headers.get('set-cookie'); // распарсить и отправить дальше

Подходит для простых случаев, но вручную поддерживать набор cookie между запросами больно.

tough-cookie — эталонная реализация cookie-хранилища с учётом домена, пути, срока жизни и флагов. Многие клиенты интегрированы с ним из коробки.

got принимает jar напрямую и сам ведёт сессию:

javascript
import got from 'got';
import { CookieJar } from 'tough-cookie';

const cookieJar = new CookieJar();
await got('https://site.ru/login', { cookieJar, method: 'POST', form: { user, pass } });
const profile = await got('https://site.ru/account', { cookieJar }); // cookie подставятся сами

Для axios есть обёртка axios-cookiejar-support, для нативного fetch — придётся прокидывать tough-cookie вручную или использовать got/undici.

6.3. В браузере

В Playwright/Puppeteer cookie живут в контексте, и их можно сохранять/восстанавливать — удобно, чтобы один раз залогиниться и переиспользовать сессию:

javascript
// сохранить состояние (cookie + localStorage)
await context.storageState({ path: 'state.json' });
// восстановить в новом запуске
const context = await browser.newContext({ storageState: 'state.json' });

7. Работа с HTTPS / SSL

Обычный HTTPS-сайт никаких усилий не требует — fetch/got/axios проверяют сертификат автоматически. Особые случаи:

7.1. Самоподписанные / просроченные сертификаты

Иногда нужно отключить проверку (например, при работе через MITM-прокси или с тестовым стендом). Делайте это осознанно — это снимает защиту от подмены трафика.

javascript
// undici / нативный fetch — через диспетчер
import { Agent, setGlobalDispatcher } from 'undici';
setGlobalDispatcher(new Agent({ connect: { rejectUnauthorized: false } }));

// got / axios — через https.Agent
import https from 'node:https';
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
// got:  got(url, { agent: { https: httpsAgent } })
// axios: axios.get(url, { httpsAgent })

Глобальный «топор» NODE_TLS_REJECT_UNAUTHORIZED=0 отключает проверку для всего процесса — лучше так не делать в проде.

7.2. Свои корневые сертификаты / клиентские сертификаты (mTLS)

javascript
import https from 'node:https';
import fs from 'node:fs';

const agent = new https.Agent({
  ca:  fs.readFileSync('./ca.pem'),     // свой удостоверяющий центр
  cert: fs.readFileSync('./client.pem'), // клиентский сертификат для mTLS
  key:  fs.readFileSync('./client.key'),
});

7.3. TLS-фингерпринт (JA3) — продвинутый антибот

Современные защиты (Cloudflare, DataDome) умеют различать клиентов по TLS-рукопожатию (JA3/JA4): у Node-клиента оно не такое, как у настоящего Chrome, и это палит бота даже при идеальных заголовках. Чистый Node это «починить» не может; помогают:

  • got-scraping — частично маскирует слой заголовков;
  • CycleTLS — подмена TLS-фингерпринта;
  • честный headless-браузер (Playwright) — даёт «настоящее» TLS-рукопожатие браузера.

8. Использование прокси

Прокси нужны, чтобы распределять нагрузку по IP, обходить гео-ограничения и бан по IP. Типы: HTTP, HTTPS и SOCKS5 (последний универсальнее — гоняет любой трафик и DNS).

8.1. Нативный fetch (важная особенность 2026!)

У нативного fetch нет старой опции { agent }. Прокси задаётся через диспетчер undici — ProxyAgent:

javascript
import { ProxyAgent, setGlobalDispatcher } from 'undici';

// глобально: все fetch пойдут через прокси
setGlobalDispatcher(new ProxyAgent('http://user:pass@proxy.host:8080'));
const res = await fetch('https://example.com');

// или точечно на один запрос
const res2 = await fetch('https://example.com', {
  dispatcher: new ProxyAgent('http://user:pass@proxy.host:8080'),
});

В Node 24+ можно включить чтение HTTP_PROXY/HTTPS_PROXY из окружения флагом NODE_USE_ENV_PROXY=1 (или --use-env-proxy), но явный ProxyAgent надёжнее.

8.2. got / axios через агенты

Через агенты https-proxy-agent и socks-proxy-agent:

javascript
import got from 'got';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { SocksProxyAgent } from 'socks-proxy-agent';

const httpsAgent  = new HttpsProxyAgent('http://user:pass@proxy:8080');
const socksAgent  = new SocksProxyAgent('socks5h://127.0.0.1:9050'); // h = DNS через прокси

const r1 = await got('https://example.com', { agent: { https: httpsAgent } });
const r2 = await got('https://example.com', { agent: { http: socksAgent, https: socksAgent } });

8.3. Ротация и пулы прокси

Для масштаба нужен пул прокси с ротацией и отбраковкой «мёртвых» адресов. Простейший вариант — выбирать случайный/round-robin прокси на каждый запрос. Готовые инструменты:

  • proxy-chain от Apify — поднимает локальный прокси, который пробрасывает на апстрим (в т. ч. с авторизацией, что важно для Chromium, который не умеет логин/пароль в --proxy-server).
  • В Crawlee ротация прокси и сессий встроена (ProxyConfiguration).
javascript
// случайный прокси из пула на каждый запрос
const pool = ['http://p1:8080', 'http://p2:8080', 'http://p3:8080'];
const pick = () => pool[Math.floor(Math.random() * pool.length)];
await fetch(url, { dispatcher: new ProxyAgent(pick()) });

Типы прокси по качеству: datacenter (дёшево, легко палится) → residentialmobile (дорого, почти не банится). Выбор зависит от агрессивности защиты цели.


9. Парсинг через TOR

TOR даёт бесплатную ротацию IP: трафик идёт через цепочку узлов, и можно по команде менять выходной IP. Это удобно для обучения и небольших задач, но у подхода есть серьёзные ограничения (см. конец раздела).

9.1. Настройка

TOR поднимает SOCKS-прокси на порту 9050 и Control-порт 9051 для управления. В конфиге torrc:

code
SocksPort 9050
ControlPort 9051
# пароль получают командой: tor --hash-password "ваш_пароль"
HashedControlPassword 16:....
CookieAuthentication 1

9.2. Запросы через TOR

Просто направляем клиент на локальный SOCKS5 (используйте socks5h, чтобы и DNS резолвился через TOR — иначе утечка реального IP):

javascript
import got from 'got';
import { SocksProxyAgent } from 'socks-proxy-agent';

const agent = new SocksProxyAgent('socks5h://127.0.0.1:9050');
const res = await got('https://httpbin.org/ip', {
  agent: { http: agent, https: agent },
});
console.log(JSON.parse(res.body).origin); // текущий выходной IP TOR

9.3. Смена identity (новый IP)

Чтобы получить новый выходной IP, шлём в Control-порт сигнал NEWNYM. Можно через готовую tor-request, либо вручную обычным TCP-сокетом без зависимостей:

javascript
import net from 'node:net';

function newTorIdentity(password = '') {
  return new Promise((resolve, reject) => {
    const socket = net.connect(9051, '127.0.0.1', () => {
      socket.write(`AUTHENTICATE "${password}"\r\nSIGNAL NEWNYM\r\nQUIT\r\n`);
    });
    socket.once('error', reject);
    socket.once('end', resolve);
    socket.resume();
  });
}

// между запросами:
await newTorIdentity('ваш_пароль');

Важно: TOR держит кулдаун ~10 секунд между сменами цепочки — чаще менять IP не получится.

9.4. Несколько инстансов для пропускной способности

Один TOR = один выходной IP в моменте и медленный кулдаун. Для пула «бесплатных прокси» поднимают несколько процессов TOR на разных портах (9050/9051, 9052/9053, …) и round-robin'ят по ним. Готовый Docker-образ для этого — rotating-tor-http-proxy (несколько инстансов за одним HTTP-эндпоинтом через HAProxy).

9.5. Ограничения (читать обязательно)

  • Выходных узлов TOR около 1500, их списки публичны, и Cloudflare/DataDome/большинство антибот-систем их заранее блокируют — на защищённых целях TOR почти бесполезен.
  • Скорость низкая и нестабильная, новый IP не гарантированно «чистый» и рабочий.
  • Подходит для обучения и небольших, незащищённых целей; для прода берите residential/mobile-прокси.
  • TOR — инструмент приватности; используйте его в рамках закона и правил сайтов.

10. Multi / многопоточность и конкурентность

Здесь важно различать два разных понятия.

10.1. Сначала — асинхронная конкурентность (а не потоки)

Парсинг — задача I/O-bound (ждём сеть). Node одним потоком благодаря событийному циклу легко держит сотни одновременных запросов — настоящие потоки тут чаще всего не нужны. Опасность ровно обратная: запустить Promise.all по 10 000 URL разом и убить и свою сеть, и сервер цели. Поэтому конкурентность ограничивают.

p-limit — ограничитель одновременных задач:

javascript
import pLimit from 'p-limit';

const limit = pLimit(5); // максимум 5 запросов одновременно
const results = await Promise.all(
  urls.map(url => limit(() => scrape(url)))
);

Родственники:

  • p-queue — очередь с приоритетами, интервалами и rate-limit (например, «не больше 10 запросов в секунду»).
  • p-mapmap с ограничением конкурентности.
  • bottleneck — продвинутый rate-limiter (в т. ч. распределённый через Redis).

10.2. worker_threads — для CPU-bound разбора

Если узкое место не сеть, а тяжёлый разбор (парсинг гигантских HTML/JSON, регэкспы, постобработка), есть смысл вынести его в потоки worker_threads, чтобы не блокировать event loop. Удобная обёртка — пулы вроде piscina.

javascript
import { Worker } from 'node:worker_threads';
// каждый воркер парсит свой кусок HTML параллельно, не блокируя основной поток

10.3. cluster / несколько процессов — для масштаба по ядрам

cluster и просто запуск N процессов (часто в Docker) распределяют нагрузку по ядрам CPU и дают отказоустойчивость. На практике для краулера это обычно «несколько воркеров читают из общей очереди (Redis)» — см. §11.

10.4. Авто-масштабирование «из коробки»

Crawlee сам подстраивает конкурентность под доступные CPU/RAM (AutoscaledPool): меньше шанс упасть в маленьком контейнере и выжать максимум в большом.

Практический рецепт: для большинства скраперов — fetch + p-limit/p-queue с лимитом 5–20 одновременных запросов. Потоки/процессы добавляйте, только когда упёрлись в CPU или в один процесс.


11. Хранение URL и очередей (обзорно)

Как только краулер обходит больше одной страницы, появляется frontier — фронт обхода: очередь URL «на посещение» + множество «уже посещённых».

Ключевые задачи:

  • Дедупликация. Нельзя ходить по одному URL дважды. В памяти — обычный Set по нормализованному URL; на больших объёмах — Bloom-фильтр (компактно, ценой редких ложных совпадений), например bloom-filters.
  • Нормализация URL. Приводите к канону (сортировка query, убрать #, трейлинг-слэш, utm-метки), иначе «дубли» расплодятся. Помогает normalize-url.
  • Персистентность. Если процесс упал, очередь не должна теряться. Память не подходит для серьёзных задач.
  • Приоритеты и порядок обхода — в ширину (BFS) или в глубину (DFS), с приоритетом для важных разделов.

Где хранить:

Масштаб Решение
Маленький разовый скрипт Set + массив в памяти
Перезапускаемый одиночный воркер файл / SQLite, или RequestQueue из Crawlee
Несколько воркеров / распределённо Redis (ioredis) как общая очередь + множество посещённых
Промышленная очередь задач BullMQ (репозиторий) поверх Redis: ретраи, задержки, приоритеты, конкурентность

Crawlee предоставляет встроенную персистентную RequestQueue с дедупликацией и обходом в ширину/глубину — если не хотите собирать frontier руками, это самый быстрый путь.

Типовая архитектура «по-взрослому»: Redis/BullMQ как очередь URL → пул воркеров берёт задачи, парсит, кладёт найденные ссылки обратно в очередь (после дедупа) и пишет результат в БД/файл.


12. Готовые фреймворки

Если не хочется собирать всё вышеперечисленное вручную:

  • Crawlee (репозиторий) — главный современный фреймворк для Node.js/TS от Apify. Единый интерфейс для HTTP- и браузерного краулинга (CheerioCrawler, PuppeteerCrawler, PlaywrightCrawler), персистентная очередь URL, ротация прокси и сессий, авто-масштабирование, браузерные фингерпринты «под человека», ретраи. Свежие версии добавляют адаптивный краулер (сам решает, нужен ли JS-рендеринг) и AI-направленные возможности. Требует Node 16+.

```js import { CheerioCrawler } from 'crawlee';

const crawler = new CheerioCrawler({ maxConcurrency: 10, async requestHandler({ $, request, enqueueLinks, pushData }) { await pushData({ url: request.url, title: $('title').text() }); await enqueueLinks(); // сам найдёт ссылки и положит в очередь с дедупом }, }); await crawler.run(['https://example.com']); ```

  • node-crawler — более классический краулер с очередью, лимитами и встроенным Cheerio.
  • x-ray — декларативное извлечение + пагинация.

Для большинства серьёзных проектов на JS ответ по умолчанию сегодня — Crawlee.


13. Антибот, robots.txt, ретраи (то, что часто забывают)

Эти темы в исходном плане не значились, но без них боевой скрапер не живёт.

13.1. Маскировка под обычного клиента

  • Задавайте правдоподобный User-Agent, Accept-Language, Referer. Список реальных UA — user-agents.
  • Генерация согласованных наборов заголовков и фингерпринтов — got-scraping и fingerprint-suite от Apify.
  • На сильных защитах (Cloudflare и т. п.) спасает только честный браузер (Playwright) или подмена TLS-фингерпринта (см. §7.3).

13.2. Вежливость и ретраи

  • Соблюдайте robots.txt там, где это требуется — парсить его помогает robots-parser.
  • Делайте rate limiting и случайные задержки между запросами (p-queue/bottleneck).
  • При 429/503 уважайте Retry-After, используйте экспоненциальный backoff с джиттером, ограничивайте число повторов.
  • Кэшируйте уже скачанное, чтобы при перезапуске не дёргать сайт повторно.

14. Основные плюсы и минусы реализации на JavaScript

Плюсы

  • Один язык со страницей. Сайты написаны на JS — селекторы, DOM-логику и даже исполнение скриптов страницы удобно держать в той же среде.
  • Лучшие headless-браузеры — нативно JS. Playwright и Puppeteer — first-class граждане Node; для тяжёлой динамики это сильное преимущество перед другими экосистемами.
  • Асинхронность из коробки. Событийный цикл идеально ложится на I/O-bound скрапинг: высокая конкурентность одним процессом без возни с потоками.
  • Зрелая экосистема. fetch/undici, Cheerio, Crawlee, BullMQ, готовые агенты прокси — всё под рукой.
  • Crawlee закрывает «оркестрацию» (очереди, прокси, фингерпринты, масштабирование) почти без кода.

Минусы

  • CPU-bound разбор (огромные документы, тяжёлая постобработка) для однопоточного Node — слабое место; нужны worker_threads/несколько процессов, тогда как в Go/Rust это проще.
  • Прожорливость браузеров. Playwright/Puppeteer едят много RAM/CPU; на масштабе это ощутимые расходы.
  • Колбэк/промис-ад при ручной оркестрации без фреймворка легко превращается в спагетти.
  • TLS-фингерпринт. Node-клиенты палятся по JA3/JA4; «починить» это чистым Node сложнее, чем кажется (нужны CycleTLS или браузер).
  • Хрупкость селекторов. Это общая беда скрапинга (вёрстка меняется), но JS-экосистема не избавляет от ручного сопровождения CSS/XPath-селекторов.
  • Data science вокруг данных. Python с pandas/numpy сильнее в последующей аналитике собранного — иногда удобнее «собрать на JS, обработать на Python».

Когда JS — хороший выбор: динамические сайты, потребность в headless-браузере, команда уже на Node, нужна высокая I/O-конкурентность и/или интеграция с веб-сервисами на JS. Когда подумать об альтернативе: чисто CPU-bound обработка терабайтов HTML или плотная интеграция с Python-аналитикой.