Если вы живёте в мире 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, популярный выбор.

Настройка проекта

Создайте папку, инициализируйте проект и установите две зависимости.

bash
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-клиент, к которому многие сайты относятся подозрительно.

javascript
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-селектору и читаете их текст или атрибуты. Паттерн таков: находим повторяющийся контейнер, затем для каждого извлекаем нужные поля.

javascript
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) или ссылку "следующая". Самый простой надёжный подход, итерировать известный диапазон страниц, загружать каждую, парсить и останавливаться, когда страница возвращает пустой результат.

javascript
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 так же прост вручную и открывается прямо в таблице.

javascript
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, но для плоского набора записей этого достаточно.

Сборка воедино

Соедините четыре части в один запускаемый скрипт.

javascript
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-клиент.

bash
npm install crawlbase

Затем замените fetchPage версией, использующей API. Всё последующее (парсинг, цикл, сохранение) остаётся точно таким, каким вы написали.

javascript
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 выдерживает несколько секунд после загрузки, чтобы поздние элементы появились до захвата. Уберите оба параметра для обычных статических страниц, и вы получите те же преимущества ротации без накладных расходов на рендеринг.

Обычный токен против JS-токена

Токены Crawlbase бывают двух видов. Обычный токен загружает статический HTML и подходит для серверно-рендеренных страниц. JavaScript (JS) токен сначала рендерит страницу в настоящем браузере, что нужно для клиентско-рендеренных целей. Если страница возвращается пустой оболочкой с обычным токеном, переключитесь на JS-токен.

Crawlbase Crawling API

Не нужно запускать флот 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 запросов бесплатно, без карты.

Самообслуживание · Звонок отдела продаж не требуется · Доступны корпоративные объёмы краулинга