Обзорная статья о том, как на Node.js забирать страницы, доставать из них данные и масштабировать всё это до боевого скрапера: кодировки, многопоточность, прокси, TOR, SSL, cookie, заголовки, очереди URL и подводные камни. С ссылками на официальные библиотеки.
Оглавление
- Что такое парсинг и из чего он состоит
- Как забираем страницу: HTTP-клиенты
- Библиотеки для парсинга содержимого
- Получение статуса ответа и других заголовков
- Решение проблем с парсингом кириллицы
- Работа с cookie
- Работа с HTTPS / SSL
- Использование прокси
- Парсинг через TOR
- Multi / многопоточность и конкурентность
- Хранение URL и очередей (обзорно)
- Готовые фреймворки
- Антибот, robots.txt, ретраи (то, что часто забывают)
- Основные плюсы и минусы реализации на JavaScript
1. Что такое парсинг и из чего он состоит
Парсинг сайта почти всегда раскладывается на три независимых слоя, и проектировать скрапер удобно именно по этим слоям:
- Транспорт — как получить байты страницы (HTTP-клиент или headless-браузер).
- Извлечение — как из HTML/JSON достать нужные поля (парсер DOM, селекторы).
- Оркестрация — как обойти много 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 для базовых задач больше не нужны.
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():
const res = await fetch(url, { signal: AbortSignal.timeout(15_000) });
2.2. undici напрямую — когда нужна максимальная скорость
undici — это и есть «движок» нативного fetch, но его
низкоуровневый API (request, пулы соединений, pipelining) в бенчмарках обгоняет fetch,
axios и got в несколько раз. Имеет смысл, когда вы упёрлись в пропускную способность.
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.
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-fetch— legacy, нужен только на очень старых 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.
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, чуть проще, огромная экосистема.
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:
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.
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); // корректная кириллица
Если кодировка нигде не объявлена, её можно определить эвристически:
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-сущности (
,П) — нормальные парсеры (Cheerio, parse5) раскодируют их за вас.
6. Работа с cookie
Cookie нужны для авторизованных зон, сессий, корзин и обхода «первого визита». Есть три уровня.
6.1. Вручную через заголовки
const res = await fetch(url, { headers: { cookie: 'sid=abc123; lang=ru' } });
const setCookie = res.headers.get('set-cookie'); // распарсить и отправить дальше
Подходит для простых случаев, но вручную поддерживать набор cookie между запросами больно.
6.2. Cookie jar (рекомендуется)
tough-cookie — эталонная реализация
cookie-хранилища с учётом домена, пути, срока жизни и флагов. Многие клиенты интегрированы с
ним из коробки.
got принимает jar напрямую и сам ведёт сессию:
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 живут в контексте, и их можно сохранять/восстанавливать — удобно, чтобы один раз залогиниться и переиспользовать сессию:
// сохранить состояние (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-прокси или с тестовым стендом). Делайте это осознанно — это снимает защиту от подмены трафика.
// 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)
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:
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:
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).
// случайный прокси из пула на каждый запрос
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 (дёшево, легко палится) → residential → mobile (дорого, почти не банится). Выбор зависит от агрессивности защиты цели.
9. Парсинг через TOR
TOR даёт бесплатную ротацию IP: трафик идёт через цепочку узлов, и можно по команде менять выходной IP. Это удобно для обучения и небольших задач, но у подхода есть серьёзные ограничения (см. конец раздела).
9.1. Настройка
TOR поднимает SOCKS-прокси на порту 9050 и Control-порт 9051 для управления. В
конфиге torrc:
SocksPort 9050
ControlPort 9051
# пароль получают командой: tor --hash-password "ваш_пароль"
HashedControlPassword 16:....
CookieAuthentication 1
9.2. Запросы через TOR
Просто направляем клиент на локальный SOCKS5 (используйте socks5h, чтобы и DNS резолвился
через TOR — иначе утечка реального IP):
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-сокетом без
зависимостей:
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 — ограничитель одновременных задач:
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-map—mapс ограничением конкурентности.bottleneck— продвинутый rate-limiter (в т. ч. распределённый через Redis).
10.2. worker_threads — для CPU-bound разбора
Если узкое место не сеть, а тяжёлый разбор (парсинг гигантских HTML/JSON, регэкспы,
постобработка), есть смысл вынести его в потоки
worker_threads, чтобы не блокировать event
loop. Удобная обёртка — пулы вроде piscina.
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-аналитикой.