Knowledge Base
How to Scrape Multiple Pages Using Beautifulsoup
A step-by-step Python guide to scraping data across many pages with BeautifulSoup, handling pagination patterns, errors and rate limits cleanly.
Knowledge Base
A step-by-step Python guide to scraping data across many pages with BeautifulSoup, handling pagination patterns, errors and rate limits cleanly.
Scraping a single page is easy; the real work begins when the data you need is spread across dozens or hundreds of pages. Product listings, search results and article archives almost always use pagination, so a useful scraper has to walk from one page to the next reliably.
This tutorial covers the common pagination patterns, how to loop over them with BeautifulSoup and requests, and how to stay polite so you do not get blocked partway through a long run.
Looping over pages is the easy part; finishing a long run reliably is the hard part. Reuse a single requests.Session for connection pooling, make the job resumable so a crash near the end does not restart from zero, deduplicate records across overlapping pages, and add controlled concurrency only after rate limiting and proxies are in place. These habits turn a fragile demo loop into a crawl that actually completes.
Before writing code, look at how the site numbers its pages. Most sites fall into one of three patterns, and the right approach depends on which you are dealing with.
?page=2 or /page/2/, where you can build the URL yourself.When the page number is part of the URL, a simple loop is the cleanest solution. Build each URL, fetch it, parse it, and stop when a page returns no results.
import requests
from bs4 import BeautifulSoup
base = "https://example.com/products?page={}"
all_items = []
for page in range(1, 11):
resp = requests.get(base.format(page), timeout=10)
soup = BeautifulSoup(resp.text, "html.parser")
items = soup.select(".product-title")
if not items:
break # no more results, stop early
all_items.extend(i.get_text(strip=True) for i in items)
print(len(all_items), "items collected")Some sites do not expose a predictable page number, but every page links to the next one. Here you start at the first URL and follow the next link until it is gone.
from urllib.parse import urljoin
url = "https://example.com/blog"
results = []
while url:
soup = BeautifulSoup(requests.get(url, timeout=10).text, "html.parser")
results += [h.get_text(strip=True) for h in soup.select("h2.post-title")]
next_link = soup.select_one("a.next")
url = urljoin(url, next_link["href"]) if next_link else NoneSending requests as fast as possible is the quickest way to get rate limited or banned. A few habits keep a multi-page scrape healthy.
import time
headers = {"User-Agent": "Mozilla/5.0 (compatible; MyScraper/1.0)"}
for page in range(1, 11):
resp = requests.get(base.format(page), headers=headers, timeout=10)
# process page...
time.sleep(2) # pause between requests
for page in range(1, 11):
try:
resp = requests.get(base.format(page), timeout=10)
resp.raise_for_status()
except requests.RequestException as e:
print("Skipping page", page, "-", e)
continue
# parse normally...For long runs, write results to disk page by page rather than holding everything in memory. Appending rows to a CSV after each page means a crash near the end does not lose all your work, and it makes the job restartable.
Across many pages from one IP, sites often throttle or block you regardless of how polite you are. Rotating proxies distribute requests across different addresses so a long paginated crawl can complete. Cheapest Proxies is our featured value pick for this kind of steady, high-volume work, though it is always worth comparing providers on rotation frequency, country coverage and price for your specific target.
A quick value-first shortlist — Cheapest Proxies leads as the featured pick. Qualitative labels only; confirm exact plans before buying.
| Provider | Best for | Profile | Value |
|---|---|---|---|
| Cheapest Proxies | Budget-conscious buyers comparing affordable proxies | Value Focused | Excellent value |
| Bright Data | Enterprises needing huge pools and compliance controls | Enterprise Focused | Premium |
| Oxylabs | Large-scale scraping and data APIs | Enterprise Focused | Premium |
| Smartproxy (Decodo) | Newcomers who want an easy dashboard | Beginner Friendly | Good |
| SOAX | Precise city and carrier targeting | Automation Friendly | Good |
Calling requests.get repeatedly opens a new connection each time and discards any cookies the site sets. A requests.Session keeps the TCP connection alive across pages, carries cookies that some paginated listings depend on, and gives you one place to set default headers. For a multi-page run this is both a speed win and a reliability win, because many sites treat a cookie-less sequence of hits as suspicious.
import requests
session = requests.Session()
session.headers.update({"User-Agent": "Mozilla/5.0 (compatible; MyScraper/1.0)"})
resp = session.get(url, timeout=10)Long crawls fail in boring ways: a network blip, a process kill, a power cut. If you record the last completed page (or a set of done page numbers) to a small state file, the next run can skip what is finished and continue. Combined with appending results to disk after each page, this means an interruption costs you one page of work rather than the entire job.
Paginated sites are not static: items shift between pages as new entries are added, so the same record can appear on page two during one fetch and page three minutes later. Deduplicating by page number does nothing for this. Instead, derive a stable key from each record, such as a product id or a normalised URL, and keep a set of seen keys so repeats are dropped no matter which page surfaced them.
Stopping when a page yields no items is a good baseline, but pages can return an empty body during a transient block and trick your loop into quitting early. A safer end-of-pagination signal combines an empty result set with a successful status code, and treats a non-200 response as a retry rather than an end. When you do hit throttling, exponential backoff with a little random jitter spreads retries out and recovers gracefully where a fixed two-second sleep simply gets blocked again.
Start on the smallest sensible tier and scale only what proves itself on your real targets.
Pick the proxy type the task needs first — it drives both success rate and cost more than the logo.
Check traffic limits, rotation rules and what happens on overage before you commit.
Our featured value pick, Cheapest Proxies, is a sensible starting point for affordable comparison.
Multi-page scraping is where proxy costs add up fastest, because every page is another request. Comparing providers on value, not just raw IP counts, keeps a large crawl affordable, and a budget-friendly rotating pool often serves paginated jobs better than a premium plan you only partially use.
Compare Proxy Zone weighs providers on value, fit and reliability using qualitative judgement — never invented prices, speeds or uptime figures. See our review methodology, or email info@compareproxyzone.com with a correction.
Either read the total from the site's pagination controls, or loop until a page returns no results and break out of the loop automatically.
Start at the first URL, parse each page, find the next link with a selector, resolve it with urljoin, and repeat until no next link exists.
You are likely being rate limited or blocked; add delays, set a real User-Agent, and use rotating proxies so requests come from varied addresses.
You can, but concurrency increases the load on the target and your block risk, so add rate limiting and proxies before scaling up parallel requests.
Write each page's results to a CSV or database as you go, so a failure late in the run does not discard everything you already collected.
Not directly, because the content loads via JavaScript; you usually call the underlying API with incrementing offsets, or use a browser automation tool instead.
For affordable proxies across the main types, our featured value pick is Cheapest Proxies — a strong budget-friendly option worth considering. Check the exact plan before ordering.