← Back to list

Web Scraping Made Simple: A Complete Guide to Using Symfony’s Dom-Crawler in Laravel — Part 1

Web scraping has become an essential skill for developers who need to extract data from websites efficiently. Whether you’re building a…

Murilo Livorato · 2025-07-02 22:09 · 13 claps · 5.7 min read
#larave #scrap #laravel-queue #php
Open on Medium ↗

Web Scraping Made Simple: A Complete Guide to Using Symfony’s Dom-Crawler in Laravel — Part 1

Web scraping has become an essential skill for developers who need to extract data from websites efficiently. Whether you’re building a price comparison tool, aggregating news articles, or collecting product information, web scraping can automate the tedious task of manual data collection.

In this comprehensive article , we’ll explore how to use Symfony’s Dom-Crawler component within Laravel to scrape websites like a pro. We’ll cover everything from basic setup to advanced techniques like handling pagination and downloading images.

What You’ll Learn

By the end of this tutorial, you’ll be able to:

  • Set up Dom-Crawler in your Laravel project
  • Scrape real-world websites and extract data
  • Handle complex HTML structures with CSS selectors
  • Download and store images from scraped content
  • Navigate through paginated results
  • Implement best practices for ethical scraping

Why Dom-Crawler?

Symfony’s Dom-Crawler is a powerful PHP library that provides an intuitive API for navigating and manipulating HTML documents. Unlike regex-based solutions, Dom-Crawler:

  • Handles malformed HTML gracefully
  • Provides jQuery-like CSS selector support
  • Offers XPath functionality for complex queries
  • Integrates seamlessly with Laravel’s ecosystem

Getting Started: Installation

Let’s begin by setting up our Laravel project with the necessary dependencies. Open your terminal and navigate to your Laravel project directory:

composer require symfony/dom-crawler symfony/http-client

Here’s what each package does:

  • symfony/dom-crawler: The core DOM manipulation library
  • symfony/css-selector: Enables CSS selector support (like jQuery)
  • guzzlehttp/guzzle: A robust HTTP client for making web requests

Basic Implementation

Let’s start with a simple example to understand the fundamentals. Create a new controller:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Symfony\Component\DomCrawler\Crawler;
use GuzzleHttp\Client;

class WebScraperController extends Controller
{
    public function scrapeBasicExample()
    {
        // Initialize HTTP client
        $client = new Client();

        // Fetch the webpage
        $response = $client->get('https://quotes.toscrape.com');
        $html = $response->getBody()->getContents();

        // Create crawler instance
        $crawler = new Crawler($html);

        // Extract the page title
        $title = $crawler->filter('title')->text();

        return response()->json(['title' => $title]);
    }
}

This basic example demonstrates the three-step process:

  1. Fetch HTML content using Guzzle
  2. Initialize the Dom-Crawler with the HTML
  3. Use CSS selectors to extract data

Real-World Example: Scraping Product Data

Now let’s tackle a more practical scenario. We’ll scrape product information from an e-commerce site. For this example, we’ll use a demo site that’s designed for scraping practice.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Symfony\Component\DomCrawler\Crawler;
use GuzzleHttp\Client;

class ProductScraperController extends Controller
{
    protected $client;

    public function __construct()
    {
        $this->client = new Client([
            'timeout' => 30,
            'verify' => false, // Only for demo sites
            'headers' => [
                'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
            ]
        ]);
    }

    public function scrapeProducts()
    {
        try {
            $url = 'https://scrapeme.live/shop/';
            $response = $this->client->get($url);
            $html = $response->getBody()->getContents();

            $crawler = new Crawler($html);
            $products = [];

            // Extract each product
            $crawler->filter('.product')->each(function (Crawler $node) use (&$products) {
                $product = [
                    'name' => $this->safeExtract($node, '.woocommerce-loop-product__title'),
                    'price' => $this->safeExtract($node, '.price'),
                    'image_url' => $node->filter('img')->count() ? $node->filter('img')->attr('src') : null,
                    'product_url' => $node->filter('a')->count() ? $node->filter('a')->attr('href') : null,
                    'scraped_at' => now()
                ];

                $products[] = $product;
            });

            return response()->json([
                'success' => true,
                'products' => $products,
                'count' => count($products)
            ]);

        } catch (\Exception $e) {
            return response()->json([
                'success' => false,
                'error' => $e->getMessage()
            ], 500);
        }
    }

