How is web scraping different from web crawling?
ScrapingCrawling answers "what pages exist." Scraping answers "what's in them." A crawler walks links to build a URL list; a scraper takes a URL and pulls out the fields you actually want. They're often chained in the same pipeline, but conflating them is where a lot of scraping code goes wrong.
Common mistake
Writing one function that both discovers pages and extracts data, tightly coupled:
def crawl_and_scrape(start_url, visited=None):
visited = visited or set()
if start_url in visited:
return []
visited.add(start_url)
html = requests.get(start_url).text
soup = BeautifulSoup(html, "html.parser")
# Extraction logic buried inside the traversal loop
results = [{
"url": start_url,
"title": soup.select_one("h1").get_text(strip=True) if soup.select_one("h1") else None,
}]
for link in soup.select("a[href]"):
results += crawl_and_scrape(urljoin(start_url, link["href"]), visited)
return results
This works for a small site, but it means you can't re-run extraction without re-crawling, can't change your parsing logic without touching traversal code, and can't parallelize the two stages independently.
The fix
Separate discovery from extraction into two passes with a URL list as the interface:
def crawl(start_url, max_pages=500):
"""Discovery: returns a list of URLs, nothing else."""
visited, queue, urls = set(), [start_url], []
while queue and len(urls) < max_pages:
url = queue.pop(0)
if url in visited:
continue
visited.add(url)
html = requests.get(url).text
urls.append(url)
soup = BeautifulSoup(html, "html.parser")
queue += [urljoin(url, a["href"]) for a in soup.select("a[href]")]
return urls
def scrape(url):
"""Extraction: takes one URL, returns one structured record."""
soup = BeautifulSoup(requests.get(url).text, "html.parser")
title_el = soup.select_one("h1")
return {"url": url, "title": title_el.get_text(strip=True) if title_el else None}
url_list = crawl("https://example.com")
records = [scrape(u) for u in url_list]
Why it works
With discovery and extraction as separate functions connected by a plain list of URLs, you can cache the crawl result and iterate on extraction logic without re-fetching the whole site, run extraction in parallel across workers since each scrape() call is independent, and swap either half (a different crawl strategy, a different parser) without touching the other.
Tips
- If you're building a one-off scrape of a known set of pages, you don't need a crawler at all — just a URL list and scrape().
- If you're building a scraper that runs on a schedule, the crawl stage is where you decide freshness and coverage; see how often you should crawl a site.
- Store the URL list from the crawl stage — it's a cheap, reusable artifact that lets you re-run extraction after fixing a selector bug, instead of re-crawling from scratch.
WebCrawlerAPI separates these concerns for you: a crawl job returns the URL list and rendered content for a whole site, so your code only has to handle the extraction logic on top.