Knowledge Base

How to Scrape Multiple Pages Using BeautifulSoup

A step-by-step handbook for crawling paginated content with BeautifulSoup: build or follow page URLs, loop safely, deduplicate, throttle politely, and rotate proxies so the whole crawl finishes.

From one page to many: the core idea

BeautifulSoup itself only ever parses a single block of HTML. Scraping multiple pages is therefore not a BeautifulSoup feature but a pattern you build around it: fetch a page, parse it, extract what you want, work out the next page, and repeat. Almost every multi-page scrape reduces to that loop. The interesting decisions are how you discover each page's URL, how you stop, and how you keep the site from blocking you halfway through. This handbook covers each of those in turn, building from a simple page-number loop to a polite, proxy-backed crawl you can trust on a real site.

Why multi-page scraping needs more care than single-page

Pulling data off one page is forgiving; a single request rarely upsets anyone. The moment you loop over dozens or hundreds of pages, two new problems appear. First, you are now making a stream of requests from one address, which sites notice and throttle. Second, small bugs that were harmless on one page, like assuming an element always exists, now crash a long run near the end and waste everything before it. Treating reliability and politeness as first-class concerns, not afterthoughts, is what separates a scrape that completes from one that dies at page forty.

What you need before you start

You need Python with beautifulsoup4, requests and ideally lxml installed. You also need to understand the target's pagination: open it in a browser, click through a few pages, and watch the URL. If a page number appears in the address, you can build URLs directly. If the URL barely changes but a "next" button advances the content, you will follow links instead. Knowing which pattern you face decides the whole approach.

Pattern one: looping over page numbers in the URL

When the URL carries a page number, building each address is a counter loop. Fetch, parse, extract, and stop when a page returns nothing.

import time
import requests
from bs4 import BeautifulSoup

base = "https://example.com/listings?page={}"
headers = {"User-Agent": "Mozilla/5.0 (compatible; PageBot/1.0)"}
rows = []

for page in range(1, 100):                 # a sane upper bound
    url = base.format(page)
    resp = requests.get(url, headers=headers, timeout=15)
    soup = BeautifulSoup(resp.text, "lxml")
    items = soup.select("div.item")
    if not items:                          # empty page = we are done
        break
    for it in items:
        title = it.select_one("h3")
        rows.append(title.get_text(strip=True) if title else "")
    time.sleep(1)                          # be polite between pages

print(len(rows), "items collected")

The empty-page check is the natural stop condition, and the upper bound on the range stops a runaway loop if the site never returns an empty page.

Pattern two: following the "next page" link

When there is no clean page number, let the markup guide you. On each page, find the next-page anchor, resolve its href, and use it as the following request. Stop when the link disappears.

from urllib.parse import urljoin

url = "https://example.com/listings"
seen = set()

while url and url not in seen:
    seen.add(url)
    resp = requests.get(url, headers=headers, timeout=15)
    soup = BeautifulSoup(resp.text, "lxml")
    # ... extract items from this page ...
    nxt = soup.select_one("a.next[href]")
    url = urljoin(url, nxt["href"]) if nxt else None
    time.sleep(1)

The seen set guards against loops where a page links back to one you already visited, and the while condition ends cleanly when there is no next link.

Reusing a session for speed and consistency

Creating a requests.Session() once and using it for every page reuses the underlying connection and carries cookies between requests, which is both faster and more consistent. Set your headers on the session so every request looks the same, and the crawl behaves more like a real browsing session.

session = requests.Session()
session.headers.update(headers)
resp = session.get(url, timeout=15)

Deduplicating data as you collect

Paginated sites frequently repeat items across page boundaries, and following links can revisit content. Deduplicate as you go rather than at the end, using a stable key such as a product ID or URL.

  • Keep a set of keys you have already stored and skip any item whose key is in it.
  • Prefer a unique field from the data, like an ID or canonical URL, over the visible title, which can repeat.
  • Track visited page URLs separately from collected item keys so the two concerns stay clear.

Throttling so the crawl stays welcome

A delay between requests is the cheapest insurance there is. Even a short pause turns a burst that looks like an attack into traffic a site can absorb. Vary the delay a little so it is not perfectly regular, and back off further if you start seeing errors. Patience here protects both the site and your access to it.

The single biggest cause of a failed multi-page scrape is firing requests as fast as the loop allows from one IP. Add a delay, rotate IPs, and your crawl goes from "blocked at page forty" to "finished all four hundred". Speed you cannot sustain is not speed; it is a stalled job.

Where proxies become essential

For a handful of pages your own IP is fine. For a real crawl it is the bottleneck, because every page comes from the same address and the site soon throttles it. Routing requests through a rotating proxy pool spreads them across many IPs, so the load on any single address stays low and the crawl keeps moving.

proxies = {
    "http":  "http://user:pass@rotating-endpoint:port",
    "https": "http://user:pass@rotating-endpoint:port",
}
resp = session.get(url, proxies=proxies, timeout=15)

With a rotating endpoint, each request can leave from a different IP without changing your loop at all, which is exactly what a long crawl needs.

Choosing the right proxy type for the job

