Введение

В этом руководстве вы пройдете полный путь от пустой папки на компьютере до рабочего скрипта, который парсит карточки товаров Amazon через Playwright и мобильные прокси. Мы пошагово разберем установку Node.js и всех зависимостей, подключим stealth-плагины для снижения вероятности блокировок, настроим мобильные прокси с авторизацией по IP и логин/пароль, добавим эмуляцию реального поведения, рандомизацию задержек и ротацию User-Agent. Вы научитесь собирать название товара, цену, рейтинг, количество отзывов, ASIN, изображения и описание, а затем сохранять результат в JSON и CSV. Дополнительно внедрим обработку ошибок, распознаем CAPTCHA, реализуем retry с экспоненциальной задержкой и настроим масштабирование на сотни товаров.

Гайд рассчитан на начинающих с элементами для продвинутых. Мы объясняем простым языком, фиксируем каждый шаг и показываем, как проверить результат. Среднее время прохождения — от 4 до 8 часов, включая установку и тестирование. После завершения у вас будет воспроизводимый проект, который можно запустить на Windows, macOS или Linux. Также вы получите набор советов, как ускорить сбор данных без роста блокировок.

⚠️ Внимание: Скрейпинг может нарушать правила использования сайтов. Перед запуском убедитесь, что вы соблюдаете условия, действующее законодательство и требования к использованию данных в вашем регионе.

Совет: Сохраняйте промежуточные результаты и ведите заметки: какие прокси использовали, какие настройки задержек работали лучше, какие маркеты Amazon вы тестировали. Это ускорит отладку и масштабирование.

Предварительная подготовка

Прежде чем писать код, подготовим окружение и доступы. Вам понадобятся: доступ к компьютеру с интернетом, права на установку программ, учетная запись провайдера мобильных прокси, базовые знания командной строки. Даже если вы совсем новичок, не переживайте — ниже все описано щепетильно.

Необходимые инструменты и доступы

  • Node.js версии 18 или выше (рекомендуется 20 LTS).
  • Playwright и библиотека playwright-extra со stealth-плагином.
  • Аккаунт мобильных прокси mobileproxy.space с активным тарифом.
  • Редактор кода (подойдет любой, например, Visual Studio Code).
  • Командная строка: Terminal на macOS и Linux, PowerShell или CMD на Windows.

Системные требования

  • Операционная система: Windows 10/11, macOS 12+, Ubuntu 20.04+ или аналогичный Linux.
  • Оперативная память: от 8 ГБ (рекомендуется 16 ГБ для параллельного парсинга).
  • Диск: минимум 5 ГБ свободного места (браузерные бинарники Playwright занимают заметный объем).
  • Стабильный интернет, желательно проводной.

Что скачать и установить

  • Node.js: установите официальный дистрибутив и проверьте версии командой node -v и npm -v.
  • Playwright и браузеры: установим через npm в процессе.
  • Редактор кода: установите и откройте папку проекта.

Совет: Установите Git, чтобы версионировать проект и легко откатывать изменения. Это не обязательно, но очень полезно.

✅ Проверка: Команда node -v должна вывести версию (например, v20.11.0). Если команда не найдена, повторите установку Node.js и перезагрузите терминал.

Базовые понятия

Прежде чем писать скрипты, проясним ключевые термины и общие принципы работы. Это поможет избежать ошибок и быстрее понимать, что и зачем мы настраиваем.

Ключевые термины

  • Парсинг — автоматическое извлечение данных со страниц сайта, например, названия, цен, рейтингов.
  • Playwright — библиотека для управления браузерами (Chromium, Firefox, WebKit) через код. Позволяет нажимать кнопки, вводить текст, прокручивать страницу.
  • Stealth — набор техник, снижающих вероятность распознавания автоматизации на сайте (скрытие автоматизации, правдоподобные параметры браузера).
  • Мобильные прокси — прокси-серверы с мобильными IP-адресами, которые чаще считаются «человеческими» и реже блокируются.
  • ASIN — уникальный идентификатор товара на Amazon.
  • CAPTCHA — проверка «Вы робот?»; часто появляется при подозрительной активности.
  • Retry — повторная попытка запроса при ошибке, часто с увеличением задержки (экспоненциальный backoff).

Основные принципы

  • Не спешить: соблюдайте задержки, имитируйте реальные действия.
  • Делайте скролл, hover и клики — это приближает поведение к пользователю.
  • Рандомизируйте User-Agent и параметры контекста.
  • Используйте прокси, лучше мобильные, и периодически меняйте IP.
  • Обрабатывайте ошибки и добавляйте повторные попытки.

⚠️ Внимание: Большие объемы запросов без задержек почти гарантированно приведут к блокировкам и возможному появлению CAPTCHA. Уважайте ресурсы сайта и соблюдайте ограничения нагрузки.

✅ Проверка: Если вам понятны термины Playwright, прокси, ASIN, stealth и retry, можно переходить дальше. Если нет — перечитайте определения еще раз и держите их под рукой.

