Quick answer: For PHP web scraping the built-in DOMDocument plus DOMXPath handles HTML parsing with no dependencies, and Simple HTML DOM is a friendlier third-party option if you prefer jQuery-style selectors. Both work for static pages fetched with cURL or file streams. When a page renders with JavaScript or blocks your IP, install the linksc PHP package (composer require getlinksc/linksc) and call a fetch API that renders and proxies server-side. Both paths are shown with runnable code.
PHP is still everywhere data lives: WordPress sites, Laravel apps, legacy dashboards. And it has always shipped with a competent HTML parser in DOMDocument, so you can scrape without pulling in a framework. The limits are the same ones every language runs into, namely JavaScript rendering and anti-bot blocking.
Let me show the from-scratch approach, then the package, and be straight about which fits where.
The from-scratch approach: DOMDocument + DOMXPath
DOMDocument is part of PHP's standard dom extension, which is enabled in almost every install. Nothing to install. Fetch the page, load it, query it.
<?php
$url = "https://example.com";
// Fetch with a real User-Agent and a timeout via stream context.
$context = stream_context_create([
"http" => [
"method" => "GET",
"header" => "User-Agent: Mozilla/5.0 (compatible; MyScraper/1.0)\r\n",
"timeout" => 15,
],
]);
$html = @file_get_contents($url, false, $context);
if ($html === false) {
exit("Failed to fetch {$url}\n");
}
// DOMDocument is noisy about imperfect HTML; silence the warnings.
$doc = new DOMDocument();
libxml_use_internal_errors(true);
$doc->loadHTML($html);
libxml_clear_errors();
$xpath = new DOMXPath($doc);
// Pull every link on the page.
foreach ($xpath->query("//a[@href]") as $link) {
$text = trim($link->textContent);
$href = $link->getAttribute("href");
echo "{$text} -> {$href}\n";
}
Two details save you pain. First, real-world HTML is messy and loadHTML emits a flood of warnings, so wrap it with libxml_use_internal_errors(true) to keep your output clean. Second, always set a User-Agent and a timeout in the stream context, because the defaults get filtered and a slow server can hang the request.
DOMXPath is the powerful part. XPath expresses structure that CSS cannot:
// Text of every h2 with a given class.
foreach ($xpath->query('//h2[@class="title"]') as $node) {
echo trim($node->textContent) . "\n";
}
// Nested extraction: heading and link inside each article.
foreach ($xpath->query("//article") as $article) {
$heading = $xpath->query(".//h3", $article)->item(0);
$anchor = $xpath->query(".//a[@href]", $article)->item(0);
if ($heading && $anchor) {
echo trim($heading->textContent) . ": "
. $anchor->getAttribute("href") . "\n";
}
}
Note the leading dot in .//h3, which scopes the query to the current node instead of the whole document. Forgetting it is the most common XPath bug in PHP scraping.
Simple HTML DOM if you prefer selectors
If XPath is not your thing, the Simple HTML DOM library gives you a jQuery-like find() API. Install it with Composer:
composer require simplehtmldom/simplehtmldom
<?php
require "vendor/autoload.php";
use simplehtmldom\HtmlWeb;
$client = new HtmlWeb();
$doc = $client->load("https://example.com");
foreach ($doc->find("a") as $link) {
echo $link->plaintext . " -> " . $link->href . "\n";
}
It is more approachable, at the cost of being slower and less strict than DOMDocument on large or malformed pages. For small jobs the ergonomics are worth it.
Where the from-scratch approach breaks
Two walls. The first is JavaScript. file_get_contents and cURL fetch the HTML the server sent and stop there. A single-page app builds its content in the browser, so you receive an empty shell. PHP's usual answer is driving a headless Chrome through symfony/panther or chrome-php/chrome, which means running and managing a browser.
The second is blocking. Datacenter IPs get flagged and you hit CAPTCHAs or 403s no header change fixes, which pushes you toward rotating residential proxies and more infrastructure. The self-hosted web scraping guide walks through what running that stack actually involves.
When you are maintaining a browser pool and proxy rotation, you have built a scraping platform. That is the point where an API earns its keep.
The low-effort path: the linksc PHP package
When you would rather not run browsers and proxies, a fetch API handles rendering, proxying, and HTML-to-markdown conversion server-side and hands back clean content. The linksc PHP package is a synchronous client that uses PHP streams under the hood and drops into any script, Laravel job, or Symfony command.
composer require getlinksc/linksc
<?php
require "vendor/autoload.php";
use Linksc\Client;
// Falls back to the LINKSC_API_KEY env var if you pass null.
$client = new Client("lsc_your_api_key");
// Fetch any URL as clean markdown. JavaScript rendered server-side.
$content = $client->fetch("https://example.com", ["format" => "markdown"]);
echo $content;
Search returns full page content, not just snippets, which is what an LLM pipeline needs:
$serp = $client->search("php web scraping", ["engine" => "google"]);
print_r($serp);
Prefer no package? The API is plain HTTP over cURL. The auth header is x-api-key, never Authorization: Bearer:
<?php
$ch = curl_init("https://api.link.sc/v1/fetch");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"x-api-key: lsc_your_api_key",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"url" => "https://example.com",
"format" => "markdown",
]),
]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
echo $data["content"];
To convert what you scrape into LLM-ready text, HTML to markdown for LLMs explains why markdown beats raw HTML for context windows.
Which should you use?
| Factor | DOMDocument / Simple HTML DOM | linksc PHP package |
|---|---|---|
| Static HTML pages | Ideal, zero dependencies | Works, mild overkill |
| JavaScript-rendered pages | Needs Panther or headless Chrome | Handled server-side |
| Blocked / anti-bot sites | You run proxies | Handled for you |
| Output format | Raw HTML, you parse | Clean markdown or JSON |
| Ongoing maintenance | All yours | Mostly the API's problem |
| Cost | Compute only | Credit-based, free tier to start |
My honest take: for static, cooperative sites, DOMDocument is right there in core PHP and adding an external service would be overkill. Use it. When you are wrestling with JavaScript rendering or blocks across many sites, the maintenance cost outweighs API credits, and the package lets you drop a pile of fragile browser and proxy code.
Start on the free tier of 500 credits per month and see the numbers at link.sc/pricing.
Need clean web data in your PHP or Laravel app without running a browser? Get a free link.sc API key and call fetch or search in a few lines.