Если вы живёте в мире JavaScript, у вас уже есть почти всё необходимое для извлечения структурированных данных из интернета. Node.js поставляет быстрый рантайм, огромную экосистему пакетов и асинхронную модель ввода-вывода, справляющуюся со множеством запросов без труда. Добавьте поверх две небольшие библиотеки, и у вас готовый скрейпер за несколько десятков строк.
Это руководство показывает, как создать веб-скрейпер с Node.js практическим способом. Мы начинаем со стандартного стека: axios для HTTP и cheerio для HTML-парсинга в стиле jQuery, и строим скрейпер, загружающий страницу, выбирающий нужные поля, обходящий список и записывающий результаты в JSON и CSV. Затем честно разберёмся, где обычный HTTP заходит в тупик (JavaScript-рендеренные страницы и блокировки в масштабе) и что с этим делать.
Что вы создадите
Небольшой Node.js-скрипт, принимающий URL, скачивающий HTML, парсящий его с помощью cheerio и извлекающий чистую запись на каждый элемент. Мы будем использовать обобщённый макет листинга продуктов как рабочий пример, поскольку этот паттерн (повторяющаяся карточка с названием, ценой и ссылкой) охватывает большинство реальных задач скрейпинга. К концу у вас будет:
- Загрузчик одной страницы, построенный на axios и cheerio.
- Экстрактор, отображающий CSS-селекторы на поля.
- Цикл, обходящий страницы пагинированного списка и собирающий все строки.
- Вывод в JSON и CSV.
- Путь замены для страниц, блокирующих вас или рендерящих на клиенте.
Почему Node.js для скрейпинга
Node.js запускает JavaScript вне браузера на движке V8 от Chrome, компилирующем в машинный код и остающемся быстрым. Его неблокирующая, событийно-ориентированная модель отлично подходит для скрейпинга, где большую часть времени вы ожидаете ответов от сети: можно держать множество запросов в полёте в одном потоке без создания потока на каждое соединение. Добавьте npm-экосистему, где почти каждая потребность в парсинге, очередях или хранилище уже имеет проверенный пакет, и вы получаете рантайм, созданный для такой работы. Компании вроде Netflix и PayPal используют Node.js в продакшне по тем же причинам.
Две библиотеки, берущие на себя основную работу при статическом скрейпинге: axios (промис-ориентированный HTTP-клиент) и cheerio (лёгкий парсер, дающий jQuery-селекторы над серверным HTML без браузера). Если хотите вспомнить именно сторону запросов, см. статью как делать HTTP-запросы в Node.js с Fetch API.
Предварительные требования
Ничего экзотического. Вам нужны три вещи перед написанием кода.
Базовый JavaScript и Node.js. Вы должны уметь писать скрипт, запускать его из терминала и устанавливать пакеты через npm. Async/await сделает код читаемым, поэтому базовые знания промисов помогут.
Node.js 18 или новее. Проверьте версию командой node --version. Если не установлен, скачайте текущую LTS с nodejs.org.
Редактор кода. Подойдёт любой; VS Code, популярный выбор.
Настройка проекта
Создайте папку, инициализируйте проект и установите две зависимости.
mkdir node-scraper && cd node-scraper npm init -y npm install axios cheerio
Для использования современного синтаксиса import добавьте "type": "module" в package.json. Если предпочитаете require, код ниже работает так же с CommonJS: просто замените строки импорта на const axios = require("axios").
Шаг 1: Загрузка страницы
Начнём с загрузки сырого HTML. axios возвращает тело ответа в response.data. Установка реалистичного заголовка User-Agent делает запрос похожим на браузерный, а не на стандартный Node-клиент, к которому многие сайты относятся подозрительно.
import axios from "axios"; const fetchPage = async (url) => { const { data } = await axios.get(url, { headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", }, timeout: 15000, }); return data; };
Параметр timeout не даёт медленному или недоступному хосту зависнуть на вечно. axios отклоняет промис при любом статусе, отличном от 2xx, поэтому оборачивание вызовов в try/catch (делаем это позже) делает сбои видимыми, а не молчаливыми.
Шаг 2: Парсинг и извлечение с помощью cheerio
cheerio загружает HTML-строку и предоставляет функцию $, работающую как jQuery. Вы выбираете элементы по CSS-селектору и читаете их текст или атрибуты. Паттерн таков: находим повторяющийся контейнер, затем для каждого извлекаем нужные поля.
import * as cheerio from "cheerio"; const parseProducts = (html) => { const $ = cheerio.load(html); const products = []; $(".product-card").each((_, el) => { const card = $(el); products.push({ title: card.find("h2.title").text().trim(), price: card.find(".price").text().trim(), url: card.find("a").attr("href"), }); }); return products; };
Здесь важны два момента. .text() возвращает объединённый текст элемента, поэтому .trim() убирает пробельные символы, оставляемые разметкой вокруг него. Чтение ссылки использует .attr("href"), а не .text(), потому что нужное значение находится в атрибуте, а не в видимом тексте. Настройте селекторы (.product-card, h2.title, .price) под страницу, которую вы реально целите; осмотрите её в инструментах разработчика браузера, чтобы найти правильные.
Имена классов меняются, когда сайт выпускает редизайн, и селектор, работавший в прошлом месяце, может тихо возвращать пустые строки. Относитесь к селекторам как к чему-то, что нужно поддерживать, а не настраивать один раз. Когда поле возвращается пустым, заново проинспектируйте живую страницу и обновите его. Периодическое обслуживание селекторов, норма для любого продакшн-скрейпера.
Шаг 3: Цикл по страницам списка
Одна страница, это демо. Реальная задача обходит пагинацию. Большинство пагинированных листингов отображают номер страницы в URL (?page=2) или ссылку "следующая". Самый простой надёжный подход, итерировать известный диапазон страниц, загружать каждую, парсить и останавливаться, когда страница возвращает пустой результат.
const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const scrapeAll = async (baseUrl, maxPages = 10) => { const all = []; for (let page = 1; page <= maxPages; page++) { try { const html = await fetchPage(`${baseUrl}?page=${page}`); const rows = parseProducts(html); if (rows.length === 0) break; all.push(...rows); console.log(`Page ${page}: ${rows.length} items`); } catch (err) { console.error(`Page ${page} failed: ${err.message}`); } await sleep(1000); } return all; };
Задержка sleep между запросами, не декорация. Пауза в одну секунду не позволяет вам долбить сервер в плотном цикле, что и вежливо, и является самым быстрым способом избежать троттлинга. try/catch означает, что одна плохая страница записывает ошибку и выполнение продолжается, вместо того чтобы упасть на элементе 4 из 200.
Шаг 4: Запись в JSON и CSV
Собранные данные полезны только после выхода из памяти. JSON, вариант по умолчанию без зависимостей; встроенный модуль Node.js fs записывает его напрямую. Плоский CSV так же прост вручную и открывается прямо в таблице.
import { writeFileSync } from "fs"; const saveJson = (rows, file) => writeFileSync(file, JSON.stringify(rows, null, 2)); const saveCsv = (rows, file) => { const headers = Object.keys(rows[0]); const escape = (v) => `"${String(v ?? "").replace(/"/g, '""')}"`; const lines = [ headers.join(","), ...rows.map((r) => headers.map((h) => escape(r[h])).join(",")), ]; writeFileSync(file, lines.join("\n")); };
Вспомогательная функция escape оборачивает каждое значение в кавычки и удваивает внутренние кавычки, это правило CSV, не позволяющее запятой или кавычке внутри названия продукта сдвигать столбцы. Для чего-то более сложного (вложенные данные, большие объёмы) возьмите библиотеку вроде csv-stringify, но для плоского набора записей этого достаточно.
Сборка воедино
Соедините четыре части в один запускаемый скрипт.
const main = async () => { const rows = await scrapeAll("https://example.com/products"); if (rows.length === 0) { console.log("No data collected."); return; } saveJson(rows, "products.json"); saveCsv(rows, "products.csv"); console.log(`Saved ${rows.length} items.`); }; main();
Запустите командой node scraper.js. Вы получаете строку прогресса на каждую страницу и два файла на диске. Это полный статический скрейпер чуть менее чем в сотне строк.
Где статический HTTP заходит в тупик
Стек axios плюс cheerio быстр и чист, и для серверно-рендеренных страниц этого достаточно. Но на реальных целях быстро появляются две стены.
JavaScript-рендеренный контент. Многие современные сайты отправляют почти пустую HTML-оболочку и строят страницу в браузере с помощью JavaScript. axios загружает только эту начальную оболочку; он не выполняет скрипты, поэтому cheerio не находит ничего там, где должны быть данные. Если ваши селекторы возвращают пустое значение на странице, явно отображающей контент в браузере, почти всегда именно это и является причиной.
Блокировки в масштабе. Несколько запросов с вашего IP, нормально. Несколько сотен с одного адреса датацентра в узнаваемых паттернах, и вы попадаете под троттлинг, CAPTCHA или прямую блокировку. Кастомный User-Agent даёт немного пространства; он не решает проблему IP.
У вас есть два пути вперёд. Первый, headless-браузер: Puppeteer или Playwright управляет реальным Chrome или Firefox, выполняет JavaScript страницы и позволяет скрейпить рендеренный DOM. Это решает проблему рендеринга, но тяжело: каждый экземпляр, полноценный браузер, потребляющий память и CPU, и в масштабе всё равно нужно управлять прокси-пулом самостоятельно, чтобы оставаться незаблокированным. Если это ваш путь, см. наше руководство по веб-скрейпингу с Playwright.
Второй, переложить обе проблемы на API.
Использование Crawling API для отрендеренных, незаблокированных страниц
Crawling API объединяет рендеринг и ротацию IP в один запрос. Вы отправляете ему URL, он загружает страницу за надёжным ротируемым резидентным IP (при необходимости сначала рендерит JavaScript) и возвращает готовый HTML. Ваш существующий парсер cheerio остаётся неизменным; меняется только шаг загрузки.
Установите официальный Node-клиент.
npm install crawlbase
Затем замените fetchPage версией, использующей API. Всё последующее (парсинг, цикл, сохранение) остаётся точно таким, каким вы написали.
import { CrawlingAPI } from "crawlbase"; import * as cheerio from "cheerio"; const api = new CrawlingAPI({ token: "YOUR_CRAWLBASE_TOKEN" }); const fetchPage = async (url) => { const response = await api.get(url, { ajax_wait: true, page_wait: 3000 }); if (response.statusCode === 200 && response.pcStatus === 200) { return response.body; } throw new Error(`Crawl failed: ${response.statusCode} / ${response.pcStatus}`); };
Два важных момента. Клиент возвращает как statusCode (ответ от целевого сайта), так и pcStatus (успешность самого краулинга); проверка обоих не даёт мягкому сбою пройти как хорошему HTML. Параметры ajax_wait и page_wait обрабатывают JavaScript-рендеренные цели: ajax_wait говорит API ждать асинхронного контента, а page_wait выдерживает несколько секунд после загрузки, чтобы поздние элементы появились до захвата. Уберите оба параметра для обычных статических страниц, и вы получите те же преимущества ротации без накладных расходов на рендеринг.
Токены Crawlbase бывают двух видов. Обычный токен загружает статический HTML и подходит для серверно-рендеренных страниц. JavaScript (JS) токен сначала рендерит страницу в настоящем браузере, что нужно для клиентско-рендеренных целей. Если страница возвращается пустой оболочкой с обычным токеном, переключитесь на JS-токен.
Не нужно запускать флот headless-браузеров и управлять собственным прокси-пулом. Crawling API рендерит JavaScript при необходимости, ротирует резидентные IP на стороне сервера и возвращает готовый HTML за один вызов, чтобы ваш парсер cheerio продолжал работать без изменений. Начните с бесплатного тарифа и укажите на страницы, которые вас блокировали.
Советы для здорового скрейпера
Несколько привычек поддерживают Node.js-скрейпер в рабочем состоянии, независимо от того, остаётесь ли вы на axios или переходите на API.
- Сначала читайте условия использования сайта и robots.txt. Знайте, что разрешено собирать и с какой частотой, прежде чем направить на это цикл.
- Делайте паузы между запросами. Задержка между вызовами и разумный уровень параллелизма не позволяют вам перегружать сервер и не выглядеть как атака.
-
Отправляйте реалистичные заголовки. Браузерный
User-Agentи стандартные заголовки accept снижают вероятность определения как бота. - Обрабатывайте ошибки на элемент. Оборачивайте каждый запрос так, чтобы один сбой записывался в лог и выполнение продолжалось, а не убивало весь запуск.
- Кешируйте во время разработки. Сохраняйте загруженный HTML на диск при итерации над селекторами, чтобы не повторно обращаться к сайту при каждом изменении кода.
- Следите за кодами статусов. Растущая доля CAPTCHA или ответов 4xx, сигнал замедлиться или ротировать IP, а не шум для игнорирования.
Для полного руководства по борьбе с блокировками, включая ротацию IP и фингерпринтинг, см. статью как скрейпить сайты без блокировок. Если предпочитаете маршрутизировать собственный трафик через ротируемый пул, а не использовать управляемый API, Smart AI Proxy (также называемый AI Proxy) предоставляет ротацию резидентных IP как готовую прокси-конечную точку.
Ключевые выводы
- axios плюс cheerio, статический стек. Загружайте HTML с axios, парсите его jQuery-стилевыми селекторами cheerio, и у вас готовый скрейпер менее чем в ста строках.
- Паттерн: загрузить, выбрать, зациклить, сохранить. Найдите повторяющийся контейнер, отобразите селекторы на поля, обходите пагинацию с задержкой и записывайте в JSON и CSV.
- Статический HTTP имеет два ограничения. Не может выполнять JavaScript, поэтому пропускает клиентский контент, и один IP блокируется в масштабе.
- Puppeteer и Playwright решают рендеринг, но тяжелы. Полноценный браузер на экземпляр стоит памяти и CPU, и вы всё равно управляете прокси самостоятельно.
- Crawling API объединяет оба решения. Один вызов возвращает отрендеренный HTML за ротируемым резидентным IP, а парсер cheerio остаётся неизменным.
Часто задаваемые вопросы
Хорош ли Node.js для веб-скрейпинга?
Да. Node.js запускает JavaScript на быстром движке V8, и его неблокирующая модель ввода-вывода позволяет держать множество сетевых запросов в полёте в одном потоке, что именно то, что нужно скрейпингу. Экосистема npm также предоставляет зрелые библиотеки для каждого шага: от HTTP-запросов до HTML-парсинга и headless-браузеров, поэтому большинство задач быстро складываются.
В чём разница между axios и cheerio?
Они выполняют разные половины задачи. axios, HTTP-клиент: загружает сырой HTML страницы по сети. cheerio, парсер: загружает эту HTML-строку и предоставляет CSS-селекторы в стиле jQuery для извлечения нужных полей. Вы почти всегда используете их вместе: axios для скачивания, cheerio для извлечения.
Почему cheerio возвращает пустые результаты на некоторых страницах?
Обычно потому что страница рендерит контент на клиенте с помощью JavaScript. axios загружает только начальную HTML-оболочку, а cheerio парсит то, что получил, поэтому если данные вставляются скриптами после загрузки, там просто нечего искать. Исправление, сначала отрендерить страницу, либо с помощью headless-браузера вроде Puppeteer или Playwright, либо с помощью Crawling API с его опциями JavaScript-рендеринга.
Как избежать блокировок при скрейпинге с Node.js?
Делайте паузы между запросами, отправляйте реалистичные заголовки браузера, держите параллелизм разумным и ротируйте IP-адреса, чтобы ни один не превысил лимит частоты запросов. Кастомный User-Agent помогает, но сам по себе не решает проблему IP. Ротируемые резидентные прокси или управляемый сервис вроде Crawling API обрабатывают ротацию за вас, чтобы вам не нужно было поддерживать прокси-пул.
Использовать Puppeteer или Crawling API?
Используйте Puppeteer (или Playwright), когда вам нужен детальный контроль над реальным браузером, например для прохождения многошаговых процессов или захвата скриншотов, и вы готовы сами запускать и масштабировать браузеры. Используйте Crawling API, когда вам нужен отрендеренный незаблокированный HTML в масштабе без управления headless-флотом и прокси-пулом. Многие команды прототипируют с headless-браузером и переходят на API, когда растут объёмы и частота блокировок.
Можно ли записывать скрейпинговые данные в базу данных вместо файлов?
Да. Собранные записи, обычные JavaScript-объекты, поэтому после получения массива вы можете вставить его куда угодно: в JSON-файл, CSV или базу данных вроде PostgreSQL, MongoDB или SQLite, используя их Node.js-драйверы. Шаг сохранения независим от логики скрейпинга, поэтому замените saveJson на вызов вставки, не затрагивая код загрузки или парсинга.
Обходите любой сайт в масштабе, без борьбы с инфраструктурой.
Crawlbase берёт на себя прокси, отпечатки и CAPTCHA, чтобы ваша команда выпускала конвейеры данных вместо поддержки обвязки краулинга. 1 000 запросов бесплатно, без карты.

