Problem 81 · Concurrency & Python
Concurrent Web Crawler (Thread Pool)
01Problem
Given a start URL and a blocking fetch(url) -> list[str] of links, crawl every reachable page on the same hostname concurrently using a thread pool and return the set of visited URLs.
02Key insight
Crawling is IO-bound, so threads scale despite the GIL (the GIL is released during network waits). Keep all mutation of the 'seen' set on the coordinating thread and use wait(FIRST_COMPLETED) so workers only fetch; this avoids locks and duplicate fetches.
03Solution
Try it first.
TimeO(V+E) fetches/link checks
SpaceO(V)
tests.py · 5 passing
tests.pypython21 lines
import threading
graph = {
"http://a.com/": ["/x", "http://a.com/y#top", "http://b.com/z"],
"http://a.com/x": ["/y", "/"],
"http://a.com/y": ["/broken"],
}
calls = []
lock = threading.Lock()
def fake_fetch(url):
with lock:
calls.append(url)
if url.endswith("/broken"):
raise IOError("500")
return graph.get(url, [])
res = crawl("http://a.com/", fake_fetch, max_workers=4)
assert res == {"http://a.com/", "http://a.com/x", "http://a.com/y", "http://a.com/broken"}
assert len(calls) == len(set(calls)) == 4 # no URL fetched twice
assert all("b.com" not in u for u in res)
calls.clear()
assert crawl("http://a.com", lambda u: calls.append(u) or ["/", "#top"]) == {"http://a.com/"}
assert calls == ["http://a.com/"] # unnormalized start URL is not fetched twice04Staff-level follow-up
How do you make it distributed and polite? Partition the frontier by hash(host) across workers so per-host rate limits and robots.txt live in one place, and persist the frontier/seen set (e.g. a KV store or Bloom filter) for crash recovery.