Node.js парсер позиций: мониторинг топ-100 по 500 запросам за час
Коротко: Node.js парсер способен мониторить позиции сайта в топ-100 по 500 запросам менее чем за час благодаря асинхронности и правильной архитектуре. Для стабильной работы нужно управление прокси, обход защиты поисковиков и обработка ошибок — тогда скрипт выдаёт 85-90% успешных результатов.
Содержание
- Почему Node.js лучше Python для парсинга позиций?
- Как устроена архитектура эффективного парсера?
- Пошаговая реализация Node.js парсера
- Как оптимизировать скорость и обходить блокировки?
- Мониторинг и автоматизация процесса сбора данных
- Что влияет на производительность парсера?
- Масштабирование и развитие системы
Почему Node.js лучше Python для парсинга позиций?
Когда мы в DS495 впервые столкнулись с задачей мониторинга позиций по сотням запросов, первым инстинктом было взять Python с Selenium. Классика жанра, всё работает, библиотек миллион. Но быстро выяснилось — для больших объёмов это не катит.
Python с его GIL (Global Interpreter Lock) просто не может эффективно обрабатывать сотни одновременных запросов. Да, есть multiprocessing, но накладные расходы на создание процессов съедают всю экономию времени. В итоге на 500 запросов уходило 3-4 часа вместо желаемого часа.
Node.js показывает прирост производительности в 3-5 раз по сравнению с Python при парсинге позиций благодаря событийному циклу и отсутствию блокирующих операций.
Node.js решает эту проблему элегантно:
- Асинхронность из коробки — event loop обрабатывает тысячи запросов без создания новых потоков
- Низкое потребление памяти — один процесс против десятков в Python
- Быстрый старт — нет времени на прогрев интерпретатора
- Простота работы с JSON — нативная поддержка формата данных
Вот сравнение реальных метрик из нашей практики:
| Параметр | Python + Requests | Python + AsyncIO | Node.js |
|---|---|---|---|
| Время на 500 запросов | 180-240 минут | 90-120 минут | 45-60 минут |
| Потребление RAM | 512-1024 МБ | 256-512 МБ | 128-256 МБ |
| Стабильность | 70-75% | 80-85% | 85-92% |
| Простота отладки | Высокая | Средняя | Высокая |
Единственный минус Node.js — меньше готовых библиотек для парсинга по сравнению с Python. Но для наших задач это не критично, а скорость разработки компенсируется производительностью готового решения.
Как устроена архитектура эффективного парсера?
Эффективный парсер позиций — это не просто скрипт, который стучится в поисковики. Это целая система со своими компонентами, каждый из которых решает конкретную задачу.
Основные модули системы:
- Менеджер очередей — распределяет запросы между воркерами
- Прокси-менеджер — ротирует IP-адреса для обхода блокировок
- Парсинг-движок — извлекает данные из поисковой выдачи
- Система кэширования — сохраняет результаты для повторного использования
- ETL-процессор — обрабатывает и нормализует данные
- Мониторинг — отслеживает ошибки и производительность
Архитектурно мы строим систему по принципу микросервисов, но в рамках одного Node.js процесса. Каждый модуль живёт в своём файле и общается с другими через события:
// Упрощённая схема взаимодействия
const EventEmitter = require('events');
class PositionParser extends EventEmitter {
constructor() {
super();
this.proxyManager = new ProxyManager();
this.queueManager = new QueueManager();
this.cache = new CacheManager();
}
}
Правильная архитектура парсера экономит 60-70% времени на доработки и масштабирование. Потратьте день на проектирование — сэкономите месяцы на поддержке.
Жизненный цикл одного запроса:
- Запрос попадает в очередь с приоритетом
- Менеджер очередей назначает свободный воркер
- Воркер получает прокси и User-Agent
- Выполняется HTTP-запрос к поисковику
- HTML парсится и извлекаются позиции
- Результат сохраняется в кэш и возвращается
Такой подход даёт нам гибкость: можно легко добавить новые поисковики, изменить алгоритм ротации прокси или подключить внешнюю очередь типа Redis.
Пошаговая реализация Node.js парсера
Теперь от теории к практике. Покажу, как мы реализуем парсер позиций с нуля. Начнём с базового функционала и постепенно добавим оптимизации.
Шаг 1: Установка зависимостей
npm init -y
npm install axios cheerio puppeteer-core node-cron fs-extra
Шаг 2: Базовый класс парсера
const axios = require('axios');
const cheerio = require('cheerio');
const fs = require('fs-extra');
class PositionParser {
constructor(options = {}) {
this.concurrency = options.concurrency || 10;
this.delay = options.delay || 1000;
this.proxies = options.proxies || [];
this.userAgents = this.loadUserAgents();
this.results = new Map();
}
async parsePosition(keyword, site) {
try {
const html = await this.fetchSearchResults(keyword);
const position = this.extractPosition(html, site);
return { keyword, site, position, timestamp: Date.now() };
} catch (error) {
console.error(`Ошибка парсинга ${keyword}:`, error.message);
return { keyword, site, position: null, error: error.message };
}
}
}
Шаг 3: Реализация HTTP-клиента с ротацией прокси
async fetchSearchResults(keyword) {
const proxy = this.getRandomProxy();
const userAgent = this.getRandomUserAgent();
const config = {
url: `https://www.google.com/search?q=${encodeURIComponent(keyword)}&num=100`,
headers: {
'User-Agent': userAgent,
'Accept-Language': 'ru-RU,ru;q=0.9,en;q=0.8',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
},
timeout: 10000,
proxy: proxy
};
const response = await axios(config);
return response.data;
}
Шаг 4: Извлечение позиций из HTML
extractPosition(html, targetSite) {
const $ = cheerio.load(html);
let position = null;
$('.g').each((index, element) => {
const link = $(element).find('a').first().attr('href');
if (link && link.includes(targetSite)) {
position = index + 1;
return false; // break
}
});
return position;
}
Не пытайтесь парсить все позиции одновременно — это прямой путь к блокировке. Оптимальный concurrency для Google: 8-12 запросов, для Яндекса: 5-8 запросов.
Шаг 5: Обработка очереди запросов
async processQueue(keywords, targetSite) {
const chunks = this.chunkArray(keywords, this.concurrency);
const results = [];
for (const chunk of chunks) {
const promises = chunk.map(keyword =>
this.parsePosition(keyword, targetSite)
);
const chunkResults = await Promise.allSettled(promises);
results.push(...chunkResults.map(r => r.value || r.reason));
// Задержка между пачками запросов
await this.sleep(this.delay);
}
return results;
}
chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
Шаг 6: Сохранение результатов
async saveResults(results, filename) {
const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
const filepath = `./results/${filename}-${timestamp}.json`;
await fs.ensureDir('./results');
await fs.writeJSON(filepath, {
timestamp: new Date().toISOString(),
total: results.length,
success: results.filter(r => r.position !== null).length,
results: results
}, { spaces: 2 });
console.log(`Результаты сохранены: ${filepath}`);
}
Это базовая версия, которая уже может парсить позиции. Но для стабильной работы с большими объёмами нужны дополнительные оптимизации.
Как оптимизировать скорость и обходить блокировки?
Самая большая проблема любого парсера — блокировки со стороны поисковиков. Google и Яндекс не любят автоматические запросы и активно с ними борются. Нужна целая стратегия обхода защиты.
Управление прокси-серверами:
Качественные прокси — основа стабильного парсинга. Мы используем несколько источников:
- Приватные HTTP/HTTPS прокси — самые надёжные, но дорогие (от $3-5 за IP)
- Мобильные прокси — имитируют реальных пользователей ($20-50 за IP)
- Резидентные прокси — реальные IP домашних пользователей ($8-15 за ГБ)
class ProxyManager {
constructor() {
this.proxies = [];
this.failedProxies = new Set();
this.proxyStats = new Map();
}
async testProxy(proxy) {
try {
const start = Date.now();
await axios.get('https://httpbin.org/ip', {
proxy: proxy,
timeout: 5000
});
const responseTime = Date.now() - start;
this.proxyStats.set(proxy.host, {
lastCheck: Date.now(),
responseTime: responseTime,
success: true
});
return true;
} catch (error) {
this.failedProxies.add(`${proxy.host}:${proxy.port}`);
return false;
}
}
getHealthyProxy() {
const availableProxies = this.proxies.filter(proxy =>
!this.failedProxies.has(`${proxy.host}:${proxy.port}`)
);
if (availableProxies.length === 0) {
this.failedProxies.clear(); // Сбрасываем "чёрный список"
return this.proxies[Math.floor(Math.random() * this.proxies.length)];
}
return availableProxies[Math.floor(Math.random() * availableProxies.length)];
}
}
Имитация человеческого поведения:
Поисковики анализируют паттерны поведения. Идеальный бот неотличим от человека:
| Параметр | Человек | Плохой бот | Хороший бот |
|---|---|---|---|
| Скорость запросов | 1-3 в минуту | 10+ в минуту | 2-5 в минуту |
| User-Agent | Реальный браузер | Один и тот же | Ротация актуальных |
| Заголовки | Полный набор | Минимальные | Как у браузера |
| Cookies | Сохраняются | Игнорируются | Управляются |
Система retry и обработки ошибок:
async fetchWithRetry(url, options = {}, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await axios(url, {
...options,
proxy: this.proxyManager.getHealthyProxy(),
timeout: 15000 + (attempt * 5000) // Увеличиваем timeout
});
// Проверяем, не заблокированы ли мы
if (this.isBlocked(response.data)) {
throw new Error('IP blocked by search engine');
}
return response.data;
} catch (error) {
lastError = error;
if (attempt < maxRetries) {
// Экспоненциальная задержка: 2^attempt секунд
const delay = Math.pow(2, attempt) * 1000;
await this.sleep(delay);
}
}
}
throw lastError;
}
isBlocked(html) {
const blockIndicators = [
'captcha',
'unusual traffic',
'access denied',
'blocked',
'robot'
];
const lowerHtml = html.toLowerCase();
return blockIndicators.some(indicator => lowerHtml.includes(indicator));
}
С такими оптимизациями наш парсер стабильно обрабатывает 500 запросов за 45-60 минут с успешностью 87-92%.
Мониторинг и автоматизация процесса сбора данных
Парсер позиций — это не разовый скрипт, а система для регулярного мониторинга. Нужно автоматизировать запуск, отслеживать ошибки и уведомлять о проблемах.
Система логирования:
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' }),
new winston.transports.Console({
format: winston.format.simple()
})
]
});
// Использование в коде парсера
async parsePosition(keyword, site) {
const startTime = Date.now();
logger.info(`Начинаем парсинг: ${keyword} для ${site}`);
try {
const result = await this.fetchSearchResults(keyword);
const duration = Date.now() - startTime;
logger.info(`Успех: ${keyword}, время: ${duration}мс`);
return result;
} catch (error) {
logger.error(`Ошибка парсинга ${keyword}:`, {
error: error.message,
stack: error.stack,
keyword,
site
});
throw error;
}
}
Автоматизация через cron:
const cron = require('node-cron');
class ScheduledParser {
constructor() {
this.parser = new PositionParser();
this.isRunning = false;
}
startSchedule() {
// Каждый день в 3:00 утра
cron.schedule('0 3 * * *', async () => {
if (this.isRunning) {
logger.warn('Предыдущий парсинг ещё не завершён');
return;
}
this.isRunning = true;
await this.runDailyParsing();
this.isRunning = false;
});
// Каждый час проверяем здоровье системы
cron.schedule('0 * * * *', () => {
this.healthCheck();
});
}
async runDailyParsing() {
try {
const keywords = await this.loadKeywords('./keywords.json');
const results = await this.parser.processQueue(keywords, 'example.com');
await this.saveResults(results);
await this.sendReport(results);
logger.info(`Ежедневный парсинг завершён. Обработано: ${results.length} запросов`);
} catch (error) {
logger.error('Ошибка ежедневного парсинга:', error);
await this.sendErrorNotification(error);
}
}
}
Метрики и алерты:
Важно отслеживать ключевые метрики производительности:
- Успешность запросов — должна быть выше 85%
- Время выполнения — не более 90 минут на 500 запросов
- Частота блокировок — не более 2-3% запросов
- Потребление ресурсов — RAM и CPU
class MetricsCollector {
constructor() {
this.metrics = {
totalRequests: 0,
successfulRequests: 0,
blockedRequests: 0,
averageResponseTime: 0,
proxyHealthMap: new Map()
};
}
recordRequest(success, responseTime, blocked = false) {
this.metrics.totalRequests++;
if (success) {
this.metrics.successfulRequests++;
}
if (blocked) {
this.metrics.blockedRequests++;
}
// Скользящее среднее времени ответа
this.metrics.averageResponseTime =
(this.metrics.averageResponseTime + responseTime) / 2;
}
getSuccessRate() {
return (this.metrics.successfulRequests / this.metrics.totalRequests) * 100;
}
shouldAlert() {
const successRate = this.getSuccessRate();
const blockRate = (this.metrics.blockedRequests / this.metrics.totalRequests) * 100;
return successRate < 85 || blockRate > 5 || this.metrics.averageResponseTime > 10000;
}
}
Система мониторинга помогает быстро реагировать на проблемы и поддерживать стабильную работу парсера.
Что влияет на производительность парсера?
За несколько лет работы с парсерами мы выяснили, какие факторы критически влияют на скорость и стабильность. Некоторые очевидны, другие — неожиданны.
Топ-5 узких мест:
- Качество прокси — плохие прокси тормозят всю систему на 60-80%
- Размер concurrency — слишком много = блокировки, слишком мало = медленно
- Обработка HTML — cheerio быстрее jsdom в 3-4 раза
- Управление памятью — утечки памяти убивают производительность
- Сетевые таймауты — слишком короткие = много retry, длинные = зависания
Оптимизация сетевого слоя:
// Переиспользование HTTP-соединений
const axios = require('axios');
const httpAgent = new require('http').Agent({
keepAlive: true,
maxSockets: 50,
maxFreeSockets: 10,
timeout: 60000
});
const httpsAgent = new require('https').Agent({
keepAlive: true,
maxSockets: 50,
maxFreeSockets: 10,
timeout: 60000
});
const optimizedAxios = axios.create({
httpAgent: httpAgent,
httpsAgent: httpsAgent,
timeout: 15000
});
Профилирование производительности:
class PerformanceProfiler {
constructor() {
this.timers = new Map();
this.counters = new Map();
}
startTimer(label) {
this.timers.set(label, process.hrtime.bigint());
}
endTimer(label) {
const start = this.timers.get(label);
if (!start) return null;
const duration = Number(process.hrtime.bigint() - start) / 1000000; // в мс
this.timers.delete(label);
return duration;
}
incrementCounter(label) {
this.counters.set(label, (this.counters.get(label) || 0) + 1);
}
getStats() {
const memUsage = process.memoryUsage();
return {
memory: {
rss: Math.round(memUsage.rss / 1024 / 1024) + ' MB',
heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024) + ' MB',
heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024) + ' MB'
},
counters: Object.fromEntries(this.counters)
};
}
}
// Использование в парсере
const profiler = new PerformanceProfiler();
async parsePosition(keyword, site) {
profiler.startTimer(`parse-${keyword}`);
try {
const result = await this.fetchSearchResults(keyword);
profiler.incrementCounter('successful_requests');
return result;
} finally {
const duration = profiler.endTimer(`parse-${keyword}`);
if (duration > 5000) { // Медленный запрос
logger.warn(`Медленный запрос: ${keyword}, ${duration}мс`);
}
}
}
Тонкая настройка Event Loop:
Node.js Event Loop — сердце производительности. Важно не блокировать его тяжёлыми операциями:
// Плохо — блокирует Event Loop
function processLargeArray(array) {
return array.map(item => heavyComputation(item));
}
// Хорошо — даём Event Loop передышку
async function processLargeArrayAsync(array, batchSize = 100) {
const results = [];
for (let i = 0; i < array.length; i += batchSize) {
const batch = array.slice(i, i + batchSize);
const batchResults = batch.map(item => heavyComputation(item));
results.push(...batchResults);
// Позволяем Event Loop обработать другие задачи
await new Promise(resolve => setImmediate(resolve));
}
return results;
}
С правильными оптимизациями парсер на 500 запросов укладывается в 45-50 минут при стабильности 90%+.
Масштабирование и развитие системы
Когда количество отслеживаемых запросов растёт с сотен до тысяч, одного процесса Node.js становится мало. Нужно думать о горизонтальном масштабировании.
Кластеризация Node.js:
const cluster = require('cluster');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
console.log(`Мастер-процесс ${process.pid} запущен`);
// Создаём воркеры
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`Воркер ${worker.process.pid} умер`);
cluster.fork(); // Перезапускаем
});
} else {
// Код воркера
const parser = new PositionParser();
parser.start();
console.log(`Воркер ${process.pid} запущен`);
}
Интеграция с внешними очередями:
Для серьёзного масштабирования используем Redis или RabbitMQ:
const Redis = require('redis');
class RedisQueueManager {
constructor() {
this.redis = Redis.createClient();
this.queueName = 'position_parsing_queue';
}
async addJob(keyword, site, priority = 0) {
const job = JSON.stringify({
id: Date.now() + Math.random(),
keyword,
site,
priority,
created: new Date().toISOString()
});
await this.redis.zadd(this.queueName, priority, job);
}
async getNextJob() {
const jobs = await this.redis.zrevrange(this.queueName, 0, 0);
if (jobs.length === 0) return null;
const job = JSON.parse(jobs[0]);
await this.redis.zrem(this.queueName, jobs[0]);
return job;
}
async getQueueSize() {
return await this.redis.zcard(this.queueName);
}
}
ETL-процессы для обработки данных:
Сырые данные парсинга нужно обрабатывать и загружать в аналитические системы:
class PositionETL {
constructor(dbConnection) {
this.db = dbConnection;
}
async transformData(rawResults) {
return rawResults.map(result => ({
keyword: result.keyword.toLowerCase().trim(),
site: this.normalizeDomain(result.site),
position: result.position,
search_engine: 'google',
location: 'ru',
device: 'desktop',
date: new Date(result.timestamp).toISOString().split('T')[0],
timestamp: new Date(result.timestamp)
}));
}
async loadToDatabase(transformedData) {
const batchSize = 1000;
for (let i = 0; i < transformedData.length; i += batchSize) {
const batch = transformedData.slice(i, i + batchSize);
await this.db.query(`
INSERT INTO position_history
(keyword, site, position, search_engine, location, device, date, timestamp)
VALUES ?
ON DUPLICATE KEY UPDATE
position = VALUES(position),
timestamp = VALUES(timestamp)
`, [batch.map(Object.values)]);
}
}
normalizeDomain(url) {
return url.replace(/^https?:\/\//, '').replace(/^www\./, '').split('/')[0];
}
}
Мониторинг распределённой системы:
const prometheus = require('prom-client');
// Создаём метрики
const parseCounter = new prometheus.Counter({
name: 'positions_parsed_total',
help: 'Общее количество распарсенных позиций',
labelNames: ['search_engine', 'status']
});
const parseHistogram = new prometheus.Histogram({
name: 'parse_duration_seconds',
help: 'Время парсинга одной позиции',
buckets: [0.1, 0.5, 1, 2, 5, 10]
});
// В коде парсера
async parsePosition(keyword, site) {
const timer = parseHistogram.startTimer();
try {
const result = await this.fetchSearchResults(keyword);
parseCounter.inc({ search_engine: 'google', status: 'success' });
return result;
} catch (error) {
parseCounter.inc({ search_engine: 'google', status: 'error' });
throw error;
} finally {
timer();
}
}
Такая архитектура позволяет масштабироваться до десятков тысяч запросов в день при сохранении стабильности и производительности.
Это часть серии материалов по теме «SEO-продвижение». Основная статья серии: Индексация сайта после обновления: 12 способов ускорить процесс в 2026 году.
Читайте также
- Индексация сайта после обновления: 12 способов ускорить процесс в 2026 году — основная статья кластера
- SPA против традиционного сайта: 7 критериев выбора архитектуры для бизнеса в 2026 году
- Node.js vs Python для бэкенда в 2026 году: что выбрать и как это влияет на стоимость разработки
- SEO для интернет-магазина: чек-лист из 30 пунктов
Частые вопросы
В: Сколько стоит запустить парсер на 500 запросов в день?
О: Около $150-200 в месяц: $50-80 на качественные прокси, $30-50 на VPS, $20-30 на мониторинг и резервное копирование. Плюс время разработки — 40-60 часов для полнофункциональной системы.
В: Почему Node.js лучше Python для парсинга?
О: Асинхронность из коробки даёт прирост производительности в 3-5 раз. Python с AsyncIO тоже быстрый, но Node.js проще в разработке и отладке для задач с большим количеством HTTP-запросов.
В: Как часто можно парсить позиции без блокировок?
О: С хорошими прокси — каждые 6-12 часов. С плохими — раз в сутки, и то рискованно. Google блокирует агрессивнее Яндекса, особенно при парсинге более 100 позиций подряд с одного IP.
В: Можно ли парсить мобильную выдачу?
О: Да, добавляем параметр &num=100&gws_rd=cr и меняем User-Agent на мобильный. Но мобильная выдача блокируется чаще — нужны специальные мобильные прокси ($20-50 за IP).
В: Что делать, если Google показывает капчу?
О: Смена IP + задержка 10-15 минут. Если капча повторяется — IP попал в чёрный список поисковика. Нужен новый пул прокси и снижение интенсивности запросов.
В: Как проверить точность парсинга позиций?
О: Сравниваем с ручной проверкой или платными сервисами типа Serpstat. Точность нашего парсера — 94-97% для первых 50 позиций, 85-90% для позиций 51-100.
В: Стоит ли использовать headless-браузеры для парсинга?
О: Только если обычные HTTP-запросы не работают. Puppeteer медленнее в 10-15 раз и жрёт много памяти. Но зато точно обходит JavaScript-защиту и выглядит как настоящий браузер.
Нужна помощь с этим? Обсудить проект с DS495 →
Читайте также: Автопарсер для SEO-продвижения сайта 2026: 8 метрик за ночь