Robust scrapers and dynamic pages

Beautiful Soup · Advanced · 12 min

Scrapers break constantly; the design choices here decide whether a break is a crash, silent bad data or an easy fix.

Pages change without notice. A few habits make scrapers resilient:

  • Anchor on meaning, not position: ids, data attributes, semantic classes, label text ("Price") and structured data, not div:nth-child(4).
  • Use structured data when present: many pages embed JSON-LD in <script type="application/ld+json"> or Open Graph <meta property="og:..."> tags. Parsing that JSON is far more stable than scraping visible markup.
  • Isolate field extraction in small functions that return None for missing data, and validate records afterwards: a required field going missing across all records means the layout changed, so fail loudly.
  • Save raw HTML of fetched pages so you can re-parse after fixing a bug without refetching.

Dynamic pages: if the data you see in the browser isn't in resp.content, JavaScript loads it. Check "View source" (not the inspector, which shows the live DOM). Options, from best to worst:

  • Find the underlying JSON API in the browser's network tab and call it directly.
  • Read the initial state embedded in a script tag (window.__INITIAL_STATE__ = {...}).
  • Render with a headless browser (Playwright) and pass page.content() to Beautiful Soup.

For large crawls with many pages, retries and pipelines, a framework like Scrapy is a better fit than a loop around requests.

Example

import json
from bs4 import BeautifulSoup

html = """<html><head>
<meta property="og:title" content="Steel Kettle">
<script type="application/ld+json">
{"@type": "Product", "name": "Steel Kettle", "offers": {"price": "29.90", "priceCurrency": "EUR"}}
</script></head><body><h1 class="pdp-title-x7">Steel Kettle</h1></body></html>"""
soup = BeautifulSoup(html, "html.parser")

def product_from_jsonld(soup):
    for s in soup.find_all("script", type="application/ld+json"):
        data = json.loads(s.string or "{}")
        if data.get("@type") == "Product":
            return {"name": data["name"], "price": float(data["offers"]["price"])}
    return None

def og(soup, prop):
    tag = soup.find("meta", property=f"og:{prop}")
    return tag["content"] if tag else None

print(product_from_jsonld(soup))   # {'name': 'Steel Kettle', 'price': 29.9}
print(og(soup, "title"), og(soup, "image"))   # Steel Kettle None

Remember

  • Prefer ids, data attributes, labels and structured data over positions
  • JSON-LD and Open Graph tags are stable data sources
  • Validate records and fail loudly when required fields vanish
  • Content missing from the raw HTML is rendered by JavaScript: find the API or render

Watch out for

  • Scraping a generated class like pdp-title-x7 that changes with every frontend build.

Check yourself

  1. Data is visible in the browser's inspector but not in resp.content. Best first step?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.crummy.com/software/BeautifulSoup/bs4/doc/#quick-start