Мы — команда Datacol, занимаемся созданием парсеров уже более 15 лет. Неоднократно создавали для наших клиентов парсера Whatsapp. Понимаем, что возможно вы захотите самостоятельно создать такой парсер, используя инструменты для вайб-кодинга. Эта статья поможет вам создать такой парсер. Но если вы не хотите разбираться в нюансах разработки, обратитесь к нам, чтобы мы создали пасрер Whatsapp под ваши требования или отправили вам готовый парсер для тестирования.
Типичный сценарий: в WhatsApp десятки каналов и групп, где организаторы постят анонсы концертов, выставок, митапов и фестивалей. Всё это разбросано по разным чатам, написано в произвольном формате и теряется среди обычного трёпа. Задача — автоматически собирать эти сообщения, структурировать их через OpenAI и публиковать в единую базу городского сайта ивентов. В этой статье — все технические нюансы, на которые потеряешь время, если не знать заранее.
1. Фундаментальное ограничение: нет официального API
WhatsApp не предоставляет публичного API для личных аккаунтов. WhatsApp Business Cloud API существует, но работает только для бизнес-аккаунтов и не позволяет читать чужие каналы — только получать входящие сообщения от пользователей, которые сами написали боту.
Для чтения публичных каналов и групп единственный практичный путь — автоматизация WhatsApp Web через библиотеку whatsapp-web.js. Она запускает headless Chromium (Puppeteer) и общается с внутренним JS API WhatsApp Web через WebSocket.
Важно: whatsapp-web.js — это не официальная интеграция. Библиотека реверс-инженерит внутренний протокол WhatsApp Web. Meta периодически меняет внутреннее API, что ломает библиотеку. Закладывайте время на обновления и следите за issues в репозитории.
2. Каналы vs Группы: принципиальная разница
В WhatsApp есть два типа объектов, где публикуются ивенты — и у них разная механика парсинга:
| Параметр | Группы (Groups) | Каналы (Channels / Newsletters) |
|---|---|---|
| Тип объекта | chat.isGroup === true | chat.isChannel === true |
| Доступность | Нужно быть участником группы | Нужно быть подписчиком канала |
| Получение сообщений | fetchMessages() работает | Поддержка через chat.fetchMessages(), но экспериментальная |
| Риск бана | Средний (вы участник, читаете как обычно) | Низкий (подписчики не отслеживаются) |
| Событие real-time | client.on(‘message’) | client.on(‘message’) (работает нестабильно) |
Практический вывод: большинство городских ивент-организаторов используют именно группы, а не каналы — они появились позже и менее распространены. Начинайте с групп, добавляйте поддержку каналов как расширение.
3. Инициализация клиента и стратегия аутентификации
Базовая настройка клиента с LocalAuth для сохранения сессии:
const client = new Client({
authStrategy: new LocalAuth({
dataPath: process.env.APPDATA
? `${process.env.APPDATA}/whatsapp-events-scraper`
: `${process.env.HOME}/.whatsapp-events-scraper`
}),
puppeteer: {
headless: true,
args: [
'--no-sandbox', // обязательно для Linux/Docker
'--disable-setuid-sandbox',
'--disable-dev-shm-usage' // важно при ограниченном /dev/shm в контейнерах
]
}
});
Нюансы аутентификации
- Первый запуск — в терминале генерируется QR-код. Нужно отсканировать его с телефона через WhatsApp — Связанные устройства.
- Персистентность сессии — LocalAuth сохраняет профиль Chromium и ключи шифрования в указанной папке. Не удаляйте её между запусками.
- Истечение сессии — сессия слетает, если телефон долго без интернета или если кто-то вручную разлогинил устройство. Нужен мониторинг и алерты.
- Docker/VPS — папку с сессией монтируйте как volume, иначе каждый перезапуск контейнера требует повторного сканирования QR.
const qrcode = require('qrcode-terminal');
client.on('qr', qr => {
qrcode.generate(qr, { small: true });
console.log('Отсканируйте QR-код через WhatsApp на телефоне');
});
client.on('authenticated', () => console.log('Сессия восстановлена'));
client.on('auth_failure', () => console.error('Сессия истекла - нужен новый QR'));
client.on('ready', () => console.log('Клиент готов к работе'));
4. Два режима работы парсера WhatsApp
Режим A: Real-time слушатель
Процесс остаётся запущенным постоянно и реагирует на новые сообщения по мере их появления. Оптимально для продакшена.
const chat = await msg.getChat();
// Фильтр: только нужные группы/каналы
if (!isTargetChat(chat)) return;
// Фильтр: только текстовые сообщения (не стикеры, не войсы)
if (msg.type !== 'chat') return;
await processMessage(msg.body, chat.name, msg.timestamp);
});
Режим B: Пакетное извлечение по расписанию
Запускается по cron или Task Scheduler, забирает сообщения за период с последнего запуска и завершается.
const lastCheckTime = await getLastCheckTime(); // из JSON или БД
const currentTime = Date.now();
const chats = await client.getChats();
const targets = chats.filter(c =>
(c.isGroup || c.isChannel) && isTargetChat(c)
);
for (const chat of targets) {
// КРИТИЧНО: лимит ~100 сообщений - нет пагинации в API
const messages = await chat.fetchMessages({ limit: 100 });
const fresh = messages.filter(m => {
const msgMs = m.timestamp * 1000; // WhatsApp timestamp в секундах!
return msgMs >= lastCheckTime && msgMs <= currentTime;
});
for (const msg of fresh) {
if (msg.type !== 'chat') continue;
await processMessage(msg.body, chat.name, msg.timestamp);
await sleep(800); // пауза между запросами
}
}
await saveLastCheckTime(currentTime);
await client.destroy();
process.exit(0);
});
Жёсткое ограничение: fetchMessages({ limit: N }) возвращает максимум последние N сообщений из кэша WhatsApp Web. Нет пагинации, нет доступа к истории старше загруженного кэша. Для ивент-парсера это обычно не критично — анонсы актуальны в горизонте нескольких дней, но уйти глубже в историю не получится.
5. Фильтрация чатов по названию
Названия групп меняются, поэтому жёсткое совпадение ненадёжно. Используйте частичное совпадение по ключевым словам:
'ивент', 'event', 'афиша', 'анонс',
'концерт', 'выставка', 'митап', 'тбилиси events',
'что делать', 'куда пойти'
];
function isTargetChat(chat) {
if (!chat.isGroup && !chat.isChannel) return false;
const name = chat.name.toLowerCase();
return TARGET_KEYWORDS.some(kw => name.includes(kw.toLowerCase()));
}
Дополнительно можно вести whitelist конкретных ID чатов — они стабильны даже при переименовании группы:
const WHITELISTED_IDS = ['120363xxxxxx@g.us', '120363yyyyyy@g.us'];
function isTargetChat(chat) {
if (WHITELISTED_IDS.includes(chat.id._serialized)) return true;
// ... fallback на keyword matching
}
6. Слой AI: OpenAI как классификатор и структуратор
Сырые сообщения из WhatsApp — это хаос: смесь анонсов, обсуждений, репостов, рекламы и флуда. OpenAI решает две задачи одновременно — определяет, является ли сообщение ивентом, и если да — извлекает структурированные данные.
Ты - парсер городских мероприятий. Анализируй сообщения из WhatsApp групп.
Сегодняшняя дата: ${new Date().toLocaleDateString('ru-RU')}.
Если сообщение является анонсом конкретного единичного события - верни JSON:
{
"is_event": true,
"title": "Название события",
"date": "YYYY-MM-DD",
"time": "HH:MM или null",
"location": "Место проведения или null",
"description": "Краткое описание 1-2 предложения",
"category": "concert|exhibition|meetup|sport|festival|other",
"price": "бесплатно / 500 руб / null",
"contact": "ссылка или телефон или null"
}
Если сообщение НЕ является анонсом события - верни:
{ "is_event": false }
Отклоняй:
- Обычные разговоры и обсуждения
- Рекламу товаров/услуг без конкретного события
- Расписания с множеством дат (серии занятий, еженедельные встречи)
- Новости без привязки к мероприятию
- Репосты без новой информации
Отвечай ТОЛЬКО валидным JSON без пояснений и markdown.
`;
async function classifyMessage(text) {
const response = await openai.chat.completions.create({
model: 'gpt-4o-mini',
max_tokens: 300,
temperature: 0.2, // низкая температура = детерминированная классификация
response_format: { type: 'json_object' },
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: text.slice(0, 2000) } // ограничение длины
]
});
return JSON.parse(response.choices[0].message.content);
}
Ключевые решения в промпте
- Передача текущей даты — без этого «завтра» и «в эту субботу» не разрешаются в конкретные даты.
- Явный список отклонений — перечисление того, что НЕ является ивентом, снижает false positive.
- JSON mode (response_format: { type: ‘json_object’ }) — гарантирует валидный JSON на выходе без markdown-обёртки.
- Температура 0.2 — классификация должна быть стабильной, а не творческой.
- gpt-4o-mini — достаточно мощный для задачи классификации при стоимости на порядок ниже GPT-4o.
7. Дедупликация: одно событие — много репостов
Анонс одного концерта может появиться в 5 разных группах и 3 раза в одной группе (анонс, напоминание, «сегодня!»). Нужна дедупликация на двух уровнях:
Уровень 1: Схожесть строк (до вызова OpenAI)
async function isDuplicate(newText, existingTexts) {
for (const existing of existingTexts) {
const score = stringSimilarity.compareTwoStrings(
newText.toLowerCase().trim(),
existing.toLowerCase().trim()
);
if (score >= 0.72) return true;
}
return false;
}
// Загружаем тексты уже сохранённых ивентов за ±3 дня
// Нет смысла сравнивать с ивентами прошлого месяца
Уровень 2: Семантическое совпадение через OpenAI
Для случаев, когда один ивент описан разными словами (например, русский и английский варианты анонса):
function isSameEvent(eventA, eventB) {
// Совпадение даты + локации = высокая вероятность дубля
if (eventA.date === eventB.date && eventA.location === eventB.location) {
const titleScore = stringSimilarity.compareTwoStrings(
eventA.title.toLowerCase(),
eventB.title.toLowerCase()
);
if (titleScore >= 0.5) return true;
}
return false;
}
8. Хранение данных: Supabase как бэкенд
Supabase (PostgreSQL) удобен для этого стека — есть встроенный REST API, который можно использовать прямо из Node.js без ORM, и real-time подписки для фронтенда сайта ивентов.
CREATE TABLE events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title TEXT NOT NULL,
date DATE NOT NULL,
time TIME,
location TEXT,
description TEXT,
category TEXT,
price TEXT,
contact TEXT,
source_chat TEXT, -- название WhatsApp группы
raw_text TEXT, -- исходное сообщение для отладки
created_at TIMESTAMPTZ DEFAULT NOW(),
is_approved BOOLEAN DEFAULT false -- модерация перед публикацией
);
const { createClient } = require('@supabase/supabase-js');
const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY);
async function saveEvent(eventData, sourceChat, rawText) {
const { error } = await supabase.from('events').insert({
...eventData,
source_chat: sourceChat,
raw_text: rawText,
is_approved: false // всегда идёт на ревью сначала
});
if (error) console.error('Supabase error:', error);
}
Архитектурный совет: добавьте поле is_approved и не публикуйте ивенты автоматически без модерации. OpenAI ошибается — особенно с нестандартными форматами дат («в ближайшую пятницу») и адресами. Простая admin-панель с кнопками «одобрить / отклонить» спасёт от публикации мусора на сайте.
9. Обработка временных меток
Классический источник багов: WhatsApp возвращает Unix timestamp в секундах, JavaScript работает с миллисекундами.
if (msg.timestamp >= lastCheckTime) { ... } // lastCheckTime в мс, msg.timestamp в сек
// ПРАВИЛЬНО
const msgTimeMs = msg.timestamp * 1000;
if (msgTimeMs >= lastCheckTime && msgTimeMs <= currentTime) { ... }
// Форматирование для отображения
const date = new Date(msg.timestamp * 1000).toLocaleString('ru-RU', {
timeZone: 'Asia/Tbilisi' // или нужный timezone города
});
Отдельный нюанс — таймзона. Если парсер запущен на сервере в UTC, а события происходят в другом городе, даты могут съезжать на сутки. Всегда указывайте timezone явно и в системном промпте для OpenAI, и при сохранении в БД.
10. Rate Limiting и защита от бана
WhatsApp детектирует автоматизацию по частоте и паттернам API-вызовов. Ключевые меры:
- Паузы между вызовами — минимум 0.5-1 секунда между fetchMessages() для разных групп.
- Только чтение — не отправляйте автоматические сообщения в ответ. Любая автоотправка многократно повышает риск бана.
- Реалистичные паттерны — не опрашивайте 50 групп за 10 секунд. Добавляйте случайный jitter к задержкам.
- Один номер — один парсер — номер телефона, привязанный к парсеру, несёт риск. Используйте отдельный номер, не основной рабочий.
- Телефон должен быть онлайн — WhatsApp Web перестаёт работать, если телефон с привязанным аккаунтом отключается от интернета дольше чем на несколько часов.
const jitter = () => Math.floor(Math.random() * 500); // 0-500мс случайная добавка
for (const chat of targetChats) {
await processChat(chat);
await sleep(1000 + jitter()); // 1.0 - 1.5 сек между группами
}
11. Персистентность состояния между запусками
Для пакетного режима по расписанию нужно хранить время последней проверки, чтобы не обрабатывать одни и те же сообщения дважды:
const path = require('path');
const STATE_FILE = path.join(
process.env.APPDATA || process.env.HOME,
'whatsapp-events-scraper',
'runtime.json'
);
async function getLastCheckTime() {
try {
const data = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
return data.lastCheckTime;
} catch {
// Первый запуск - берём последние 24 часа
return Date.now() - 24 * 60 * 60 * 1000;
}
}
async function saveLastCheckTime(time) {
fs.mkdirSync(path.dirname(STATE_FILE), { recursive: true });
fs.writeFileSync(STATE_FILE, JSON.stringify({
lastCheckTime: time,
lastCheckTimeFormatted: new Date(time).toISOString()
}));
}
Не храните state-файл в директории скрипта — при запуске через Task Scheduler или systemd без прав на запись в рабочую директорию файл не создастся. Используйте %APPDATA% на Windows или $HOME на Linux.
12. Уведомления о новых ивентах: Telegram-бот
Добавьте Telegram-бот для мониторинга — он уведомляет команду о каждом найденном ивенте, ожидающем одобрения:
const text = `
🎉 *Новый ивент найден*
📍 Источник: ${sourceChat}
*${event.title}*
📅 ${event.date}${event.time ? ' в ' + event.time : ''}
📍 ${event.location || 'место не указано'}
💰 ${event.price || 'цена не указана'}
${event.description}
[Открыть в панели модерации](${process.env.ADMIN_URL}/events/pending)
`.trim();
await fetch(`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendMessage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: process.env.TELEGRAM_CHAT_ID,
text,
parse_mode: 'Markdown'
})
});
}
13. Проблема Puppeteer/Chromium в продакшене
whatsapp-web.js при установке скачивает Chromium (~150-170MB). Это создаёт ряд проблем:
- Первая установка — npm install долгий, особенно на слабом VPS.
- Память — headless Chrome потребляет 200-400MB RAM. На VPS с 512MB это проблема.
- Linux-сервер — обязательны флаги —no-sandbox и —disable-setuid-sandbox, иначе Chromium не запустится без root.
- Docker — нужны дополнительные зависимости. Используйте base image node:18-bullseye и доустанавливайте chromium-browser через apt, затем указывайте путь к нему явно вместо встроенного.
FROM node:18-bullseye-slim
RUN apt-get update && apt-get install -y \
chromium \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# Монтируем сессию как volume
VOLUME ["/root/.whatsapp-events-scraper"]
CMD ["node", "scraper.js"]
Чеклист запуска
| Шаг | Действие | Нюанс |
|---|---|---|
| Node.js | Установить v18+ | v16 не поддерживается |
| npm install | Скачает Chromium автоматически | ~150MB, нужен стабильный интернет |
| .env файл |
OPENAI_API_KEY, SUPABASE_URL, SUPABASE_KEY, TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID |
Не коммитьте в git |
| Первый запуск | Отсканировать QR с телефона | Телефон должен оставаться онлайн |
| Сессия | Убедиться, что папка сессии персистентна | Volume в Docker, не удалять при деплое |
| Расписание | cron (Linux) или Task Scheduler (Windows) | Оптимально каждые 2–4 часа |
| Мониторинг | Алерт в Telegram при auth_failure | Сессия может слететь без предупреждения |
| Модерация | Admin-панель для одобрения ивентов | Не публикуйте автоматически без ревью |
Общая архитектура системы
Полный пайплайн от WhatsApp до сайта выглядит так:
|
v
whatsapp-web.js <- автоматизация WhatsApp Web через Puppeteer
|
v
Фильтр по чату <- keyword matching + whitelist ID
|
v
Фильтр дублей <- string similarity против свежих записей в БД
|
v
OpenAI gpt-4o-mini <- классификация + структурирование в JSON
|
/ \
is_event not_event
| |
v drop
Supabase
(is_approved=false)
|
v
Telegram-бот <- уведомление команды о новом ивенте
|
v
Ручная модерация <- одобрить / отклонить в admin-панели
|
v
Сайт ивентов <- Supabase REST API или real-time подписка
Фундаментальный риск архитектуры
Вся система строится на том, что WhatsApp Web остаётся доступным и whatsapp-web.js успевает за внутренними изменениями Meta. Это не официальный канал интеграции — Meta не обязана уведомлять о breaking changes и периодически намеренно усложняет реверс-инжиниринг своего протокола.
Для продакшн-системы закладывайте:
- Мониторинг auth_failure и disconnected событий с немедленными алертами
- Регулярные обновления whatsapp-web.js (следите за релизами на GitHub)
- Fallback-источники данных — Telegram-каналы с теми же организаторами, Instagram, городские сайты афиш
- Архитектуру, в которой WhatsApp — один из источников, а не единственный