Шаг 1: Установка Node.js и подготовка проекта

Цель этапа

Подготовить рабочую папку и базовые файлы проекта Node.js, убедиться, что окружение готово к установке зависимостей.

Пошаговая инструкция

  1. Создайте папку проекта, например amazon-playwright-scraper.
  2. Откройте папку в терминале.
  3. Инициализируйте проект командой npm init -y. Будет создан файл package.json.
  4. Создайте подпапки src и data. В src будет код, в data — входные и выходные данные.
  5. Создайте файл .env в корне проекта для хранения конфиденциальных настроек: прокси, регион, константы. Пока оставьте пустым.
  6. Установите dotenv командой npm i dotenv для загрузки переменных окружения.
  7. Создайте файл src/index.js — это будет точка входа.
  8. Создайте файл data/input_asins.txt и добавьте несколько ASIN, по одному в строке (например, B0C61JQSG7, B08N5WRWNW). Эти ASIN вы можете взять из адресной строки карточек товара или из блока характеристик.

Важные моменты

  • Структура проекта облегчает поддержку. Разделяйте код и данные.
  • .env не коммитьте в общий репозиторий. В нем будут пароли и конфиденциальные настройки.

Совет: Сразу добавьте .gitignore и включите туда node_modules, datahost:port'.

  • В режиме логин/пароль нужно указать server, username и password при запуске браузера.
  • Мобильные IP часто меняются автоматически по расписанию. Это плюс для антибота, но помните про стабильность сеанса.
  • Совет: Добавьте несколько прокси в список PROXY_SERVERS, чтобы у вас был запас для ротации и параллельных воркеров. Разделяйте их запятыми.

    Ожидаемый результат

    .env содержит корректные переменные, вы понимаете, как работает авторизация и как менять IP.

    Возможные проблемы и решения

    • Авторизация по IP не срабатывает. Решение: проверьте, какой внешний IP у вашей машины, добавьте его в белый список провайдера, перезапустите сессию.
    • Неверный логин/пароль. Решение: проверьте учетные данные, сбросьте пароль в кабинете провайдера при необходимости.

    ✅ Проверка: В файле .env указаны PROXY_MODE, PROXY_SERVERS и MARKET. Вы уверены, что прокси активны и IP авторизован или логин/пароль верны.

    Шаг 4: Базовый скрипт запуска браузера с прокси и stealth

    Цель этапа

    Создать минимальный рабочий скрипт, который запускает Chromium через playwright-extra со stealth-плагином, использует мобильный прокси, открывает главную страницу Amazon и корректно закрывается.

    Пошаговая инструкция

    1. Откройте файл src/index.js.
    2. Добавьте базовый код импорта и инициализации: подключите dotenv, playwright-extra и stealth-плагин.
    3. Считайте переменные окружения из .env: режим прокси, список серверов, учетные данные, домен MARKET.
    4. Реализуйте функцию getProxyOptions, которая возвращает объект proxy для Playwright в зависимости от выбранного сервера и режима авторизации.
    5. Реализуйте простой запуск браузера с chromium.launch и проверкой, что страница MARKET загружается без ошибок.
    6. Закройте браузер и залогируйте успешность шага.

    Пример кода для src/index.js (минимальный тест)

    Скопируйте и вставьте, затем адаптируйте под себя:

    require('dotenv').config();
    const { chromium } = require('playwright-extra');
    const stealth = require('puppeteer-extra-plugin-stealth')();
    chromium.use(stealth);
    const { setTimeout: sleep } = require('timers/promises');
    const market = process.env.MARKET || 'https:/www.amazon.com';
    const proxyMode = process.env.PROXY_MODE || 'ip';
    const proxyServers = (process.env.PROXY_SERVERS || '').split(',').map(s => s.trim()).filter(Boolean);
    const proxyUser = process.env.PROXY_USER || '';
    const proxyPass = process.env.PROXY_PASS || '';
    function pickProxy() {
     if (!proxyServers.length) return null;
     const i = Math.floor(Math.random() * proxyServers.length);
     return proxyServers[i];
    }
    function getProxyOptions(server) {
     if (!server) return {};
     const [host, port] = server.split(':');
     const proxyUrl = 'http:' + host + ':' + port;
     if (proxyMode === 'login') {
    return { server: proxyUrl, username: proxyUser, password: proxyPass };
     }
     return { server: proxyUrl };
    }
    (async () => {
     const server = pickProxy();
     const proxy = getProxyOptions(server);
     const browser = await chromium.launch({ headless: true, proxy });
     const context = await browser.newContext({
    viewport: { width: 1366, height: 768 },
    userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'
     });
     const page = await context.newPage();
     await page.goto(market, { waitUntil: 'domcontentloaded', timeout: 45000 });
     await sleep(1000 + Math.random() * 2000);
     await browser.close();
     console.log('Base launch OK via proxy:', server || 'no-proxy');
    })().catch(err => {
     console.error('Base launch failed', err);
     process.exit(1);
    });

    Важные моменты

    • Для смены прокси в Playwright обычно требуется новый процесс браузера. Планируйте это при масштабировании.
    • stealth подключается через chromium.use(stealth). Это должно быть до launch.

    Совет: На этапе отладки используйте headless: false, чтобы видеть, что происходит в окне браузера. При массовом парсинге верните headless: true.

    Ожидаемый результат

    Скрипт открывает главную страницу маркетплейса Amazon через ваш прокси, затем закрывает браузер без ошибок.

    Возможные проблемы и решения

    • Страница не открывается. Причина: прокси недоступен. Решение: замените сервер на рабочий или временно запустите без прокси.
    • Ошибка авторизации прокси. Причина: неверный логин/пароль или IP. Решение: перепроверьте .env, исправьте данные.

    ✅ Проверка: В консоли выводится Base launch OK via proxy и адрес прокси (или no-proxy, если не настроено). Ошибок нет.

    Шаг 5: Логика парсинга карточки товара Amazon

    Цель этапа

    Научиться открывать страницу товара по ASIN, корректно выбирать селекторы, извлекать название, цену, рейтинг, количество отзывов, ASIN, изображения и описание. Реализуем одну универсальную функцию parseProduct.

    Пошаговая инструкция

    1. Создайте файл src/scraper.js для логики парсинга.
    2. Реализуйте функцию buildProductUrl(market, asin), которая возвращает ссылку вида market + '/dp/' + asin.
    3. Добавьте функцию extractors с безопасным получением текста по селекторам с несколькими fallback.
    4. Реализуйте parseProduct(page, asin), которая открывает карточку и возвращает объект данных.
    5. Подключите scraper.js в index.js и протестируйте на 1–2 ASIN.

    Пример кода для src/scraper.js

    Скопируйте и адаптируйте:

    const { setTimeout: sleep } = require('timers/promises');
    function buildProductUrl(market, asin) {
     return market.replace(/\/+$/, '') + '/dp/' + asin;
    }
    async function getText(page, selectors) {
     for (const sel of selectors) {
    try {
     const el = await page.locator(sel).first();
     if (await el.count()) {
    const txt = (await el.innerText()).trim();
    if (txt) return txt;
     }
    } catch {}
     }
     return '';
    }
    async function getAttrAll(page, selector, attr) {
     try {
    const els = await page.locator(selector);
    const count = await els.count();
    const out = [];
    for (let i = 0; i < count; i++) {
     const el = els.nth(i);
     const v = await el.getAttribute(attr);
     if (v) out.push(v);
    }
    return out;
     } catch {
    return [];
     }
    }
    function normalizePrice(str) {
     if (!str) return '';
     const m = str.replace(/[\s,]/g, '').match(/([0-9]+(?:\.[0-9]{1,2})?)/);
     return m ? m[1] : '';
    }
    function parseReviewsCount(str) {
     if (!str) return 0;
     const n = str.replace(/[^0-9]/g, '');
     return Number(n || '0');
    }
    async function parseProduct(page, market, asin) {
     const url = buildProductUrl(market, asin);
     await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
     try {
    await page.waitForTimeout(500 + Math.random() * 700);
    const accept = page.locator('input#sp-cc-accept, button#sp-cc-accept, input[name="accept"]');
    if (await accept.count()) {
     await accept.click({ delay: 50 + Math.random() * 60 });
     await page.waitForTimeout(300 + Math.random() * 400);
    }
     } catch {}
     const title = await getText(page, ['#productTitle', 'h1 span#productTitle', 'span#productTitle']);
     const priceRaw = await getText(page, ['#corePriceDisplay_desktop_feature_div span.a-offscreen', '.a-price .a-offscreen', '#priceblock_ourprice', '#priceblock_dealprice']);
     const price = normalizePrice(priceRaw);
     const ratingRaw = await getText(page, ['i[data-hook="average-star-rating"] span.a-icon-alt', 'span[data-hook="rating-out-of-text"]', 'i.a-icon-star span.a-icon-alt']);
     const rating = ratingRaw ? (ratingRaw.match(/[0-9,.]+/) || [''])[0] : '';
     const reviewsRaw = await getText(page, ['#acrCustomerReviewText', '#acrCustomerReviewLink #acrCustomerReviewText']);
     const reviewsCount = parseReviewsCount(reviewsRaw);
     let asinFound = asin;
     try {
    const asinInput = page.locator('input#ASIN, input[name="ASIN"]');
    if (await asinInput.count()) {
     const v = await asinInput.first().getAttribute('value');
     if (v) asinFound = v;
    }
     } catch {}
     const thumbs = await getAttrAll(page, ['#altImages img', '#imageBlockThumbs img'].join(','), 'src');
     const images = Array.from(new Set(thumbs.map(s => s.replace(/\._.*_\.jpg/i, '.jpg'))));
     const description = await getText(page, ['#productDescription', '#bookDescription_feature_div', '#productFactsDesktopExpander']);
     const bulletsEls = await page.locator('#feature-bullets ul li span');
     const bulletsCount = await bulletsEls.count();
     const bullets = [];
     for (let i = 0; i < bulletsCount; i++) {
    const t = (await bulletsEls.nth(i).innerText()).trim();
    if (t && !/^\s*\(.*\)\s*$/.test(t)) bullets.push(t);
     }
     const features = bullets.join(' | ');
     return { url, asin: asinFound, title, price, rating, reviewsCount, images, description, features };
    }
    module.exports = { parseProduct, buildProductUrl };

    Интеграция со src/index.js (тест 1–2 ASIN)

    Пример:

    require('dotenv').config();
    const { chromium } = require('playwright-extra');
    const stealth = require('puppeteer-extra-plugin-stealth')();
    chromium.use(stealth);
    const { parseProduct } = require('./scraper');
    const { setTimeout: sleep } = require('timers/promises');
    const market = process.env.MARKET || 'https:/www.amazon.com';
    const proxyMode = process.env.PROXY_MODE || 'ip';
    const proxyServers = (process.env.PROXY_SERVERS || '').split(',').map(s => s.trim()).filter(Boolean);
    const proxyUser = process.env.PROXY_USER || '';
    const proxyPass = process.env.PROXY_PASS || '';
    function pickProxy() {
     if (!proxyServers.length) return null;
     const i = Math.floor(Math.random() * proxyServers.length);
     return proxyServers[i];
    }
    function getProxyOptions(server) {
     if (!server) return {};
     const [host, port] = server.split(':');
     const proxyUrl = 'http:' + host + ':' + port;
     if (proxyMode === 'login') {
    return { server: proxyUrl, username: proxyUser, password: proxyPass };
     }
     return { server: proxyUrl };
    }
    (async () => {
     const server = pickProxy();
     const proxy = getProxyOptions(server);
     const browser = await chromium.launch({ headless: true, proxy });
     const context = await browser.newContext({
    viewport: { width: 1366, height: 768 },
    userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
    locale: 'en-US'
     });
     const page = await context.newPage();
     const asin = 'B08N5WRWNW';
     const data = await parseProduct(page, market, asin);
     console.log(data);
     await browser.close();
    })().catch(console.error);

    Важные моменты

    • У Amazon динамический интерфейс. Мы используем несколько селекторов для надежности.
    • ASIN можно получить и из URL, и из скрытого поля. Мы страхуемся.

    Совет: Если данные не находятся, временно используйте page.screenshot с сохранением картинки в data/debug-скриншоты. По скриншоту проще понять, что отрисовалось и какие селекторы доступны.

    Ожидаемый результат

    В консоли появляется объект с полями asin, title, price, rating, reviewsCount, images, description, features. Некоторые поля могут быть пустыми на отдельных карточках — это нормально, позже добавим дополнительные проверки.

    Возможные проблемы и решения

    • Пустой title. Причина: баннеры закрывают контент. Решение: закрывайте баннеры или ждите дольше.
    • Пустая цена. Причина: нет цены на конкретной карточке. Решение: добавьте fallback или парсинг блока вариантов.

    ✅ Проверка: Вы видите корректный title и непустой ASIN. Хотя бы одно изображение должно присутствовать в массиве images.

    Шаг 6: Рандомизация поведения, задержки, скролл, hover, клики

    Цель этапа

    Снизить вероятность детекта автоматизации. Добавим случайные задержки, движения мыши, прокрутку, hover на элементы, клики и разные схемы ожидания.

    Пошаговая инструкция

    1. Создайте файл src/humanize.js с функциями случайной задержки и поведения.
    2. Реализуйте randomDelay(min, max), randomMouseMove(page), humanScroll(page), hoverRandomThumb(page).
    3. Вызовите эти функции в процессе парсинга перед извлечением данных.
    4. Добавьте непредсказуемость: разные порядки действий, случайные точки кликов, небольшие паузы.

    Пример кода для src/humanize.js

    Пример:

    const { setTimeout: sleep } = require('timers/promises');
    function rand(min, max) {
     return Math.floor(Math.random() * (max - min + 1)) + min;
    }
    async function randomDelay(minMs, maxMs) {
     const t = rand(minMs, maxMs);
     await sleep(t);
    }
    async function randomMouseMove(page) {
     const w = 1366, h = 768;
     const steps = rand(3, 9);
     let x = rand(10, w - 10), y = rand(10, h - 10);
     for (let i = 0; i < steps; i++) {
    x = Math.max(1, Math.min(w - 1, x + rand(-60, 60)));
    y = Math.max(1, Math.min(h - 1, y + rand(-40, 40)));
    await page.mouse.move(x, y, { steps: rand(2, 5) });
    await sleep(rand(40, 120));
     }
    }
    async function humanScroll(page) {
     const total = rand(2000, 6000);
     let scrolled = 0;
     while (scrolled < total) {
    const delta = rand(200, 600);
    await page.mouse.wheel(0, delta);
    scrolled += delta;
    await sleep(rand(200, 600));
     }
    }
    async function hoverRandomThumb(page) {
     const thumbs = page.locator('#altImages li, #imageBlockThumbs li');
     const count = await thumbs.count();
     if (count > 0) {
    const i = rand(0, Math.min(count - 1, 4));
    const el = thumbs.nth(i);
    await el.hover();
    await sleep(rand(200, 700));
     }
    }
    module.exports = { randomDelay, randomMouseMove, humanScroll, hoverRandomThumb, rand };

    Интеграция

    В src/scraper.js, перед извлечением данных, вызовите:

    const human = require('./humanize');
    await human.randomMouseMove(page);
    await human.humanScroll(page);
    await human.hoverRandomThumb(page);
    await human.randomDelay(400, 1100);

    Важные моменты

    • Слишком длинные задержки замедлят сбор, слишком короткие — повысят риски. Балансируйте.
    • Имитация действий не должна быть шаблонной. Добавляйте случайность.

    Совет: Сложите задержки в .env, чтобы не менять код: MIN_DELAY_MS и MAX_DELAY_MS. В humanize.js оттуда читайте значения.

    Ожидаемый результат

    Скрипт стал «вести себя как человек»: делает небольшие паузы, водит мышью, скроллит, наводит курсор на миниатюры. Блокировок и CAPTCHA меньше.

    Возможные проблемы и решения

    • Сильное замедление. Решение: уменьшите число действий, уберите часть hover, оптимизируйте задержки.
    • CAPTCHA все равно появляется. Решение: добавьте прокси-ротацию и измените скорость запросов.

    ✅ Проверка: При запуске headless: false видно, что курсор двигается, страница скроллится, миниатюры подсвечиваются при hover. Ошибок нет.

    Шаг 7: Ротация User-Agent и масштабирование на сотни товаров

    Цель этапа

    Снизить корреляцию между запросами. Добавим ротацию User-Agent, ограничим параллелизм, подготовим очередь задач и управление прокси-пулами.

    Пошаговая инструкция

    1. Установленный пакет user-agents позволяет брать правдоподобные строки. Подготовьте функцию getRandomUA.
    2. Сделайте простую очередь: ограничьте число одновременно работающих браузеров с p-limit или p-queue.
    3. Добавьте пул прокси: выбирайте свободный прокси из списка, запускайте под него браузер, возвращайте в пул после завершения.
    4. Переработайте index.js, чтобы читать ASIN из файла, создавать задачи и сохранять результаты.

    Пример кода для src/index.js (масштабируемая версия)

    Пример:

    require('dotenv').config();
    const { chromium } = require('playwright-extra');
    const stealth = require('puppeteer-extra-plugin-stealth')();
    chromium.use(stealth);
    const fs = require('fs');
    const path = require('path');
    const PQueue = require('p-queue').default;
    const UserAgents = require('user-agents');
    const { parseProduct } = require('./scraper');
    const human = require('./humanize');
    const market = process.env.MARKET || 'https:/www.amazon.com';
    const proxyMode = process.env.PROXY_MODE || 'ip';
    const proxyServers = (process.env.PROXY_SERVERS || '').split(',').map(s => s.trim()).filter(Boolean);
    const proxyUser = process.env.PROXY_USER || '';
    const proxyPass = process.env.PROXY_PASS || '';
    const MIN_DELAY_MS = Number(process.env.MIN_DELAY_MS || '800');
    const MAX_DELAY_MS = Number(process.env.MAX_DELAY_MS || '2000');
    const CONCURRENCY = Number(process.env.CONCURRENCY || '2');
    function getRandomUA() {
     const ua = new UserAgents({ deviceCategory: 'desktop' }).toString();
     return ua;
    }
    function getProxyOptions(server) {
     if (!server) return {};
     const [host, port] = server.split(':');
     const proxyUrl = 'http:' + host + ':' + port;
     if (proxyMode === 'login') {
    return { server: proxyUrl, username: proxyUser, password: proxyPass };
     }
     return { server: proxyUrl };
    }
    async function withBrowser(server, fn) {
     const proxy = getProxyOptions(server);
     const browser = await chromium.launch({ headless: true, proxy });
     try {
    const context = await browser.newContext({
     viewport: { width: 1366, height: 768 },
     userAgent: getRandomUA(),
     locale: 'en-US'
    });
    const page = await context.newPage();
    await fn(page);
    await context.close();
     } finally {
    await browser.close();
     }
    }
    async function processAsin(server, asin) {
     let result = null;
     await withBrowser(server, async page => {
    await human.randomDelay(MIN_DELAY_MS, MAX_DELAY_MS);
    result = await parseProduct(page, market, asin);
     });
     return result;
    }
    function readAsins(file) {
     const p = path.resolve(file);
     const txt = fs.readFileSync(p, 'utf8');
     return txt.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
    }
    (async () => {
     const asins = readAsins('data/input_asins.txt');
     const queue = new PQueue({ concurrency: CONCURRENCY });
     const outputs = [];
     let proxyIndex = 0;
     function nextProxy() {
    if (!proxyServers.length) return null;
    proxyIndex = (proxyIndex + 1) % proxyServers.length;
    return proxyServers[proxyIndex];
     }
     for (const asin of asins) {
    queue.add(async () => {
     const server = nextProxy();
     const data = await processAsin(server, asin);
     outputs.push(data);
     console.log('Done', asin, data && data.title ? 'OK' : 'EMPTY');
     await human.randomDelay(MIN_DELAY_MS, MAX_DELAY_MS);
    });
     }
     await queue.onIdle();
     fs.writeFileSync('data/output.json', JSON.stringify(outputs, null, 2));
     console.log('Saved to data/output.json');
    })().catch(err => {
     console.error(err);
     process.exit(1);
    });

    Важные моменты

    • CONCURRENCY подбирайте осторожно. Начните с 1–2 и увеличивайте до 3–4, наблюдая за блокировками.
    • User-Agent меняем при создании контекста. Это добавляет разнообразие.

    Совет: Для отдельных рынков Amazon используйте локаль и часовой пояс контекста, соответствующие региону. Например, для Германии locale: 'de-DE'.

    Ожидаемый результат

    Скрипт обрабатывает список ASIN, использует пул прокси и сохраняет массив результатов в output.json.

    Возможные проблемы и решения

    • Чрезмерные блокировки. Решение: уменьшите CONCURRENCY, увеличьте задержки, добавьте больше прокси.
    • Низкая скорость. Решение: оптимизируйте задержки, но без фанатизма, увеличьте пул прокси.

    ✅ Проверка: В data/output.json присутствуют объекты с полями title и asin. Большинство карточек заполнились корректно.

    Шаг 8: Сохранение результатов в JSON и CSV

    Цель этапа

    Добавить надежное сохранение в два формата. JSON — для гибкого анализа, CSV — для быстрого импорта в таблицы и BI-системы.

    Пошаговая инструкция

    1. Создайте файл src/save.js с функциями saveJSON и saveCSV.
    2. Добавьте формирование заголовков CSV и нормализацию данных (экранирование разделителей).
    3. В index.js вызовите сохранение после завершения очереди.
    4. Проверьте корректность файлов в data/output.json и data/output.csv.

    Пример кода для src/save.js

    Пример:

    const fs = require('fs');
    const path = require('path');
    function saveJSON(arr, file) {
     const p = path.resolve(file);
     fs.writeFileSync(p, JSON.stringify(arr, null, 2), 'utf8');
    }
    function escapeCsv(v) {
     if (v == null) return '';
     const s = String(v);
     if (/[",\n;]/.test(s)) return '"' + s.replace(/"/g, '""') + '"';
     return s;
    }
    function saveCSV(arr, file) {
     const p = path.resolve(file);
     const header = ['asin','title','price','rating','reviewsCount','images','description','features','url'];
     const rows = [header.join(';')];
     for (const it of arr) {
    if (!it) continue;
    const row = [
     it.asin || '',
     it.title || '',
     it.price || '',
     it.rating || '',
     it.reviewsCount || 0,
     (it.images || []).join(' '),
     it.description || '',
     it.features || '',
     it.url || ''
    ].map(escapeCsv).join(';');
    rows.push(row);
     }
     fs.writeFileSync(p, rows.join('\n'), 'utf8');
    }
    module.exports = { saveJSON, saveCSV };

    Интеграция

    В src/index.js после обработки очереди импортируйте сохранение:

    const { saveJSON, saveCSV } = require('./save');
    saveJSON(outputs, 'data/output.json');
    saveCSV(outputs, 'data/output.csv');

    Важные моменты

    • В CSV используйте стабильный разделитель, например точку с запятой. Это упростит импорт в разные таблицы.
    • Экранируйте кавычки и переносы строк в текстах.

    Совет: Добавьте timestamp в имя файла для версионирования, например data/output_2026-02-01_1200.csv.

    Ожидаемый результат

    В папке data появились два файла: output.json и output.csv. В JSON удобный массив объектов; в CSV — строки с заголовком.

    Возможные проблемы и решения

    • Кодировка. Решение: сохраняйте в UTF-8 и открывайте в редакторе с соответствующей кодировкой.
    • Большие файлы. Решение: сохраняйте частями или построчно при больших объемах данных.

    ✅ Проверка: Откройте output.csv в любой таблице. Данные в столбцах не смещены, текст в кавычках, изображения объединены пробелом.

    Шаг 9: Обработка ошибок, CAPTCHA, таймауты, retry с экспоненциальной задержкой

    Цель этапа

    Повысить надежность. Добавим распознавание проблемных ситуаций, повторные попытки с увеличением задержки, смену прокси при фейлах. Подготовим логи для анализа.

    Пошаговая инструкция

    1. Добавьте функцию isCaptchaPage(page), которая проверяет URL на наличие слов captcha и наличие элементов проверки.
    2. Добавьте retry-обертку вокруг processAsin с экспоненциальной задержкой и джиттером.
    3. При повторных ошибках меняйте прокси и User-Agent.
    4. Логируйте коды и шаги, сохраняйте ошибочные страницы при необходимости.

    Пример кода для retry в src/index.js

    Пример:

    function isCaptchaHtml(html) {
     return /captcha/i.test(html) || /Type the characters/i.test(html) || /Enter the characters/i.test(html);
    }
    async function tryProcessWithRetry(asin) {
     const maxRetries = Number(process.env.MAX_RETRIES || '5');
     let attempt = 0;
     let lastError = null;
     while (attempt < maxRetries) {
    attempt++;
    let server = proxyServers.length ? proxyServers[(attempt - 1) % proxyServers.length] : null;
    try {
     const data = await withBrowser(server, async page => {
    await human.randomDelay(MIN_DELAY_MS, MAX_DELAY_MS);
    await page.goto(market.replace(/\/$/, '') + '/dp/' + asin, {
     waitUntil: 'domcontentloaded',
     timeout: 60000
    });
    const html = await page.content();
    if (isCaptchaHtml(html)) {
     throw new Error('CAPTCHA');
    }
    for (let i = 0; i < 2; i++) await human.randomMouseMove(page);
    return await parseProduct(page, market, asin);
     });
     if (data && data.title) return data;
     throw new Error('Empty data');
    } catch (err) {
     lastError = err;
     const base = 1200;
     const backoff = Math.min(base * Math.pow(2, attempt - 1), 20000);
     const jitter = Math.floor(Math.random() * 400);
     console.warn('Retry', attempt, 'ASIN', asin, 'server', server, 'err', err.message);
     await new Promise(r => setTimeout(r, backoff + jitter));
    }
     }
     throw lastError || new Error('Unknown error');
    }

    Интеграция в очередь

    Замените в очереди процессинг:

    const data = await tryProcessWithRetry(asin);

    Важные моменты

    • Экспоненциальный backoff помогает разгрузить частые повторы.
    • При частых CAPTCHA снизьте параллелизм и увеличьте паузы. Смените IP.

    Совет: Если провайдер мобильных прокси поддерживает принудительную смену IP по URL, вызывайте этот эндпоинт после нескольких подряд неудач. Делайте паузу 30–60 секунд после смены IP.

    ⚠️ Внимание: Не пытайтесь обходить защиту вредоносными методами. Не используйте взлом или несанкционированный доступ. Работайте только в рамках законных и этичных практик.

    Ожидаемый результат

    Скрипт устойчивее выдерживает блокировки. Ошибки повторяются с паузами, прокси и User-Agent меняются, итоговые данные сохраняются для большинства ASIN.

    Возможные проблемы и решения

    • Много пустых карточек. Решение: увеличьте таймаут загрузки, добавьте ожидания конкретных селекторов.
    • Повторные CAPTCHA. Решение: уменьшите скорость, добавьте больше прокси, выполняйте парсинг в другие временные окна.

    ✅ Проверка: В логах видны шаги Retry. После нескольких попыток большинство ASIN парсятся успешно. Количество фатальных ошибок снизилось.

    Проверка результата

    Чек-лист

    • Node.js установлен, npm работает.
    • Playwright и браузер Chromium установлены.
    • Stealth-плагин активен.
    • Мобильные прокси подключены (IP или логин/пароль).
    • Скрипт открывает Amazon и парсит карточку по ASIN.
    • Данные сохраняются в JSON и CSV.
    • Работает retry с backoff, обрабатываются CAPTCHA и таймауты.
    • Скрипт масштабируется на десятки и сотни ASIN с очередью и пулом прокси.

    Как протестировать

    • Запустите на 3–5 ASIN. Убедитесь, что в output.json есть все ключевые поля.
    • Увеличьте список до 50 ASIN. Проверьте стабильность, подсчитайте долю успешных парсингов.
    • Попробуйте разное CONCURRENCY: 1, 2, 3. Выберите оптимум по скорости и блокировкам.

    Показатели успеха

    • Успешно спарсено не менее 80% карточек на небольшой выборке.
    • CAPTCHA возникает редко и обрабатывается retry.
    • Данные корректны, загружаются в таблицы без смещения колонок.

    ✅ Проверка: Если при повторном запуске на том же наборе ASIN вы стабильно получаете сопоставимые результаты, ваш пайплайн готов к масштабированию.

    Типичные ошибки и решения

    • Проблема: пустой title. Причина: страница не полностью прогрузилась. Решение: увеличьте таймаут и добавьте ожидание #productTitle.
    • Проблема: пустая цена. Причина: нет цены для текущей вариации. Решение: парсите .a-price .a-offscreen и альтернативные селекторы, переключайте вариации при необходимости.
    • Проблема: CAPTCHA на каждом шаге. Причина: слишком быстрый парсинг или повторяющиеся параметры. Решение: уменьшите параллелизм, добавьте задержки, ротацию IP и User-Agent.
    • Проблема: таймауты. Причина: медленный прокси. Решение: смените прокси, увеличьте таймаут, добавьте retry с backoff.
    • Проблема: сбой авторизации прокси. Причина: неверные креды или список IP. Решение: перепроверьте .env, белый список IP, перезапустите сессию.
    • Проблема: CSV открывается с «ломаными» строками. Причина: неэкранированные кавычки и переносы. Решение: добавьте escapeCsv и используйте стабильный разделитель.
    • Проблема: падение памяти при сотнях карточек. Причина: слишком много параллельных браузеров. Решение: уменьшите CONCURRENCY, закрывайте контексты и браузеры своевременно.

    Дополнительные возможности

    Продвинутые настройки

    • Локали и часовые пояса: создавайте контексты с timezoneId, locale и geolocation, соответствующими рынку.
    • Настройки устройств: имитируйте ноутбук или планшет с помощью viewport и user agent конкретного устройства.
    • Кэширование: сохраняйте промежуточные результаты, чтобы не перезапрашивать карточки.
    • Ротация IP по событию: при 2–3 ошибках подряд вызывайте ротацию IP у провайдера, затем ждите и повторяйте попытку.

    Оптимизация

    • Разогрев: делайте 1–2 «разогревающих» хита на нейтральные страницы, чтобы прогрузить куки и скрипты.
    • Гибкая очередь: увеличивайте параллелизм постепенно при низком уровне ошибок.
    • Хранение логов: сохраняйте логи в файлы с датой, анализируйте пики ошибок.
    • Распределенный запуск: запускайте несколько экземпляров на разных машинах/серверных локациях.

    Совет: Добавьте мониторинг метрик: среднее время парсинга на карточку, процент успешных запросов, частоту CAPTCHA, скорость сети и медленные прокси.

    Совет: Храните результаты в базе данных (например, SQLite или Postgres), если объем растет. Это упростит аналитику и обновления.

    Совет: Добавьте дедупликацию: если один и тот же ASIN встречается несколько раз, обновляйте запись, а не создавайте дубликаты.

    Совет: Используйте расписание (cron) для регулярного обновления цен и отзывов. Запускайте в часы меньшей нагрузки.

    Совет: Для некоторых карточек полезно загружать вкладку отзывов и парсить несколько страниц отзывов. Делайте это выборочно, чтобы не перегружать.

    FAQ

    • Можно ли парсить без прокси? Да, но риски блокировок выше. Мобильные прокси значительно уменьшают частоту CAPTCHA и банов.
    • Как выбрать регион Amazon? Укажите MARKET в .env, например https://www.amazon.com или другой домен. Подберите locale контекста под регион.
    • Нужен ли headless? Для отладки лучше headless: false, для серийного парсинга headless: true. Это быстрее и стабильнее.
    • Почему цена иногда пустая? На некоторых карточках нет цены или она спрятана в вариациях. Добавьте дополнительные селекторы и логику переключения вариаций.
    • Как понять, что меня заблокировали? Частые CAPTCHA, редирект на страницу с проверкой, пустые блоки. Снижайте скорость, меняйте IP, увеличивайте задержки.
    • Сколько потоков можно запускать? Начните с 1–2. Если ошибок нет, попробуйте 3–4. Наблюдайте метрики и балансируйте с количеством прокси.
    • Можно ли сохранять изображения? Мы сохраняем ссылки. Чтобы скачивать файлы, пройдитесь по массиву images и скачайте по одному с задержкой.
    • Как обновлять данные? Храните прошлые результаты вместе с timestamp и регулярно запускайте скрипт на список ASIN. Обновляйте только изменившиеся поля.
    • Чем мобильные прокси лучше обычных? Мобильные IP реже попадают под строгие блокировки и воспринимаются ближе к реальному трафику от пользователей мобильных сетей.
    • Что делать, если провайдер прокси медленный? Добавьте еще несколько прокси, уменьшите параллелизм, отбрасывайте медленные серверы по метрикам времени отклика.

    Заключение

    Вы настроили полноценный пайплайн парсинга Amazon: установили Node.js и Playwright, подключили playwright-extra и stealth-плагин, конфигурировали мобильные прокси с авторизацией по IP и логин/пароль, добавили эмуляцию реального поведения, рандомизацию задержек и ротацию User-Agent. Вы научились извлекать ключевые данные карточек — название, цену, рейтинг, количество отзывов, ASIN, изображения и описание — и сохранять их в JSON и CSV. Мы внедрили обработку ошибок, распознавание CAPTCHA и retry с экспоненциальным backoff, а также масштабирование на сотни товаров через очередь и пул прокси.

    Дальше вы можете углубляться: парсинг вариаций товаров, сбор отзывов по страницам, интеграция с базой данных, шедулинг регулярных запусков, мониторинг метрик и авто-переключение прокси при низкой успешности. Начинайте с маленьких партий, отлаживайте поведение, затем расширяйте масштаб.

    ⚠️ Внимание: Всегда учитывайте юридические ограничения и правила площадки. Соблюдайте разумный темп запросов, не вредите инфраструктуре платформы и уважайте конфиденциальность пользователей.

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