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.







