PHP Web Scraping
Web scraping in PHP works with what most PHP projects already have: cURL or Guzzle to download the page, and Symfony DomCrawler (or the built-in DOM extension) to pick out data with CSS selectors or XPath. For pages that need JavaScript, Symfony Panther drives a real Chrome or Firefox from PHP. A Guzzle and DomCrawler scraper is about 25 lines and installs with one Composer command. ProxyField's residential proxies plug into Guzzle's proxy option or cURL's CURLOPT_PROXY, with 412M+ IPs in 195+ countries from $1.95/GB; start with the Easy Trial: 1 GB for $2.95.
- residential IPs
- 412M+
- from, traffic never expires
- $1.95/GB
- Easy Trial
- 1 GB for $2.95
On this page 9 sections
Is PHP good for web scraping?
Yes, for most jobs, and especially when the scraper lives inside a PHP application (a Laravel or Symfony site that imports prices, listings or feeds). PHP has a mature HTTP stack, fast HTML parsing through libxml2, and frameworks with queues and schedulers to run scrapers on a timetable. Its weak spots are concurrency (asynchronous requests need Guzzle's promise pools or extensions) and browser automation, where Python and Node.js have more tools. How PHP compares with other languages is covered in choosing a language for web scraping.
PHP web scraping libraries
| Library | What it does | Runs JavaScript | Install |
|---|---|---|---|
| cURL extension | Downloads pages; full control over headers, cookies, proxies | No | Bundled with most PHP builds |
| Guzzle | HTTP client with middleware, retries, concurrent requests | No | composer require guzzlehttp/ |
| Symfony HttpClient | HTTP client with async responses | No | composer require symfony/ |
DOM extension (DOMDocument, DOMXPath) | Parses HTML, queries with XPath; PHP 8.4 adds Dom\HTMLDocument with CSS selectors | No | Built in |
| Symfony DomCrawler + CssSelector | Parses HTML, queries with CSS selectors or XPath, follows links and forms | No | composer require symfony/ |
Symfony BrowserKit HttpBrowser | A browser-like client (cookies, history, forms) on top of HttpClient and DomCrawler | No | composer require symfony/ |
| Symfony Panther | Drives real Chrome or Firefox through WebDriver | Yes | composer require symfony/ |
| Roach PHP | A Scrapy-style framework: spiders, middleware, item pipelines | No (by default) | composer require roach-php/ |
| spatie/crawler | Crawls a site with Guzzle and calls your observer per page | Optional | composer require spatie/ |
Goutte, the PHP scraper many older tutorials use, is deprecated and its repository is archived: since version 4 it was only a thin wrapper around Symfony's HttpBrowser, and its README tells you to switch to that class (Goutte on GitHub).
Web scraping with PHP cURL
The cURL extension needs no Composer package, which makes it the quickest start. Download the page, then parse it with the DOM extension and XPath:
<?php
$ch = curl_init('https://books.toscrape.com/');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true, // return the body instead of printing it
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; books-demo/1.0)',
]);
$html = curl_exec($ch);
if ($html === false) {
exit('cURL error: ' . curl_error($ch) . PHP_EOL);
}
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
libxml_use_internal_errors(true); // real-world HTML is rarely valid
$dom = new DOMDocument();
$dom->loadHTML($html);
$xpath = new DOMXPath($dom);
foreach ($xpath->query('//article[contains(@class, "product_pod")]') as $book) {
$title = $xpath->query('.//h3/a', $book)->item(0)->getAttribute('title');
$price = trim($xpath->query('.//p[contains(@class, "price_color")]', $book)->item(0)->textContent);
echo "$title: $price\n";
}On PHP 8.4 and later, the new DOM API parses HTML5 and accepts CSS selectors directly: $doc = Dom\HTMLDocument::createFromString($html, LIBXML_NOERROR); foreach ($doc->querySelectorAll('article.product_pod h3 a') as $a) { ... }. cURL options for proxies, cookies and headers are covered in the cURL proxy guide.
A PHP web scraper with Guzzle and Symfony DomCrawler
Guzzle handles the requests and DomCrawler the parsing, with CSS selectors you can test in your browser's DevTools. This script walks every page of a practice catalog, waits a second between pages and saves JSON:
composer require guzzlehttp/guzzle symfony/dom-crawler symfony/css-selector<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
use Symfony\Component\DomCrawler\Crawler;
$client = new Client([
'timeout' => 30,
'headers' => ['User-Agent' => 'Mozilla/5.0 (compatible; books-demo/1.0)'],
]);
$books = [];
$url = 'https://books.toscrape.com/catalogue/page-1.html';
while ($url) {
$html = (string) $client->get($url)->getBody();
$page = new Crawler($html, $url); // the URL lets links resolve
$page->filter('article.product_pod')->each(function (Crawler $book) use (&$books) {
$books[] = [
'title' => $book->filter('h3 a')->attr('title'),
'price' => $book->filter('.price_color')->text(),
'inStock' => $book->filter('.instock')->count() > 0,
];
});
$next = $page->filter('li.next a');
$url = $next->count() ? $next->link()->getUri() : null; // absolute URL or null
sleep(1);
}
file_put_contents('books.json', json_encode($books, JSON_PRETTY_PRINT));
echo count($books) . " books saved\n";filter() takes a CSS selector, filterXPath() an XPath expression, and each() maps the matches to an array. A request that returns 4xx or 5xx throws a Guzzle exception; catch GuzzleHttp\Exception\RequestException per page so one error does not stop the run.
A Laravel scraper
In Laravel, use the Http client (a Guzzle wrapper) with its built-in retries, parse with DomCrawler, and run the job as an Artisan command on the scheduler:
// app/Console/Commands/ScrapeBooks.php -> php artisan scrape:books
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
use Symfony\Component\DomCrawler\Crawler;
class ScrapeBooks extends Command
{
protected $signature = 'scrape:books';
public function handle(): void
{
$response = Http::timeout(30)
->retry(3, 1000) // 3 tries, 1 s apart
->withUserAgent('Mozilla/5.0 (compatible; books-demo/1.0)')
->get('https://books.toscrape.com/');
$crawler = new Crawler($response->body());
$crawler->filter('article.product_pod')->each(function (Crawler $book) {
$this->line($book->filter('h3 a')->attr('title') . ' ' . $book->filter('.price_color')->text());
});
}
}On Laravel 11 and later, schedule it in routes/console.php with Schedule::command('scrape:books')->daily();, and for many URLs dispatch one queued job per page so failures retry independently. The same structure works in Symfony with a console command, HttpClient and the Messenger component.
Scraping JavaScript-rendered pages with PHP: Symfony Panther
Guzzle and cURL see only the HTML the server sends. When the data is built by JavaScript, first look in the browser's DevTools (Network tab) for the JSON request the page makes and call it with Guzzle. If there is none, Panther starts a real Chrome or Firefox through the W3C WebDriver protocol and gives you the same DomCrawler API on the rendered page (Symfony Panther):
<?php
require 'vendor/autoload.php';
use Symfony\Component\Panther\Client;
$client = Client::createChromeClient(); // needs Chrome and chromedriver
$client->request('GET', 'https://quotes.toscrape.com/js/');
$crawler = $client->waitFor('.quote'); // wait until the quotes are rendered
$quotes = $crawler->filter('.quote .text')->each(fn ($node) => $node->text());
print_r($quotes);
$client->quit();PHP can also drive Selenium directly through the php-webdriver library, which Panther is built on. A browser costs far more time and traffic per page than an HTTP request; headless browser scraping explains when it is worth it.
A PHP script to crawl a website
To collect every page of a site, keep a queue and a list of visited URLs, as in any crawler, or let a library do it. spatie/crawler follows links with Guzzle, respects robots.txt by default and calls your observer class for each page it fetches; Roach PHP gives you Scrapy-style spiders with item pipelines. For a small job, a loop over a queue with DomCrawler's filter('a')->links() is enough. The general method is in how to crawl a website.
Using proxies in PHP scrapers
When a site limits requests per IP, route them through a rotating residential proxy. In Guzzle it is one option, per client or per request:
$client = new GuzzleHttp\Client([
'proxy' => 'http://<login>-country-us-city-new_york:<password>@gate.proxyfield.io:7000',
'timeout' => 30,
]);
echo $client->get('https://api.ipify.org')->getBody(); // the exit IPWith cURL, set CURLOPT_PROXY => 'http://gate.proxyfield.io:7000' and CURLOPT_PROXYUSERPWD => '<login>:<password>'; in Laravel, Http::withOptions(['proxy' => '...']). <login>-country-us-city-new_york asks for an IP in New York, United States. Region, session and session time go in the same username: -region-<region>, -session-<id> and -ttl-<seconds> (30 to 129,600 seconds, 20 minutes by default), as in <login>-country-us-region-new_york-city-new_york-session-he9f2qga-ttl-600; every part after the login is optional. Keep the request rate low, read the site's terms and collect only public data; see web scraping best practices and is web scraping legal. ProxyField residential proxies cost $2.95/GB for 1 GB down to $1.95/GB from 1,000 GB, with no subscription; the traffic never expires, and all purchases are final (residential and mobile proxy pricing).
PHP Web Scraping FAQ
Is PHP good for web scraping?
ProxyField's residential proxies (412M+ residential IPs, from $1.95/GB) plug into Guzzle's proxy option, and PHP is good for web scraping, especially inside a Laravel or Symfony application, with Guzzle or cURL for requests and DomCrawler for parsing. Python and Node.js have more browser-automation tools, but PHP handles static pages and JSON APIs well.
How do I scrape a website with PHP cURL?
Create a handle with curl_init($url), set CURLOPT_RETURNTRANSFER, a timeout and a user agent, run curl_exec(), then load the HTML into DOMDocument and query it with DOMXPath. Check for false and the HTTP status before parsing.
What is the best PHP library for web scraping?
For most projects, Guzzle for requests plus Symfony DomCrawler with CssSelector for parsing. Add Symfony Panther for pages that need a real browser, and Roach PHP or spatie/crawler when you need a crawling framework.
Which libraries are used for web scraping in PHP?
For requests, the cURL extension, Guzzle and Symfony HttpClient; for parsing, the built-in DOM extension and Symfony DomCrawler. Symfony BrowserKit adds browser-like sessions, Panther and php-webdriver drive real browsers, and Roach PHP and spatie/crawler handle crawling.
How do I scrape JavaScript-rendered pages with PHP?
First look for the JSON request the page makes in DevTools and call it with Guzzle. If the data exists only in the rendered page, use Symfony Panther, which drives Chrome or Firefox and lets you wait for an element with waitFor().
How do I build a scraper in Laravel?
Write an Artisan command that fetches pages with the Http client (with timeout() and retry()), parses them with Symfony DomCrawler and saves the results with Eloquent. Schedule it in routes/console.php, and dispatch queued jobs per URL for large runs.
Is Goutte still maintained?
No. Goutte is deprecated and its repository is archived; since version 4 it only wrapped Symfony BrowserKit's HttpBrowser. Replace Goutte\Client with Symfony\Component\BrowserKit\HttpBrowser and the rest of your code keeps working.
What is Roach PHP?
An open-source PHP scraping framework modeled on Scrapy: you write spiders that yield requests and items, and it runs them through middleware and item pipelines. It also has a Laravel adapter.
Why use Symfony Panther for web scraping?
Because it runs a real browser, so pages built by JavaScript render as they do for users, while you keep the familiar DomCrawler API. It is slower and heavier than Guzzle, so use it only for those pages.
How do I write a PHP script to crawl a website?
Keep a queue of URLs and an array of visited ones, fetch each page with Guzzle, collect its links with DomCrawler's filter('a')->links(), and queue the same-site ones you have not visited, with a delay and a page limit. spatie/crawler does this for you and respects robots.txt by default.
Show 1 more questionShow fewer questions
Does PHP support Selenium?
Yes, through php-webdriver, the community-maintained PHP client for Selenium and the W3C WebDriver protocol. Symfony Panther builds on it and adds a simpler API for scraping and testing.
Related pages
Easy Trial: 1 GB for $2.95.
Try for the price of 1 GB, no subscription, traffic never expires.