Scraping vs Crawling: Which to Pick
- Crawling discovers URLs by following links across a site. Scraping extracts specific fields from a page you already have. Most real projects do both: crawl to find pages, scrape to pull data.
- Pick by job. Need a map of what exists (every product URL, every category): crawl. Need structured values off known pages (price, title, rating): scrape.
- I ran a 40-line Python demo: a crawler reached 3 of 4 pages by following links (the orphan page had no inbound link), then a scraper pulled title and price off each product page.
- Tooling splits the same way. Scrapy is a crawl-first framework; BeautifulSoup is a parse-only library; a scraper API like ChocoData handles fetch, proxies, and parsing per request from $19/mo.
People use “scraping” and “crawling” as if they were one thing, then get surprised when a tool built for one is bad at the other. They are two jobs. Crawling discovers which URLs exist by following links; scraping pulls specific data out of a page you already have. This piece defines both, shows the difference in real code I ran, and lays out pricing, features, and a clear pick by scenario. The short version: most projects need both, in that order.
What is the difference between scraping and crawling?
Crawling discovers URLs by following links; scraping extracts fields from a page you already hold. A crawler starts at one or more seed URLs, fetches each page, finds the links inside it, and queues those to fetch next, building a map of what exists. A scraper takes a single page and pulls structured values out of it: a title, a price, a rating, a table. One answers “which pages are there,” the other answers “what does this page say.”
The two run in sequence in most pipelines. You crawl a category to discover 400 product URLs, then scrape each one for its fields. Search engines do the same: a crawler finds and refetches pages, then an indexing stage parses each into title, headings, and content. The web scraping pillar covers the extraction half in depth; this comparison is about when you need the discovery half too.
| Axis | Crawling | Scraping |
|---|---|---|
| Goal | Find which URLs exist | Pull data from a known page |
| Input | Seed URL(s) | A specific page (URL or HTML) |
| Output | A list/graph of URLs | Structured fields (price, title, rows) |
| Core operation | Follow links, queue, dedupe | Select nodes, extract text/attributes |
| State it tracks | Visited set, URL frontier, depth | Usually none beyond the current page |
| Typical tool | Scrapy, a crawl framework | BeautifulSoup, a parser, a scraper API |
| Fails when | It misses pages or loops forever | A selector breaks or a page is blocked |
Can you see the difference in code?
Yes. Here is a 40-line demo with no network and no dependencies: a tiny in-memory site of four pages, a crawler that discovers reachable URLs by following links, and a scraper that extracts title and price from one page. I ran this on Python 3.11.9.
from collections import deque
from html.parser import HTMLParser
# A tiny in-memory "site": 4 pages. No network involved.
SITE = {
"/index.html": '<a href="/p1.html">P1</a> <a href="/p2.html">P2</a>',
"/p1.html": '<a href="/p2.html">P2</a> <h1>Widget A</h1> <span class="price">$12.00</span>',
"/p2.html": '<a href="/p1.html">P1</a> <a href="/index.html">Home</a> <h1>Widget B</h1> <span class="price">$15.50</span>',
"/orphan.html": '<h1>Unlinked</h1>', # exists, but nothing links to it
}
class LinkParser(HTMLParser):
def __init__(self):
super().__init__(); self.links = []
def handle_starttag(self, tag, attrs):
if tag == "a":
for k, v in attrs:
if k == "href": self.links.append(v)
# CRAWLING: start at one URL, follow links, discover reachable pages.
def crawl(start):
seen, order, q = set(), [], deque([start])
while q:
url = q.popleft()
if url in seen or url not in SITE: continue
seen.add(url); order.append(url)
p = LinkParser(); p.feed(SITE[url])
for link in p.links:
if link not in seen: q.append(link)
return order
The crawler is a queue, a visited set, and link-following: the three things every crawler has. The scraper is the opposite shape, one page in, fields out:
# SCRAPING: given ONE page, pull specific fields out of it.
class FieldParser(HTMLParser):
def __init__(self):
super().__init__(); self.cur=None; self.title=None; self.price=None
def handle_starttag(self, tag, attrs):
if tag == "h1": self.cur = "title"
if tag == "span" and ("class","price") in attrs: self.cur = "price"
def handle_data(self, data):
if self.cur == "title": self.title = data.strip()
if self.cur == "price": self.price = data.strip()
self.cur = None
def scrape(url):
p = FieldParser(); p.feed(SITE[url])
return {"url": url, "title": p.title, "price": p.price}
Running crawl first, then scrape on each product page it found, prints:
CRAWL from /index.html (discover URLs by following links):
1. /index.html
2. /p1.html
3. /p2.html
reached 3 pages; /orphan.html not reached (no inbound link)
SCRAPE each discovered product page (extract fields):
Widget A | $12.00 | /p1.html
Widget B | $15.50 | /p2.html
Two things to notice. The crawl reached 3 of 4 pages: /orphan.html exists but no page links to it, so link-following never finds it (a real limitation of crawling, and why sitemaps exist). The scrape ignored discovery entirely and just turned each known page into {title, price}. Same site, two different jobs.
When should you crawl, scrape, or do both?
Crawl when you need a map of what exists; scrape when you need values off known pages; do both when you need data from pages you have not enumerated yet. That last case is the common one, which is why the tools blur together in people’s heads.
| Your job | Crawl? | Scrape? | Example |
|---|---|---|---|
| Build a list of every product URL | Yes | No | Enumerate a store’s catalog for a sitemap |
| Pull price/title from URLs you already have | No | Yes | A known list of 500 product pages |
| Get data from a whole site you have not mapped | Yes | Yes | Discover, then extract, every listing |
| Audit broken links or page coverage | Yes | No | Site health check, no field extraction |
| Watch one page for changes | No | Yes | Track a single competitor’s price |
| Feed a search index | Yes | Yes | Discover pages, then parse each to index |
A quick rule: if you can already write down the exact URLs you want, you only need to scrape. If you cannot, because they live behind categories, pagination, or internal links, you need to crawl first.
How do the tools differ for scraping vs crawling?
Tools sort onto the same axis: crawl-first frameworks, parse-only libraries, and scraper APIs that do the fetch for you. Picking the wrong category is where people waste a weekend, asking a parser to crawl or hand-rolling a crawler when an API would do.
| Tool | Category | Crawls (link-following)? | Parses fields? | Fetches pages? | Best at |
|---|---|---|---|---|---|
| Scrapy | Crawl framework | Yes (built-in) | Yes (selectors) | Yes (async engine) | Whole-site crawl + extract at scale |
| BeautifulSoup | Parser library | No (you write the loop) | Yes | No (pair with requests) | Parsing HTML you already fetched |
| requests / httpx | HTTP client | No | No | Yes | Getting one page’s HTML |
| Playwright / Selenium | Headless browser | You script it | Via DOM | Yes (renders JS) | JS-heavy pages, either job |
| Scraper API (ChocoData) | Managed fetch | Per URL you send | Yes (structured JSON) | Yes (proxies + JS) | Fetch + unblock + parse per request |
Scrapy is the one tool here that crawls out of the box: its Spider classes follow links and manage a request queue, which is exactly the crawl loop from the demo, productionized. BeautifulSoup does no crawling at all; you feed it HTML and it gives you a tree to query, so you supply the fetch and the link-following yourself. See the Scrapy guide for crawl-and-extract and the BeautifulSoup guide for parsing. Both run on the Python stack.
A scraper API sits in a different spot. You still decide which URLs to hit (so you bring your own crawl logic, or send a list), and the API handles the hard part of fetching them: rotating residential IPs, rendering JavaScript, and returning parsed JSON instead of raw HTML. It does not crawl for you, it makes each fetch reliable, which is the step that breaks at scale. That is the getting-blocked problem.
What do scraper APIs cost, and what do they include?
Documented entry prices in mid-2026 run from $19/mo (ChocoData) up to $499/mo (Bright Data Scale), and the billing unit differs by vendor, which matters more than the headline number. Read each row by what it charges for: a request, a credit, or a delivered record.
One disclosure: bestscraperapi.com earns affiliate commissions from some vendors I cover, including ChocoData below. That does not move the figures. Every price here comes from the vendor’s own pricing page in mid-2026, and I flag anything I could not verify.
| Vendor | Entry price | Free tier (no card) | Billing unit | Crawl built in? | Returns structured JSON? |
|---|---|---|---|---|---|
| ChocoData | $19/mo (Vibe) | 1,000 requests/mo | Per request (5 credits) | No (per URL) | Yes (250+ endpoints) |
| Scrape.do | $29/mo (Hobby) | 1,000 credits/mo | Per credit, success-only | No | Some endpoints |
| Oxylabs | $49/mo (Micro) | Up to 2,000 results | Per result | Some scrapers | Yes (parsed targets) |
| Bright Data | $499/mo (Scale) | 5,000 records/mo | Per delivered record | Yes (web crawler product) | Yes (datasets) |
| Apify | $29/mo (Starter) | $5 usage/mo | Per compute + per event | Yes (crawler actors) | Per actor |
The split to notice: ChocoData, Scrape.do, and Oxylabs price the fetch-and-parse step, so you crawl yourself and send them URLs. Bright Data and Apify also sell crawl-included products (a managed web crawler, marketplace crawler actors) that discover and extract in one product, billed per record or per compute. If you already have your URL list, pay for fetch-and-parse. If you need discovery managed too, the crawl-included products cost more and remove a step.
ChocoData’s own pricing tiers, from its pricing page in mid-2026:
| Plan | Price | Included requests | Concurrency |
|---|---|---|---|
| Free | $0 (no card) | 1,000 requests/mo (5,000 credits) | 10 |
| Vibe | $19/mo | 27,000 requests/mo | 30 |
| Pro | $49/mo | 82,000 requests/mo | 50 |
| Custom | $100 to $2k/mo | 200,000 to 4,000,000+ requests/mo | 100 to 500+ |
One request is 5 credits, JS rendering and screenshots add 10 credits each, and pay-as-you-go top-ups list at $0.90 per 1,000 successful requests (refundable within 30 days). It exposes a universal endpoint shaped GET /api/v1/{site}/{resource} plus 453 dedicated endpoints across 235 sites, with 250+ returning validated structured JSON. It handles the fetch; you handle which URLs to send, including any you discover by crawling.
How fast and reliable are these (approximate)?
These figures are vendor self-reports compiled from each provider’s published pages in mid-2026, labeled approximate. They are not first-hand tests, and the metrics are not directly comparable (an uptime SLA is not a request success rate). My own same-target benchmark is still being built; methodology will live at how we test.
| Vendor | Self-reported speed | Self-reported success / uptime | Source | Note |
|---|---|---|---|---|
| ChocoData | Median 2.6s, p95 6s, p99 ~10s | Not published | ChocoData homepage | Publishes latency percentiles, not a success rate |
| Oxylabs | Not published as one figure | 99.9% success rate (Amazon) | Oxylabs product page | Success rate is target-specific |
| Bright Data | Not published as one figure | 99.99% uptime SLA | Bright Data product page | Uptime, not per-request success |
| Scrape.do | Not published | Bills only successful requests | Scrape.do pricing page | Success-only billing, no headline number |
Treat all of these as directional. Latency swings with JavaScript rendering on or off, “success” is defined differently by each vendor, and every row is a vendor measuring itself on its own targets. The only fair comparison is running them against the same pages at the same time, which is the benchmark I have not finished. Until then, the honest use of this table is sanity-checking orders of magnitude, not picking a winner by a tenth of a second.
Which should you pick? A recommendation by scenario
Pick by the job in front of you, using the rule that if you can list the URLs, you scrape, and if you cannot, you crawl first. Here is my call per scenario, tools and product included.
| Scenario | My pick | Why |
|---|---|---|
| Map every URL on a site (sitemap, audit) | Scrapy (crawl only) | Built-in link-following, queue, and dedupe; no API cost for a pure crawl |
| Parse HTML you already downloaded | BeautifulSoup | Parse-only, zero crawl overhead, simplest path |
| Crawl + extract a whole site, self-hosted | Scrapy | One framework does discovery and extraction at scale |
| Fetch known URLs reliably, get clean JSON | ChocoData | Per-request fetch with proxies, JS, structured JSON, from $19/mo |
| JS-heavy pages, either job | Playwright | Renders the page; script the crawl or the scrape as needed |
| Want discovery managed for you too | Bright Data / Apify | Crawl-included products, billed per record or compute |
| Watch one page for changes | A scraper API on a schedule | No crawl needed; one URL, parsed fields, on a timer |
My honest default for most developers: write the crawl yourself when you control the logic (Scrapy or a short link-following loop like the demo), and pay a per-request scraper API for the fetch when sites block you or render with JavaScript. That keeps discovery cheap and in your control while outsourcing the part that actually breaks. Start on a free tier (ChocoData gives 1,000 requests/mo with no card) and only move up when volume forces it. Try ChocoData if a managed fetch fits your pipeline.
One reminder for budgeting: every price here is what the vendor published in mid-2026, and scraper pricing changes often. Confirm the current figure on the vendor’s own page, and check whether a plan bills per request, per credit, or per delivered record before you compare, so you are comparing like with like.
FAQ
Yes. 'Spider' is the older name for a crawler, the program that walks a site by following links. Scrapy calls its crawl classes Spiders for this reason. Both terms mean the link-following discovery program, separate from the parser that extracts fields.
Both, in sequence. Googlebot crawls to discover and refetch URLs, then an indexing stage parses each page to extract title, headings, links, and content. The crawl finds the pages; the parse turns them into index entries. Your own pipeline usually mirrors that split.
Yes. A pure crawl that only records URLs (for a sitemap audit, a broken-link check, or coverage measurement) never extracts page fields. You still fetch each page to read its links, but you discard the body instead of parsing values out of it.