Open any PHP web scraping tutorial and you will see the same promise: “build a full crawler.” Then you scroll, and what you actually get is a loop that clicks a “next” button five times. That is not a crawler. That is pagination with extra steps.
A real crawler discovers its own pages. It keeps a queue of URLs it has not visited, a record of the ones it has, and it does not fall over when a request times out at 2am. None of the top-ranking guides build that. They stop at the easy part and call it done.
So this time we are going to actually finish the job. We will start from a single cURL request and end with a crawler that follows links across a whole site, runs requests in parallel, retries failures with backoff, and writes everything to SQLite.
A real PHP crawler needs more than a pagination loop: a link frontier, deduplication, concurrency, retries, and SQLite storage. This tutorial builds all of it in runnable PHP 8, crawling a 1,000-book catalog with up to a 5× concurrency speedup, then shows when a structured API like FlyByAPIs beats maintaining your own scraper.
1 request
Where we start
1,000
Pages crawled by the end
5×
Faster with concurrency
2 libs
Guzzle + Symfony DomCrawler
I run scraping infrastructure for a living. At FlyByAPIs we handle millions of search and product requests a month, and most of the support tickets we get start the same way: someone built a PHP scraper, it worked for a week, then it broke. Usually because they skipped exactly the parts this guide covers.
By the end you will have real, runnable PHP 8 code for every stage, plus an honest answer to the question nobody likes: when does it stop being worth maintaining your own scraper, and when should you just call an API?
Every snippet below is real and runnable. The complete, working code for this tutorial lives in the blog-web-scraping-code repo on GitHub
, in the php-web-scraping folder, so you are never copying a half-finished example.
Let me show you.
Is PHP good for web scraping, honestly?
Short answer: yes, for most of what people actually need.
PHP gets dunked on a lot, but it has three things going for it here. cURL is built into the language. There is a real DOM parser in the standard library. And Composer puts battle-tested HTTP clients one command away. You are not fighting the language to get started.
Bottom line:
If your data lives in server-rendered HTML, PHP scrapes it just fine. The cracks only show on heavy JavaScript pages and very high-volume crawls, and we will cover both honestly later on.
Here is the realistic boundary, because no tutorial should pretend a language is good at everything.
PHP handles this well
Server-rendered HTML, product listings, search results pages, articles, tables, anything you can see in "view source." This is the bulk of the web.
Where PHP struggles
Single-page apps that build content with JavaScript, sites with aggressive bot detection, and jobs that need thousands of requests per second. Solvable, but harder.
If you are still deciding which language to commit to, I wrote a longer breakdown comparing the best language for web scraping across Python, Node, Go, and Rust. For this guide, the assumption is you already know PHP and want to use it. Good. Let us set it up properly.
Setting up: PHP 8, Composer, and the modern stack
You need three things: PHP 8.1 or newer, the cURL extension (almost always already on), and Composer. Check your version first.
| |
Now create a project and pull in the two libraries that do the heavy lifting.
| |
That is the whole stack. Guzzle for HTTP, Symfony DomCrawler for reading the HTML, and CssSelector so you can use CSS selectors instead of raw XPath.
Skip Goutte. Yes, even though every other guide tells you to install it.
Goutte was archived years ago and folded into Symfony. If a 2026 tutorial still says composer require fabpot/goutte, it is copying old content. The maintained replacement is exactly what we just installed, plus symfony/http-client when you want Symfony's own client.
One more thing before we write code. Pick a practice target that wants to be scraped. We will use books.toscrape.com , a sandbox built specifically for this. It has 1,000 books across 50 pages, stable HTML, and nobody gets hurt. For the JavaScript section later, we will switch to its sibling, quotes.toscrape.com.
Never learn scraping on a site that matters. Break things on the sandbox first.
The libraries this PHP web scraping tutorial relies on
Before we write more code, a quick map of the territory. Most guides dump a list of twelve libraries on you and let you sort it out. You do not need twelve. You need to know which job each one does and which two you will actually reach for.
There are four categories, and a real scraper usually combines one from the first group with one from the second.
| Job | Library | Use it when |
|---|---|---|
| Fetch pages (HTTP) | Guzzle | Always. The default HTTP client. |
| Fetch pages (HTTP) | Symfony HttpClient | You are already in a Symfony app |
| Parse HTML | Symfony DomCrawler | Always. CSS selectors and clean traversal. |
| Parse HTML | DiDOM | You want a lighter, faster parser |
| Render JavaScript | Symfony Panther | The page builds content with JS |
| Full framework | Roach PHP | You want a Scrapy-style structure out of the box |
For this guide we pair Guzzle with DomCrawler, because that combination teaches you what is actually happening at each step. A framework like Roach PHP is excellent, but it hides the moving parts, and you learn more by assembling them yourself first.
Reach for these
Guzzle, Symfony DomCrawler, Symfony Panther. Three libraries cover the fetch, parse, and render jobs for almost everything you will build.
Skip in 2026
Goutte (archived into Symfony), the abandoned Simple HTML DOM Parser, and raw regex parsing. All three show up in old tutorials and all three will let you down.
One name you will see in every older guide is Simple HTML DOM Parser. It was useful a decade ago. It is slow, unmaintained, and chokes on malformed markup that DomCrawler handles fine. Leave it in the past.
Your first request: cURL, then why you leave it behind
Every scrape is the same two moves: fetch a page, then pull data out of it. Let us do the fetch the rawest way possible, with cURL, so you understand what is actually happening underneath every library.
| |
Run it. You get a 200 status and a few thousand bytes of HTML. That is web scraping at its most basic. No magic.
But look at how much ceremony that is for one request. Now imagine adding retries, headers, cookies, and parallel requests on top of raw cURL. You would be rebuilding a library by hand.
So we do not. We switch to Guzzle, which wraps cURL with a clean API and gives us everything we will need later for free.
| |
Same result, a third of the noise. From here on, Guzzle is our HTTP layer.
Why learn cURL first if we abandon it?
Because when Guzzle throws a connection error or a TLS warning at 3am, you need to know it is cURL underneath and what those errors mean. Abstractions leak. Knowing the layer below saves you hours.
Parsing HTML: regex is a trap, use the DOM
You have the HTML. Now you need the book titles and prices out of it. Here is where a lot of beginners take a wrong turn.
The wrong turn is regex. You see <h3> tags, you think “I’ll just match those with a pattern.” It works on the first page. Then the site nests a tag differently, or adds a class, and your pattern silently returns garbage. HTML is not a regular language. Parsing it with regex is a classic mistake, and there is a famous Stack Overflow answer
about why.
Use the DOM instead. Symfony’s DomCrawler loads the HTML into a real tree and lets you query it with CSS selectors.
| |
That gives you a clean array of 20 books from one page, each with a title, price, and stock status. No fragile pattern matching. If the site adds a class to .product_pod, your selector still works.
Regex breaks on structure changes
A pattern matches text, not meaning. One nested tag and it silently fails. You will not even get an error, just wrong data.
CSS selectors map to how you think
You already read .price_color the same way the browser does. The selector survives layout tweaks that would shatter a regex.
If you prefer XPath, DomCrawler does that too. $crawler->filterXPath('//article[@class="product_pod"]') gets you the same nodes. CSS is friendlier for most jobs, so we will stick with it.
We now have a scraper: it fetches one page and extracts data. This is exactly where most guides stop. We are barely getting started.
From one page to many: building a real link frontier
Here is the difference between a scraper and a crawler, and it is the whole point of this guide.
A scraper reads a page you hand it. A crawler finds its own pages. It needs three things working together: a queue of URLs to visit (the frontier), a seen set of URLs so it never visits the same page twice, and a depth limit so it does not crawl the entire internet by accident.
The three parts of a crawler
Frontier
A queue of URLs to visit
Start with one seed, add links as you find them
Seen set
URLs already visited
Stops loops and wasted requests
Depth limit
How far to follow links
Keeps the crawl bounded and predictable
Let us build it. DomCrawler has a lovely feature here: if you give it the page’s URL, ->links() returns Link objects that resolve relative URLs to absolute ones for you. No manual URL joining, which is the part everyone gets wrong.
| |
Read that loop carefully, because it is the spine of every crawler ever written. Pull a URL off the queue. Skip it if seen. Fetch it. Extract data. Find links. Queue the new ones. Repeat until the queue is empty.
Point it at books.toscrape.com and it will walk all 50 catalogue pages and every product page, deduplicating as it goes, until there is nothing left to visit. That is a crawler. A real one.
Add a depth limit before you point this at the real web.
Store ['url' => $url, 'depth' => 0] in the queue instead of a bare string, increment depth for discovered links, and skip anything past your max. Without it, a crawl on a large site never ends.
There is one problem, though. This crawler fetches pages one at a time. Page two does not start until page one finishes. For 50 pages that is fine. For 50,000, you will be waiting all afternoon. Time to fix that.
Crawling faster: concurrent requests with Guzzle
Scraping is mostly waiting. Your code fires a request, then sits there doing nothing while bytes cross the internet. If you can keep ten requests in flight at once, the whole job finishes roughly ten times faster.
Guzzle has this built in with Pool, which runs a stream of requests at a fixed concurrency. You give it a generator of requests and callbacks for success and failure.
| |
Set concurrency to 5 and you have five requests running at once instead of one. On a 50-page crawl that is the difference between a coffee break and a blink.
1
Sequential: one at a time
5
Pool: five in flight
up to 5×
Typical speedup
Now, the important part: do not crank concurrency to 100 because you can. That is how you get rate-limited or banned, and it is rude to the site you are scraping. Five to ten is polite and plenty for most jobs. The number should reflect what the target can handle, not what your laptop can fire off. Inside the pool, that concurrency cap is your throttle. The per-request usleep delay from earlier only applies to the sequential loop, so keep the pool small instead.
This is also where languages start to differ. If you genuinely need thousands of concurrent requests, a Go web scraper built with Colly will out-scale a PHP one, because goroutines make that kind of concurrency cheap. For most jobs, Guzzle’s pool is more than enough.
Going two levels deep: listing pages and detail pages
Real crawls are rarely flat. The listing page gives you a title and a price, but the good stuff (the full description, the UPC, the review count) lives on each item’s own detail page. So you crawl in two passes: collect links from the listing pages, then visit every detail page to extract the full record.
This is exactly where concurrency pays off, because you usually have hundreds of detail pages to fetch. First, gather the detail-page URLs from a listing.
| |
Now run those detail pages through the Guzzle pool from earlier and pull the richer fields each one exposes.
| |
That two-level pattern (listing to detail) is how most serious crawls are shaped, and almost no beginner tutorial shows it. They scrape the listing, get a price, and stop. The detail pages are where the data you actually wanted has been hiding the whole time.
Listing first, detail second, both deduplicated.
Feed the detail URLs through your seen-set too. On a big site the same product links from many listing pages, and without dedup you would fetch each detail page a dozen times.
Making it survive: retries and exponential backoff
Here is a truth nobody puts in the intro: on a long crawl, requests fail. Connections drop. Servers return a 503 because they are briefly overloaded. The network hiccups. If one failure kills your whole crawl after two hours, you will lose your mind.
The fix is retries with backoff. Try again, but wait longer each time so you are not hammering a struggling server. Guzzle handles this with a middleware, so you set it up once and every request inherits it.
| |
That waits one second after the first failure, two after the second, four after the third, then gives up. Exponential backoff. It is the single biggest reliability win you can add to a crawler, and almost no tutorial bothers to include it.
Retry only what is worth retrying
A 404 will never become a 200, so do not retry it. A 503 or a dropped connection often clears on the next try.
Back off, do not retry-storm
Hammering a server that just failed makes things worse. Doubling the wait each time gives it room to recover.
Wrap your extraction in a try/catch too, so one malformed page does not crash the run. Log the URL, skip it, keep going. A crawler that finishes with 998 of 1,000 pages beats one that dies at page 3.
Storing what you scrape: SQLite, not a pile of CSVs
Every other guide ends with fputcsv() and calls it storage. CSV is fine as a final export. It is a terrible working store for a crawler.
Why? Because a crawler that runs for hours needs to deduplicate, resume after a crash, and update records when it re-crawls. CSV does none of that. SQLite does all of it, it is built into PHP through PDO, and it is just a single file. No server to run.
| |
The url primary key plus INSERT OR REPLACE gives you deduplication for free. Scrape the same book twice and you get one up-to-date row, not two. Crash halfway and your data is already on disk, so you can resume from where you stopped.
CSV as a working store
No dedup, no resume, no updates. One crash and you start over. Fine for a final export, painful as a crawler's memory.
SQLite as a working store
Primary-key dedup, survives crashes, updates on re-crawl, and you can query it with SQL. One file, zero setup.
When the crawl is done and you want a spreadsheet, export from SQLite to CSV in one query. If your end goal is Excel specifically, I walk through that path in scraping website data to Excel . The point stands: crawl into a database, export at the end.
Cleaning the data before you store it
Raw scraped data is messy. Prices arrive as "£51.77", titles come padded with newlines and tabs, and half the time a field you expected is just missing. Store that as-is and every query downstream becomes a fight. Clean it at the point of extraction instead.
| |
Three small habits save you hours later. Cast numbers to actual numbers so you can do math on them. Normalize whitespace so "Book Title\n" and "Book Title" are not treated as different values. And give missing fields a defined default instead of letting a stray null blow up a report three steps downstream.
Clean on the way in, not on the way out.
If you store the mess and plan to "fix it later," later never comes. Normalize each field the moment you extract it, and your database stays queryable from day one.
Running it on a schedule: cron and incremental crawls
A scraper you run by hand is a script. A scraper that runs itself on a schedule is a data pipeline. The jump between them is small, and it is where a crawler starts earning its keep.
On any Unix box, cron handles the scheduling. One line runs your crawler every morning at 6am.
| |
The smarter move is an incremental crawl. On the first run you scrape everything. On every run after, you only fetch pages you have not seen recently, using the scraped_at column you already store. No point re-downloading 1,000 pages to find the 12 that changed.
| |
That one check turns a brute-force re-crawl into a polite, fast update. Your crawler does less work, the target site sees fewer requests, and your data stays fresh. Everyone wins, which is rare.
Full re-crawl every run
1,000 requests to update 12 pages. Slow, wasteful, and far more likely to get you rate-limited or noticed.
Incremental crawl
Check timestamps, fetch only what is stale. A handful of requests, fresh data, almost invisible to the target.
Logging in: cookies, sessions, and forms
Plenty of the data worth scraping sits behind a login. A dashboard, a members-only listing, a price you only see when signed in. To reach it, your crawler has to log in like a browser does: post the form, receive a session cookie, then send that cookie on every request after.
Guzzle makes this painless with a cookie jar. Turn it on and Guzzle remembers cookies across requests automatically, the same way your browser does.
| |
Three steps, and they map to what a human does. Load the form, read the hidden CSRF token, submit your credentials. After that the jar carries the session cookie on every call, so the rest of your crawler does not even know it is authenticated.
Watch the CSRF token. It is the part people forget.
Most modern forms reject a POST that does not include a fresh hidden token from the login page. If your login silently fails, this is almost always why. Read it from the form, do not hardcode it.
A word of caution, because this is where scraping gets legally and ethically grey. Scraping data behind a login you control is one thing. Scraping behind someone else’s login, in violation of their terms, is another. Stay on the right side of that line.
Scraping responsibly: robots.txt and rate limits
Before you crawl anything you do not own, check its robots.txt. It is the site telling you, in plain text, which paths it does and does not want bots touching. Ignoring it is both rude and, in some places, legally risky.
You do not need a library for the basics. PHP can read and respect it in a few lines.
| |
That is a deliberately simple parser. For production crawls across many sites, reach for a proper one that understands user-agent groups and Allow rules, but the principle holds: ask before you take.
Identify yourself
Use a real User-Agent with a contact URL. A site owner who can reach you is far less likely to block you outright.
Crawl at a human pace
A delay between requests and modest concurrency keeps you off the radar and keeps the site healthy for everyone else.
Politeness is also pure self-interest. A scraper that hammers a site gets noticed and blocked fast. A scraper that behaves like a considerate visitor often runs for months without anyone caring. Restraint is a feature.
Catching breakage before your users do: selector drift
Here is the failure mode that bites everyone who runs a scraper long enough. It works perfectly for weeks. Then one morning the data is empty, or wrong, and you have no idea since when. The site quietly changed its HTML and your selectors stopped matching. We call it selector drift, and it is the number one reason scrapers rot.
The fix is not cleverness. It is validation. After every extraction, assert that you got something sane. If you did not, fail loudly instead of writing garbage to your database.
| |
Two layers of defense. Per-record validation catches a broken selector on the spot. A count check at the end catches the subtler case where everything “works” but you scraped half the data you should have.
Without validation
The site changes, your scraper writes empty rows for a week, and you find out when someone downstream asks why the report is blank.
With validation
The first broken page throws, you get an alert the same day, and you fix one selector instead of cleaning up a week of bad data.
This is also one of the quiet advantages of using an API for the hard targets. When Google or Amazon reshuffles their HTML, that is our problem to fix on our end, not a 6am alert in your inbox. At FlyByAPIs we run a whole monitoring process for exactly this kind of drift, so the JSON you get back stays the same shape even when the underlying page does not.
That is the full local crawler. Frontier, dedup, concurrency, retries, storage, logins, politeness, and breakage detection. Now we hit the two walls that send most people looking for help.
When the page is empty: scraping JavaScript with Panther
You point your shiny new crawler at a modern site and get back… nothing. Empty divs. No data. What happened?
The page renders its content with JavaScript after it loads. Guzzle, like any plain HTTP client, only sees the raw HTML the server sends. It does not run JavaScript, so all that client-rendered content is invisible to it. This is the single most common “my scraper sees no data” bug.
The fix in PHP is Symfony Panther. It drives a real Chrome instance, waits for the page to render, then hands you the same DomCrawler API you already know.
| |
| |
The key line is waitFor('.quote'). Panther pauses until that selector appears, which is how you handle content that loads a beat after the page does. Without it, you would read the page too early and get nothing, same as Guzzle.
Panther is powerful and slow. Use it only when you must.
Running a real browser per page is heavy on CPU and memory. Always try Guzzle first. If the data is in the raw HTML, you do not need a browser. Reserve Panther for pages that genuinely render with JavaScript.
A quick way to check before you reach for Panther: open the page, view source (not “inspect,” actual view-source), and search for the data. If it is there, Guzzle can get it. If it is not, the page builds it with JavaScript and you need Panther or an API.
Common PHP scraping errors, and how to fix them
You will hit these. Everyone does. Knowing the cause turns an hour of confusion into a one-line fix, so here are the ones that come up again and again.
| Symptom | Likely cause | Fix |
|---|---|---|
| Empty results, page loads fine in a browser | Content rendered by JavaScript | Use Panther, or an API that renders for you |
| 403 or 429 status codes | Bot detection or rate limiting | Real User-Agent, slow down, rotate IPs |
| Garbled accents and symbols | Character encoding mismatch | Convert to UTF-8 before parsing |
| Allowed memory size exhausted | Holding the whole crawl in an array | Write to SQLite as you go, not at the end |
| cURL error 60: SSL certificate problem | Missing or stale CA bundle | Update your CA certs, never disable verification |
The encoding one trips up everyone scraping non-English sites. If a page declares a non-UTF-8 charset, your accents turn into question marks. Fix it before you parse, not after.
| |
The memory error is the other classic, and it shows up exactly when your crawler gets serious. If you append every result to a PHP array, a big crawl will eventually blow past the memory limit and die. The cure is the SQLite store from earlier: write each record to disk the moment you have it, and keep almost nothing in memory.
Never solve cURL error 60 by disabling SSL verification.
Turning off verify "fixes" the error and quietly opens you to man-in-the-middle attacks. Update your CA bundle instead, or point cURL at a fresh cacert.pem. The error is real and worth respecting.
Most of the time, though, the error you are fighting is not in your code at all. It is the site deciding it does not want you there. Which brings us to the wall.
Why you keep getting blocked, and the honest fix
So far we have practiced on sites that want to be scraped. The real web is different. The moment you point a crawler at a site that does not want you there, you meet its defenses.
Rate limits and IP bans
Too many requests from one IP and the site blocks it. Slow down, cap concurrency, and you delay this. You do not avoid it forever.
Bot fingerprinting
Sites read your headers, TLS signature, and behavior. A default Guzzle User-Agent screams "bot." Real browser headers help, until they update the detection.
CAPTCHAs and JavaScript challenges
Cloudflare and friends throw interactive challenges your crawler cannot solve. This is usually where DIY scraping stops being fun.
The basic hygiene buys you a lot, and you should always do it: set a realistic User-Agent, add a small random delay between requests, respect robots.txt, and keep concurrency modest. Here is the polite-delay pattern, by the way.
| |
When one IP is not enough, the next step is proxies. The idea is simple: route requests through different IP addresses so no single one trips a rate limit. Guzzle takes a proxy per request, so rotating through a pool is a few lines.
| |
That works, on paper. In practice, good residential proxies cost real money, free ones are slow and often poisoned, and you now own a second system to monitor: which proxies are alive, which are banned, which are leaking your real IP. Rotating proxies is a project of its own.
But let us be honest about the ceiling. For small, friendly targets, the hygiene above is enough. For big sites that actively fight scrapers, especially the search engines and large marketplaces, you end up in an arms race: rotating proxies, header rotation, CAPTCHA solving, browser fingerprint spoofing. You can build all of that. The question is whether you should.
This is the wall everyone hits with a homemade crawler. And it is exactly where a different approach starts to make sense.
When to stop building and call an API instead
I am not going to pretend a DIY crawler is always the answer. I run an API company, sure, but I also maintained scrapers for years before that, and I know which jobs are worth the maintenance and which ones quietly eat your weeks.
Here is the honest split.
Build it yourself when
The target is small and friendly, the data is in plain HTML, the job is a one-off, or scraping is the thing you are learning. The crawler in this guide handles all of these well.
Use an API when
You are scraping Google, Amazon, or another heavily defended site, you need reliable data on a schedule, or you would rather ship a feature than maintain proxy rotation forever.
Take search results as the obvious case. The day your PHP crawler points at Google, you walk straight into CAPTCHAs, layout changes every few weeks, and IP bans. You can fight that, or you can call a Google Search API built for PHP developers that returns clean JSON and absorbs the blocking on its end.
Here is the same crawl-the-search-results job, but through our SERP API instead of a homemade Google scraper . Notice it is the same Guzzle you already learned.
| |
No HTML parsing. No proxy pool. No CAPTCHA solving. One request, structured results, position tracking included. The FlyByAPIs PHP-friendly SERP API handles the part that would otherwise consume your maintenance budget.
200 requests/month free · No credit card required
The same logic applies to other heavily defended targets. Scraping product pages is its own arms race, which is why a structured way to scrape Amazon data without getting blocked tends to beat a homemade product crawler once you care about reliability. Our Amazon scraping API returns prices, ratings, and reviews as JSON across 22 marketplaces, with the IP and anti-bot work handled for you.
And it goes well beyond search and shopping. Whatever data you are after, the pattern repeats: the harder the site fights, the more an API earns its keep.
| If you need to scrape... | DIY difficulty | The structured API |
|---|---|---|
| Google search results | Hard — CAPTCHAs, bans | Google Search API |
| Amazon products | Hard — aggressive blocking | Amazon scraping API |
| Local business listings | Medium — rate limits | Google Maps scraper |
| Company and funding data | Medium — login walls | Crunchbase scraper API |
| Job listings | Medium — JS-heavy boards | Jobs Search API |
| Translating scraped text | N/A — different job | Translation API |
Every one of those is the same one-request, JSON-back pattern you saw above. You keep the crawler skills from this guide for the friendly targets, and you hand the hostile ones to an API that already solved blocking. That is not giving up. That is spending your time on the part that is actually your product.
Putting the full crawler together
Let us recap what you built, because it is genuinely more than any of the top-ranking guides give you.
Fetch
cURL → Guzzle
Parse
DomCrawler, not regex
Crawl
Frontier + dedup
Survive
Concurrency, retries, SQLite
A fetch layer that started with raw cURL and graduated to Guzzle. A parser that reads the DOM with CSS selectors instead of fragile patterns. A frontier-based crawler that discovers its own pages and never visits one twice. Concurrency through Guzzle’s pool, retries with exponential backoff, and SQLite storage that survives a crash. Plus Panther for the JavaScript pages and a clear-eyed view of when to stop fighting and call an API.
Here is the local crawler pulled into one small class, so the pieces sit together instead of scattered across snippets. This is roughly what a clean version looks like before you start adding the production extras.
| |
That is a real crawler. Not a pagination loop wearing a costume. The full runnable version, including the concurrency pool and validation, lives in the blog-web-scraping-code repo so you are never copying half-finished snippets.
If you are weighing PHP against other languages for your next scraping project, the same crawler concepts carry over. I built the equivalent in Python and Node.js if you want to compare the ergonomics, and I benchmarked the managed options in the best web scraping APIs for when DIY stops making sense.
200 requests/month free · No credit card required
The crawler you built here will serve you well on every site that does not actively fight back. For the ones that do, you now know the line between a fun engineering challenge and a maintenance sinkhole. Knowing where that line sits is most of the skill.
The real skill is knowing where the line sits.
Build the crawler for the friendly targets, hand the hostile ones to an API that already solved blocking, and spend your time on the part that is actually your product.
Build the crawler. Learn how the web actually works under the hood. And when you find yourself writing your third proxy-rotation hack to keep up with Google, remember there is a Google search data API for PHP that already lost that fight so you do not have to.
All the code from this tutorial, runnable end to end, is in the blog-web-scraping-code repo
under php-web-scraping. Clone it, run composer install, and point it at the sandbox site to watch the whole crawler work.
What are you crawling? If you build something with this, I would genuinely like to hear about it.
Oriol.