    private function safeExtract(Crawler $node, $selector)
    {
        $element = $node->filter($selector);
        return $element->count() ? trim($element->text()) : null;
    }
}

Key improvements in this code:

  • Error handling: Wrapped in try-catch blocks
  • Safe extraction: The safeExtract method prevents errors when elements don't exist
  • Proper headers: Added User-Agent to avoid being blocked
  • Data validation: Checks if elements exist before extracting data

Downloading Images

One of the most common requirements in web scraping is downloading ima $baseUrl = ‘https://example.com/properties'; $client = HttpClient::create();ges. Let’s enhance our scraper to handle image downloads:

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Symfony\Component\DomCrawler\Crawler;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\Storage;

class ScrapeImages extends Command
{
    protected $signature = 'scrape:images';
    protected $description = 'Scrape products with images from a website';

    protected $client;

    public function __construct()
    {
        parent::__construct();

        $this->client = new Client([
            'timeout' => 30,
            'headers' => [
                'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
            ]
        ]);
    }

    public function handle()
    {
        try {
            $url = 'https://scrapeme.live/shop/';
            $response = $this->client->get($url);
            $html = $response->getBody()->getContents();

            $crawler = new Crawler($html);
            $products = [];

            $crawler->filter('.product')->each(function (Crawler $node) use (&$products) {
                $imageUrl = $node->filter('img')->count() ? $node->filter('img')->attr('src') : null;
                $localImagePath = null;

                if ($imageUrl) {
                    $localImagePath = $this->downloadImage($imageUrl);
                }

                $product = [
                    'name' => $this->safeExtract($node, '.woocommerce-loop-product__title'),
                    'price' => $this->safeExtract($node, '.price'),
                    'image_url' => $imageUrl,
                    'local_image_path' => $localImagePath,
                    'product_url' => $node->filter('a')->count() ? $node->filter('a')->attr('href') : null,
                ];

                $products[] = $product;
            });

            $this->info("Scraped " . count($products) . " products:");
            foreach ($products as $product) {
                $this->line("• {$product['name']} - {$product['price']} - Saved: {$product['local_image_path']}");
            }

        } catch (\Exception $e) {
            $this->error('Error: ' . $e->getMessage());
        }
    }

    private function downloadImage($imageUrl)
    {
        try {
            $extension = pathinfo($imageUrl, PATHINFO_EXTENSION) ?: 'jpg';
            $filename = 'scraped-images/' . uniqid() . '.' . $extension;

            $imageResponse = $this->client->get($imageUrl);
            $imageContent = $imageResponse->getBody()->getContents();

            Storage::disk('public')->put($filename, $imageContent);

            return $filename;

        } catch (\Exception $e) {
            \Log::error('Failed to download image: ' . $imageUrl . ' - ' . $e->getMessage());
            return null;
        }
    }

    private function safeExtract(Crawler $node, $selector)
    {
        $element = $node->filter($selector);
        return $element->count() ? trim($element->text()) : null;
    }
}

Important considerations for image downloading:

  • File naming: Use unique identifiers to avoid conflicts
  • Error handling: Images might fail to download, so handle gracefully
  • Storage: Use Laravel’s Storage facade for better organization
  • Extensions: Extract proper file extensions from URLs

Handling Pagination

Most e-commerce sites use pagination to display large product catalogs. Here’s how to handle multiple pages:

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\HttpClient\HttpClient;

class ScrapeProperties extends Command
{
    protected $signature = 'scrape:properties';
    protected $description = 'Scrape paginated property listings';

