Разработка бота-парсера новых поступлений у поставщиков

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Разработка бота-парсера новых поступлений у поставщиков
Средняя
~3-5 рабочих дней
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Разработка бота-парсера новых поступлений у поставщиков

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

Стратегии обнаружения новинок

По дате добавления — если сайт поставщика показывает дату появления товара:

// app/Services/NewArrivals/DateBasedDetector.php
class DateBasedDetector
{
    public function detectNew(string $categoryUrl, \DateTimeInterface $since): array
    {
        $page = 1;
        $newProducts = [];

        do {
            $items = $this->scrapePage($categoryUrl, $page);
            $hasOlderItems = false;

            foreach ($items as $item) {
                $itemDate = $this->parseDate($item['date_added'] ?? '');

                if ($itemDate && $itemDate < $since) {
                    $hasOlderItems = true;
                    break; // Дальше только старые товары
                }

                if (!$this->existsInDatabase($item['sku'])) {
                    $newProducts[] = $item;
                }
            }

            $page++;
        } while (!$hasOlderItems && count($items) > 0);

        return $newProducts;
    }
}

По секции "Новинки" — большинство поставщиков имеют отдельный URL:

// config/suppliers.php
'supplier_abc' => [
    'new_arrivals_url' => 'https://supplier.ru/catalog/new/',
    'new_arrivals_selector' => '.product-card',
    'strategy' => 'new_section', // Парсим только этот раздел
],

По сравнению SKU — универсальный метод, не зависящий от структуры сайта:

// app/Services/NewArrivals/SkuDiffDetector.php
class SkuDiffDetector
{
    public function detect(int $supplierId, array $currentSkus): array
    {
        // Загружаем предыдущий снапшот SKU
        $previousSnapshot = SupplierSnapshot::where('supplier_id', $supplierId)
            ->latest()
            ->first();

        if (!$previousSnapshot) {
            // Первый запуск — сохраняем как baseline, новинок нет
            $this->saveSnapshot($supplierId, $currentSkus);
            return [];
        }

        $previousSkus = $previousSnapshot->sku_list;

        $newSkus = array_diff($currentSkus, $previousSkus);
        $removedSkus = array_diff($previousSkus, $currentSkus);

        // Обновляем снапшот
        $this->saveSnapshot($supplierId, $currentSkus);

        // Логируем снятые с производства товары
        if (!empty($removedSkus)) {
            Log::info("Supplier #{$supplierId}: removed SKUs", ['skus' => $removedSkus]);
            SupplierProductsRemoved::dispatch($supplierId, $removedSkus);
        }

        return $newSkus;
    }

    private function saveSnapshot(int $supplierId, array $skus): void
    {
        SupplierSnapshot::create([
            'supplier_id' => $supplierId,
            'sku_list'    => $skus,
            'sku_count'   => count($skus),
            'captured_at' => now(),
        ]);
    }
}

Полный цикл обнаружения и обработки

// app/Jobs/CheckSupplierNewArrivals.php
class CheckSupplierNewArrivals implements ShouldQueue
{
    public int $tries = 3;
    public int $timeout = 600;

