Managers spend up to 3 hours daily copying data from supplier dashboards. Manual entry leads to error rates of 15%, and outdated prices cause direct losses. The result: lost deals and dissatisfied customers. We automate the collection of products, prices, and stock without human intervention. In our time in the market, we have delivered 30+ projects, reducing data collection time for clients by 80%. For example, an online store with 5,000 SKUs spent 40 hours monthly updating prices; after implementing the parser, the process takes 4 hours autonomously. Budget savings of up to 30%, which in monetary terms could, for example, amount to $2.2k–3.1k per year.
According to the definition, web scraping is the automatic extraction of data from the internet. Here's how it works in practice.
Why manual data collection is costly?
80% of time goes to monotonous tasks. 15% of records contain errors due to human factors. Customers are lost due to outdated prices or out-of-stock items. Automation reduces costs by 30% and completely eliminates input errors.
What data we collect
| Data Type | Example | Source |
|---|---|---|
| Basic fields | Name, price, SKU | Product list page |
| Extended | Description, specs, images | Product detail page |
| Dynamic | Stock, availability status | AJAX requests or DOM |
How the parser works and how we bypass protection?
Typical solution architecture:
- Scheduler (cron / Laravel Horizon) runs tasks on schedule.
- HTTP client (Guzzle / Playwright) fetches supplier pages.
- Parser extracts data using CSS selectors or XPath.
- Normalizer brings prices, dates, and currencies to a unified format.
- Deduplicator checks products by SKU and updates or creates records.
- Notifications for errors or significant price changes.
Modern suppliers use Cloudflare, CAPTCHAs, and dynamic loading. We apply residential proxy rotation, Playwright/Puppeteer browser emulation with random mouse movements, delays from 500 ms to 2 s, and User-Agent switching. If the site uses JavaScript, Playwright loads the page like a real browser and returns the ready DOM.
What is better: Guzzle or Playwright?
| Criterion | Guzzle (PHP) | Playwright (Node) |
|---|---|---|
| Speed | High (no browser) | Medium (browser launch) |
| JS handling | Not supported | Full emulation |
| Resources | Minimal | Requires more RAM |
| Parsing complexity | Enough for static | Required for SPA |
Guzzle processes static pages 2 times faster than Playwright. Combined approach — Guzzle for static, Playwright for SPA — yields the best result.
Technical implementation
Basic parser in PHP
// app/Services/Scrapers/SupplierScraper.php use GuzzleHttp\Client; use Symfony\Component\DomCrawler\Crawler; class SupplierScraper { private Client $client; public function __construct( private string $baseUrl, private array $proxyPool = [] ) { $this->client = new Client([ 'timeout' => 15, 'connect_timeout' => 5, 'headers' => [ 'User-Agent' => $this->randomUserAgent(), 'Accept-Language' => 'ru-RU,ru;q=0.9', 'Accept' => 'text/html,application/xhtml+xml', ], ]); } public function scrapeProductList(string $categoryUrl): array { $html = $this->fetchWithRetry($categoryUrl); $crawler = new Crawler($html); return $crawler->filter('.product-card')->each(function (Crawler $node) { return [ 'url' => $node->filter('a.product-link')->attr('href'), 'title' => trim($node->filter('.product-title')->text()), 'price' => $this->parsePrice($node->filter('.price')->text()), 'sku' => $node->filter('[data-sku]')->attr('data-sku'), ]; }); } public function scrapeProductDetail(string $productUrl): array { $html = $this->fetchWithRetry($this->baseUrl . $productUrl); $crawler = new Crawler($html); return [ 'title' => $crawler->filter('h1.product-name')->text(), 'description' => $crawler->filter('.description')->html(), 'price' => $this->parsePrice($crawler->filter('.current-price')->text()), 'images' => $crawler->filter('.gallery img')->each( fn(Crawler $img) => $img->attr('src') ), 'specs' => $this->extractSpecs($crawler), 'in_stock' => $crawler->filter('.in-stock')->count() > 0, 'sku' => $crawler->filter('[itemprop="sku"]')->text(''), ]; } private function extractSpecs(Crawler $crawler): array { $specs = []; $crawler->filter('.specs-table tr')->each(function (Crawler $row) use (&$specs) { $key = trim($row->filter('td:first-child')->text('')); $val = trim($row->filter('td:last-child')->text('')); if ($key && $val) { $specs[$key] = $val; } }); return $specs; } private function fetchWithRetry(string $url, int $attempts = 3): string { $proxy = $this->proxyPool ? $this->randomProxy() : null; for ($i = 0; $i < $attempts; $i++) { try { $options = $proxy ? ['proxy' => $proxy] : []; $response = $this->client->get($url, $options); return (string) $response->getBody(); } catch (\Exception $e) { if ($i === $attempts - 1) throw $e; sleep(rand(2, 5)); } } } private function parsePrice(string $text): float { return (float) preg_replace('/[^\d.,]/', '', str_replace(',', '.', $text)); } private function randomUserAgent(): string { $agents = [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15', ]; return $agents[array_rand($agents)]; } } Job for background processing
// app/Jobs/ScrapeSupplierProducts.php class ScrapeSupplierProducts implements ShouldQueue { use Queueable; public int $tries = 2; public int $timeout = 300; public function __construct( private int $supplierId, private string $categoryUrl ) {} public function handle( SupplierScraper $scraper, ProductImportService $importer ): void { $products = $scraper->scrapeProductList($this->categoryUrl); foreach ($products as $productPreview) { ScrapeSupplierProductDetail::dispatch( $this->supplierId, $productPreview['url'] )->onQueue('scraper-detail'); usleep(rand(500000, 1500000)); } } } Playwright for JS-heavy sites
// scraper/playwright-worker.js const { chromium } = require('playwright'); async function scrapeProduct(url) { const browser = await chromium.launch({ headless: true }); const context = await browser.newContext({ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)...', viewport: { width: 1366, height: 768 }, }); const page = await context.newPage(); await page.goto(url, { waitUntil: 'networkidle' }); const product = await page.evaluate(() => ({ title: document.querySelector('h1')?.textContent, price: document.querySelector('.price')?.textContent, images: [...document.querySelectorAll('.gallery img')].map(i => i.src), })); await browser.close(); return product; } PHP calls the Node process via proc_open or an HTTP microservice.
Deduplication and update
// app/Services/ProductImportService.php class ProductImportService { public function upsert(int $supplierId, array $data): void { $product = SupplierProduct::updateOrCreate( [ 'supplier_id' => $supplierId, 'supplier_sku' => $data['sku'], ], [ 'title' => $data['title'], 'price' => $data['price'], 'in_stock' => $data['in_stock'], 'description' => $data['description'], 'images' => json_encode($data['images']), 'specs' => json_encode($data['specs']), 'scraped_at' => now(), ] ); if ($product->wasChanged('price')) { $change = abs($product->price - $product->getOriginal('price')); if ($change / $product->getOriginal('price') > 0.05) { PriceChangedNotification::dispatch($product); } } } } Schedule configuration
// app/Console/Kernel.php protected function schedule(Schedule $schedule): void { $schedule->command('scraper:supplier --supplier=1') ->dailyAt('03:00') ->withoutOverlapping(); $schedule->command('scraper:supplier --supplier=1 --prices-only') ->everyFourHours() ->withoutOverlapping(); } Example data structure for parsing
| Field | Type | Example |
|---|---|---|
| sku | string | ART-12345 |
| title | string | Smartphone XYZ |
| price | float | 19999.99 |
| in_stock | bool | true |
Development process and timelines
- Analysis (1-2 days) — study the supplier site structure, identify protection types, agree on fields.
- Design (1 day) — choose the stack, design architecture (queues, schedule, data schema).
- Implementation (3-7 days) — write parsers, normalizers, deduplication, integration with your system.
- Testing (1-2 days) — run on real data, verify correctness and robustness.
- Deployment (1 day) — configure environment, monitoring, notifications, hand over documentation.
Estimated timelines
- Basic parser for one supplier (static HTML, 5-10 fields): from 3 to 5 business days.
- Parser with complex protection or JS rendering: from 7 to 14 days.
- Integration with multiple suppliers into a single pipeline: from 10 business days.
Cost is calculated individually after auditing the target sites.
What's included in the result
- Parser with open-source code (PHP + Laravel or Node.js).
- Integration with your database via API or direct import.
- Schedule and monitoring configuration.
- Documentation for launch and maintenance.
- 30-day warranty on stable operation.
Typical mistakes in parser development
- Ignoring rate limit — the site blocks your IP after 100 requests. Solution: add random delays and rotation.
- Hardcoding HTML structure — the slightest design change breaks the parser. Solution: use data-attributes or XPath.
- Lack of error handling — one supplier failure crashes the entire queue. Solution: isolate tasks and add retries.
- Storing all data in a single table without normalization — duplicates and gaps. Solution: use SKU as the unique key.
Contact us for a consultation — we'll evaluate your project and offer a turnkey solution. Order your parser development and get a ready solution that saves your time and money.







