Розробка бота-парсера нових надходжень у постачальників

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, 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) {
            // Перший запуск — зберігаємо як базовий, новинок немає
            $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 робочих днів.