    public function handle(
        SupplierScraper $scraper,
        SkuDiffDetector $detector,
        NewArrivalsNotifier $notifier
    ): void {
        $supplier = Supplier::findOrFail($this->supplierId);

        // Шаг 1: Получить все SKU с сайта поставщика
        $allProducts = $scraper->scrapeAllProductSkus($supplier);
        $currentSkus = array_column($allProducts, 'sku');

        // Шаг 2: Определить новые SKU
        $newSkus = $detector->detect($this->supplierId, $currentSkus);

        if (empty($newSkus)) {
            Log::info("No new arrivals for supplier #{$this->supplierId}");
            return;
        }

        // Шаг 3: Загрузить детали по новым товарам
        $newProducts = array_filter(
            $allProducts,
            fn($p) => in_array($p['sku'], $newSkus)
        );

        // Шаг 4: Уведомление
        $notifier->notify($supplier, $newProducts);

        // Шаг 5: Авто-импорт если настроен
        if ($supplier->auto_import_new_arrivals) {
            foreach ($newProducts as $product) {
                ImportNewSupplierProduct::dispatch($this->supplierId, $product)
                    ->onQueue('imports');
            }
        } else {
            // Сохраняем как "ожидает проверки"
            foreach ($newProducts as $product) {
                PendingImport::create([
                    'supplier_id' => $this->supplierId,
                    'data'        => $product,
                    'status'      => 'pending_review',
                ]);
            }
        }

        Log::info("Found new arrivals", [
            'supplier_id' => $this->supplierId,
            'count'       => count($newProducts),
        ]);
    }
}

Уведомления о новинках

// app/Notifications/NewSupplierArrivalsNotification.php
class NewSupplierArrivalsNotification extends Notification implements ShouldQueue
{
    use Queueable;

    public function via($notifiable): array
    {
        return ['mail', 'slack'];
    }

    public function toMail($notifiable): MailMessage
    {
        return (new MailMessage)
            ->subject("Новые поступления: {$this->supplier->name} ({$this->count} товаров)")
            ->line("Обнаружено {$this->count} новых товаров у поставщика **{$this->supplier->name}**")
            ->line("Дата обнаружения: " . now()->format('d.m.Y H:i'))
            ->action('Просмотреть новинки', route('admin.pending-imports.index', [
                'supplier_id' => $this->supplier->id,
            ]))
            ->line('Товары ожидают проверки перед публикацией.');
    }

    public function toSlack($notifiable): SlackMessage
    {
        return (new SlackMessage)
            ->content(
                "🆕 *{$this->supplier->name}*: {$this->count} новых товаров\n" .
                implode("\n", array_map(
                    fn($p) => "• {$p['sku']} — {$p['name']}",
                    array_slice($this->products, 0, 10)
                ))
            );
    }
}

Очередь для ручной проверки

Новые товары часто требуют ручной проверки: проверка категории, добавление SEO-описания, проверка фотографий. Интерфейс для модератора:

// app/Http/Controllers/Admin/PendingImportController.php
class PendingImportController extends Controller
{
    public function index(Request $request): Response
    {
        $pending = PendingImport::query()
            ->with('supplier')
            ->when($request->supplier_id, fn($q, $id) => $q->where('supplier_id', $id))
            ->where('status', 'pending_review')
            ->orderBy('created_at', 'desc')
            ->paginate(50);

        return Inertia::render('Admin/PendingImports/Index', [
            'imports' => $pending,
        ]);
    }

    public function approve(PendingImport $import): RedirectResponse
    {
        ImportNewSupplierProduct::dispatch($import->supplier_id, $import->data);
        $import->update(['status' => 'approved']);

        return back()->with('success', 'Товар отправлен в импорт');
    }

    public function reject(PendingImport $import, Request $request): RedirectResponse
    {
        $import->update([
            'status' => 'rejected',
            'reject_reason' => $request->reason,
        ]);
        return back()->with('success', 'Товар отклонён');
    }
}

Расписание

// Проверка новинок — каждый день утром
$schedule->command('check:new-arrivals --all-suppliers')
    ->dailyAt('08:00')
    ->withoutOverlapping();

// Приоритетные поставщики — чаще
$schedule->command('check:new-arrivals --supplier=priority')
    ->everyFourHours()
    ->withoutOverlapping();

Хранение снапшотов

Снапшоты накапливаются — нужна очистка старых:

// Удаляем снапшоты старше 90 дней, оставляем по одному на месяц
$schedule->command('snapshots:cleanup --keep-monthly --older-than=90')
    ->weekly();

Срок разработки: детектор новинок для 1 поставщика с уведомлениями и очередью на проверку — 4-6 рабочих дней.