    public function handle()
    {
        $baseUrl = 'https://example.com/properties';
        $client = HttpClient::create();

        // Step 1: Get number of pages
        $firstPage = $client->request('GET', $baseUrl);
        $crawler = new Crawler($firstPage->getContent());

        $pages = $crawler->filter('.pagination a')->each(fn($node) => (int) $node->text());
        $totalPages = max($pages);

        $this->info("Total pages found: $totalPages");

        // Step 2: Loop through each page
        for ($i = 1; $i <= $totalPages; $i++) {
            $this->info("Scraping page $i...");
            $url = $baseUrl . '?page=' . $i;

            $response = $client->request('GET', $url);
            $pageCrawler = new Crawler($response->getContent());

            // Step 3: Loop through each item
            $pageCrawler->filter('.property')->each(function (Crawler $node) {
                $title = $node->filter('.title')->text();
                $link = $node->filter('.detail-link')->attr('href');

                $this->info("Found: $title - $link");

                // You can save to database or dispatch a job here
            });
        }

        $this->info('Scraping completed.');
    }
}
}

Best Practices and Ethics

Web scraping comes with responsibilities. Here are essential best practices:

1. Respect Robots.txt

Always check the target website’s robots.txt file (e.g., https://example.com/robots.txt) to understand what's allowed.

2. Implement Rate Limiting

Add delays between requests to avoid overwhelming servers:

php// Add delay between requests
sleep(1); // 1 second delay
// or
usleep(500000); // 0.5 second delay

3. Handle Errors Gracefully

Implement comprehensive error handling:

try {
    // Scraping code
} catch (ConnectException $e) {
    // Handle connection errors
} catch (RequestException $e) {
    // Handle HTTP errors
} catch (\Exception $e) {
    // Handle other errors
}

4. Use Proper Headers

Always identify your scraper with appropriate headers:

'headers' => [
    'User-Agent' => 'YourBot/1.0 (+http://yoursite.com/bot-info)',
    'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language' => 'en-US,en;q=0.5',
    'Accept-Encoding' => 'gzip, deflate',
    'Connection' => 'keep-alive',
]

5. Legal Considerations

  • Always check the website’s Terms of Service
  • Respect copyright and intellectual property
  • Consider using official APIs when available
  • Be aware of local laws regarding data scraping

Performance Tips

1. Use Queues for Large Jobs

For extensive scraping operations, use Laravel’s queue system:

php artisan make:job ScrapingJob

// In your job class
public function handle()
{
    // Your scraping logic here
}

// Dispatch the job
ScrapingJob::dispatch($url);

2. Use Database Storage

For large datasets, store results in a database:

// Create migration
php artisan make:migration create_scraped_products_table

// In your scraper
foreach ($products as $product) {
    ScrapedProduct::updateOrCreate(
        ['product_url' => $product['product_url']],
        $product
    );
}

Conclusion

Web scraping with Symfony’s Dom-Crawler in Laravel provides a powerful and flexible solution for extracting data from websites. By following the patterns and best practices outlined in this tutorial, you can build robust scrapers that handle real-world challenges like pagination, image downloads, and error handling.

Remember that with great power comes great responsibility. Always scrape ethically, respect server resources, and consider the legal implications of your scraping activities.

Thanks a lot for reading till end. Follow or contact me via:

Github:https://github.com/murilolivorato LinkedIn: https://www.linkedin.com/in/murilo-livorato-80985a4a/


메타데이터
post_id
b0fe061d5de5
slug
web-scraping-made-simple-a-complete-guide-to-using-symfonys-dom-crawler-in-laravel-part-1-b0fe061d5de5
url
https://medium.com/@murilolivorato/web-scraping-made-simple-a-complete-guide-to-using-symfonys-dom-crawler-in-laravel-part-1-b0fe061d5de5
canonical_url
https://medium.com/@murilolivorato/web-scraping-made-simple-a-complete-guide-to-using-symfonys-dom-crawler-in-laravel-part-1-b0fe061d5de5
author_url
https://medium.com/@murilolivorato
status
ok
fetched_at
2026-07-19 04:24:24