The right type depends on how strict the target is. Affordable datacenter or IPv4 proxies often handle tolerant sites and large crawls well. Stricter consumer platforms tend to need residential or ISP proxies that resemble ordinary home traffic, and the most defensive sites may require mobile IPs. Start with the cheapest type that survives, watch your block rate, and only move to a more trusted type when the data tells you to.

Handling errors so one bad page does not sink the run

Long crawls hit timeouts, transient errors and the occasional malformed page. Wrap each fetch in error handling, retry a failed request on a fresh IP, and skip a page that will not parse rather than letting it crash everything. Logging which pages failed lets you revisit them later without re-running the whole job.

Who this technique suits

Multi-page scraping is the backbone of price monitoring, listing aggregation, SEO competitor research, dataset building and any task where the data spans more than one page. If your target paginates results, this loop is the heart of your scraper.

Common use cases

  • Collecting every listing across a paginated catalogue or marketplace.
  • Gathering all articles in an archive or blog for analysis.
  • Tracking prices or stock across many product pages over time.
  • Building a dataset from search results that span dozens of pages.
  • Auditing a large site section page by page for SEO purposes.

Common mistakes to avoid

Scrapers fail in predictable ways: no delay between requests so the IP is blocked, no upper bound so an empty page never stops the loop, no visited set so the crawl loops forever, assuming an element exists so one odd page crashes the run, and no proxy so everything routes through a single address. Each of these has a one-line fix shown above, and applying them up front saves hours of debugging later.

Limitations of BeautifulSoup for multi-page work

BeautifulSoup parses the HTML the server returns, so pagination driven entirely by JavaScript, such as infinite scroll that loads content via background requests, will not appear in the raw markup. In those cases you either call the underlying data endpoint directly or render the page with a browser tool like Selenium first, then parse the result. BeautifulSoup remains ideal for server-rendered pagination; pair it with a renderer when the pages only build after scripts run.

Recommended proxy providers

Because a multi-page crawl lives or dies on reliable fetching, pick a proxy provider deliberately. The options below are listed fairly, with our featured value pick first.

  • Cheapest Proxies is our Featured Value Pick. For crawling many pages without overspending while you test, it is a sensible first stop for affordable rotating residential, ISP or datacenter IPs.
  • A large residential network is worth considering when your targets are strict sites that demand IPs resembling ordinary home traffic across a long crawl.
  • A datacenter-focused provider may suit high-volume crawls against tolerant sites where speed and low cost matter most.

How to get started

Identify which pagination pattern the target uses, then adapt the matching loop above against a saved page or two. Add a delay, a visited set and basic error handling, and confirm it completes a small range cleanly. Only then wire in a rotating proxy pool and let it run the full crawl. Building up from a short, local test keeps the inevitable early mistakes cheap and fast to fix.

Key takeaways

Scraping multiple pages is a loop around single-page parsing: build URLs from page numbers or follow next links, stop on an empty page or a missing link, and deduplicate as you go. Politeness and proxies are not optional at scale: pace your requests, keep a visited set, handle errors, and rotate IPs through a well-matched pool so the whole crawl finishes. For JavaScript-driven pagination, reach for a browser tool and then parse with BeautifulSoup.

Related proxy guides

Frequently asked questions

BeautifulSoup parses a single page, so scraping many means looping: build or discover each page's URL, fetch it with requests, parse the HTML, extract what you need, and append the results. Most sites paginate either with a page number in the URL or with a 'next' link in the markup. You walk that structure in a loop until there are no more pages, collecting data as you go.
When the URL contains a page number, like ?page=2, you can build each URL in a loop by substituting the number. Iterate from the first page upward, fetch and parse each one, and stop when a page returns no results or an error. Constructing URLs from a counter is the simplest pattern and works well whenever the site exposes the page number directly in its addresses.
When there is no predictable page number, follow the markup. On each page, find the anchor that points to the next page, read its href, resolve it to an absolute URL with urljoin, and use it as the next request. Repeat until the next link disappears. This 'follow the next button' pattern adapts automatically to however many pages exist, without you knowing the count in advance.
Fetching page after page from a single IP looks abusive, so sites rate-limit or block the address. The fix is twofold: behave politely with delays and a realistic user agent, and spread requests across many IPs using a rotating proxy pool so no single address carries the whole crawl. Pacing plus rotation is what lets a multi-page scrape run to completion instead of stalling partway.
Keep a set of URLs you have already visited and check it before each request, adding every new URL as you go. This matters most when following next links or discovered URLs, because sites often link back to pages you have seen. A visited set prevents loops and wasted requests, and pairs naturally with deduplicating the data you extract so the final result has no repeated rows.
It depends on the target. Tolerant sites and large crawls often run fine on affordable datacenter or IPv4 proxies. Stricter consumer sites usually need residential or ISP proxies that resemble ordinary home traffic, and the most defensive platforms may require mobile IPs. A practical approach is to start with the cheapest type that survives on the site, measure your block rate, and only move to a more trusted type if blocks force the change.

Questions or a correction? Email info@proxyranked.com. Always confirm a provider's exact package, proxy type and locations before ordering.