Data Science · Scraping & Automation
Playwright vs Traditional Tools for Data Scientists
Quick summary — which scraping tool when?
Playwright is the right tool for JS-heavy pages that no other method can reach. For static HTML, requests + BeautifulSoup is faster and simpler. For large crawls, Scrapy handles scheduling and concurrency far better. Selenium still works but is now the slower, older option.
In this guide you will learn:
- requests + BS4 — the default for static pages.
- Playwright — the modern browser automation choice.
- Selenium & Scrapy — when each still makes sense.
- Comparison tables — task-to-tool mapping.
- Real templates — a full data science scraping pipeline.
SECTION 01requests + BeautifulSoup — the static default
For any page whose HTML is server-rendered, requests + BS4 is the fastest and most reliable way to get data. Use it first:
| Task | requests + BS4 | Why it wins | When to switch |
|---|---|---|---|
| Static HTML pages | Yes | Fast, no browser overhead | Content loads via JS |
| JSON APIs | Yes | parse directly, no HTML | API is rate-limited / OAuth |
| Simple pagination | Yes | Just loop over URLs | Pagination requires clicks |
| ~1,000 pages | Yes (with a session) | Enough for a small dataset | ~100k+ pages → Scrapy |
| Login-protected pages | Possible (with cookies) | Works if you can extract cookies | Login flow has JS / captcha |
| SPA / React site | No | Content isn't in HTML | Use Playwright |
# static_scrape.py — the workhorse pattern
import requests
from bs4 import BeautifulSoup
import pandas as pd
import time
BASE_URL = "https://example.com/products"
HEADERS = {
"User-Agent": "Mozilla/5.0 (your research bot)",
}
def fetch_page(page_num: int):
url = f"{BASE_URL}?page={page_num}"
resp = requests.get(url, headers=HEADERS, timeout=10)
resp.raise_for_status()
return resp.text
def parse_page(html: str):
soup = BeautifulSoup(html, "html.parser")
rows = []
for card in soup.select(".product-card"):
rows.append({
"name": card.select_one(".title").get_text(strip=True),
"price": card.select_one(".price").get_text(strip=True),
"rating": card.select_one(".rating")["data-value"],
})
return rows
if __name__ == "__main__":
all_rows = []
for page in range(1, 21):
html = fetch_page(page)
all_rows.extend(parse_page(html))
time.sleep(1) # be polite
df = pd.DataFrame(all_rows)
df.to_csv("products.csv", index=False)
print(f"Saved {len(df)} rows")
# Why this wins for static pages:
# ✓ No browser process to start
# ✓ Can fetch 20 pages in 20 seconds
# ✓ Easy to debug (you can see the HTML)
# ✓ No Playwright/Selenium to install
#
# Do this FIRST on every scraping task.
# Only reach for Playwright if this returns empty.
The 3-step check before writing any scraper:
1. FETCH THE PAGE MANUALLY
Open DevTools → Network tab → reload
Look at the first request to the page
Check "Response" tab.
Is the data you want inside the HTML?
YES → use requests + BeautifulSoup
NO → continue
2. LOOK FOR AN API
Filter Network to "Fetch/XHR"
Reload. Any JSON response with the data?
Often the site loads via a clean JSON API
you can call directly.
YES → call the JSON API (still requests)
NO → continue
3. CHECK IF RENDERED BY JAVASCRIPT
View page source (Ctrl+U)
Is the data in the source?
Is it missing but visible in the browser?
NO → requests + BS4 works
YES → you need Playwright
The rule:
95% of "scraping is hard" problems are
"the site is a React app now".
Check that before you install Playwright.
Remember: respect robots.txt, terms of service,
and rate limits. Data collection is not the
same as bypassing protections.
SECTION 02Playwright — the modern browser choice
Playwright drives real browsers and handles what requests can't. It's faster and more stable than Selenium for the same job:
| Feature | Playwright | Selenium | Winner |
|---|---|---|---|
| Setup | One pip install + playwright install | Driver per browser | Playwright |
| Auto-wait | Built-in, reliable | Manual explicit waits | Playwright |
| Browser support | Chromium, Firefox, WebKit | All major + more | Tie |
| Speed | Faster (multi-browser parallel) | Slower | Playwright |
| Network inspection | Built-in (route, HAR) | Limited | Playwright |
| Community size | Growing fast | Huge | Selenium (legacy) |
# playwright_scrape.py — for JS-rendered pages
from playwright.sync_api import sync_playwright
import pandas as pd
BASE_URL = "https://example.com/products"
def scrape_products():
rows = []
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context(
user_agent="Mozilla/5.0 (research bot)",
viewport={"width": 1280, "height": 800},
)
page = context.new_page()
page.goto(BASE_URL, wait_until="networkidle")
# Wait for the actual content
page.wait_for_selector(".product-card")
# Extract
cards = page.query_selector_all(".product-card")
for card in cards:
rows.append({
"name": card.query_selector(".title").inner_text(),
"price": card.query_selector(".price").inner_text(),
"rating": card.get_attribute("data-rating"),
})
context.close()
browser.close()
return pd.DataFrame(rows)
if __name__ == "__main__":
df = scrape_products()
df.to_csv("products.csv", index=False)
print(f"Saved {len(df)} rows")
# Why Playwright works here when requests doesn't:
# - The browser actually runs the JavaScript
# - The DOM contains the rendered content
# - You wait for the real state (networkidle + selector)
#
# The three steps that matter:
# 1. wait_until="networkidle" (or "domcontentloaded" if faster)
# 2. page.wait_for_selector() (wait for the data to appear)
# 3. query_selector_all() (extract)
# playwright_async.py — the faster version for many pages
import asyncio
from playwright.async_api import async_playwright
import pandas as pd
URLS = [f"https://example.com/products?page={i}"
for i in range(1, 51)]
async def scrape_one(page, url):
try:
await page.goto(url, wait_until="domcontentloaded")
await page.wait_for_selector(".product-card", timeout=10000)
rows = []
for card in await page.query_selector_all(".product-card"):
name = await (await card.query_selector(".title")).inner_text()
price = await (await card.query_selector(".price")).inner_text()
rows.append({"name": name, "price": price})
return rows
except Exception as e:
print(f"Failed {url}: {e}")
return []
async def main():
all_rows = []
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
context = await browser.new_context()
page = await context.new_page()
# Concurrency in batches of 5
for i in range(0, len(URLS), 5):
batch = URLS[i:i+5]
results = await asyncio.gather(
*[scrape_one(page, u) for u in batch]
)
for r in results:
all_rows.extend(r)
await browser.close()
df = pd.DataFrame(all_rows)
df.to_csv("products.csv", index=False)
print(f"Saved {len(df)} rows")
if __name__ == "__main__":
asyncio.run(main())
# What you gain over sync:
# - 5x faster for the same number of pages
# - Same API, easy to reason about
# - Reuses one browser context
# What you don't get:
# - Magical bypass of login / captcha
# - Permission to scrape a site that forbids it
wait_until + wait_for_selector, not
time.sleep(). Sleep-based scraping is the biggest cause
of flaky data collection.
SECTION 03Selenium & Scrapy — where they still fit
Neither is obsolete. Here's when each one is actually the right tool:
| Tool | Still right for | Weak at | Verdict |
|---|---|---|---|
| Selenium | Existing QA frameworks, legacy codebases | New scraping projects, slow, flaky by default | Migrate to Playwright |
| Scrapy | 100k+ page crawls, structured pipelines, scheduling | JS-rendered pages (needs a Playwright plugin) | Best at large scale |
| Playwright | JS-heavy pages, complex interactions | Massive crawls (no scheduling built-in) | Best general-purpose browser tool |
| requests + BS4 | Static pages, APIs, small to medium scraping | Anything that needs JS | Default starting point |
# scrapy_example.py — for crawling at scale
import scrapy
class ProductsSpider(scrapy.Spider):
name = "products"
start_urls = ["https://example.com/products"]
custom_settings = {
"DOWNLOAD_DELAY": 1,
"CONCURRENT_REQUESTS": 8,
"RETRY_TIMES": 3,
"FEED_FORMAT": "csv",
"FEED_URI": "products.csv",
}
def parse(self, response):
for card in response.css(".product-card"):
yield {
"name": card.css(".title::text").get(),
"price": card.css(".price::text").get(),
}
# Follow pagination
next_page = response.css("a.next::attr(href)").get()
if next_page:
yield response.follow(next_page, callback=self.parse)
# What Scrapy gives you that Playwright doesn't:
# ✓ Automatic scheduling of thousands of requests
# ✓ Concurrency out of the box
# ✓ Built-in retry, throttling, dedup
# ✓ Pipelines: cleaning, DB writing, exports
# ✓ Robots.txt respected by default
# When you actually need Scrapy:
# - 100,000+ pages
# - Multiple sites, same structure
# - Scheduled re-scrapes
# - You care about politeness and rate limits
# When you don't:
# - A few hundred pages → requests/Playwright is enough
# - One-off data collection for a project
# When you need both: JS + scale
#
# Scrapy handles scheduling; Playwright handles JS.
# Install the plugin: pip install scrapy-playwright
# settings.py
DOWNLOAD_HANDLERS = {
"http": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",
"https": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",
}
TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor"
# spider
import scrapy
class JsSiteSpider(scrapy.Spider):
name = "js_site"
start_urls = ["https://example.com/products"]
def start_requests(self):
for url in self.start_urls:
yield scrapy.Request(
url,
meta={
"playwright": True,
"playwright_page_methods": [
# wait for the rendered content
# (import PlaywrightPageMethod)
],
},
)
def parse(self, response):
for card in response.css(".product-card"):
yield {
"name": card.css(".title::text").get(),
}
# This is the professional setup for large-scale
# JS-heavy scraping.
#
# But note: Playwright inside Scrapy is slow.
# Each page uses a full browser context.
# Budget for it — 100k pages with JS is expensive.
#
# Rule:
# - Under 1,000 JS pages → plain Playwright is fine
# - 1,000-100,000 JS pages → Playwright + a queue (Celery/RQ)
# - 100,000+ JS pages → Scrapy + Playwright, or negotiate an API
SECTION 04Task-to-tool comparison templates
Use these three templates to pick the right tool every time:
Task-to-tool checklist:
STEP 1 — CHECK FOR AN API
☐ DevTools → Network → XHR
☐ Any JSON endpoint with the data?
→ Use requests, done.
STEP 2 — CHECK THE HTML SOURCE
☐ Ctrl+U on the page
☐ Is the data in the source?
→ YES: requests + BeautifulSoup
→ NO: continue
STEP 3 — HOW MUCH DATA?
☐ Under 1,000 pages?
→ Playwright (sync), simple
☐ 1,000-100,000 pages with JS?
→ Playwright (async) + a queue
☐ 100,000+ pages with JS?
→ Scrapy + Playwright plugin
☐ Static + 100,000+ pages?
→ Scrapy alone
STEP 4 — IS IT RECURRING?
☐ Once?
→ Any of the above
☐ Daily / weekly?
→ Schedule it (cron, Airflow, GitHub Actions)
STEP 5 — RESPECT THE SITE
☐ Robots.txt checked?
☐ Terms of Service checked?
☐ Rate limit defined and enforced?
☐ Consent for personal data collected?
☐ Contact email in User-Agent?
Tool comparison — fill in for your task:
requests+BS4 Playwright Scrapy
Setup time 10 min 15 min 1 hour
Static pages ✓ best ✓ ok ✓ ok
JS pages ✗ ✓ best ✓ with plugin
Scale (100k+) ✗ slow ✓ best
Scheduling ✗ ✗ ✓ built-in
Interactive (login) ~ cookies ✓ best ~ with plugin
Debug difficulty low medium high
Speed (static) fastest medium fast
Speed (JS) n/a fast medium
Learning curve low medium high
The default choice:
requests + BS4 → start here
Playwright → when the page needs JS
Scrapy → when you need scale + scheduling
Don't install all three.
Install what the task actually requires.
Ethics and compliance — non-negotiable:
Before scraping any site:
☐ Have you read robots.txt?
→ If disallowed, stop. There's no "but".
☐ Have you read the ToS?
→ Some sites explicitly forbid scraping.
☐ Are you collecting personal data?
→ If yes, you need a lawful basis (GDPR / DPDP Act).
→ Anonymise when possible.
→ Never store more than you need.
☐ Are you rate-limiting?
→ 1 request per second is a reasonable default.
→ Use exponential backoff on 429s.
☐ Do you identify yourself?
→ User-Agent should say what you are.
→ Include a contact email if you're a researcher.
☐ Are you caching?
→ Don't refetch pages you've already fetched.
→ Saves bandwidth for the site too.
What will get you blocked or sued:
✗ Bypassing login
✗ Bypassing CAPTCHA
✗ Ignoring robots.txt
✗ Heavy concurrent requests without throttling
✗ Scraping personal data without a basis
✗ Reselling scraped data
The rule:
If you wouldn't want it done to you,
don't do it to the site.
SECTION 05A complete data science scraping pipeline
Here's the pipeline a data scientist actually needs — not just scraping, but a usable dataset:
- Fetch — requests for static, Playwright for JS, Scrapy for scale.
- Parse & extract — BeautifulSoup or Playwright selectors into dicts.
- Validate — schema checks (Pandera or Pydantic), dedup, and null handling.
- Store — raw JSONL for audit, cleaned Parquet for analysis, Postgres for queries.
- Schedule — cron or Airflow for recurring collection.
- Monitor — success count per run, schema drift, empty-page rate.
SECTION 06Interview Q&A — Playwright vs traditional tools
Q1When should a data scientist use Playwright?
Only when the page's content is generated by JavaScript and can't be fetched by requests. Playwright is slower and heavier — use it when you must. Always try requests first.
Q2Is Selenium obsolete?
Not obsolete, but less chosen for new work. Existing QA frameworks still use it, and it has broader browser support. For new scraping or automation, Playwright is a stronger default.
Q3Which tool handles JS at large scale?
Scrapy with the Playwright plugin. Scrapy handles the scheduling, concurrency and pipelines; Playwright handles the JS. Running 100,000 JS pages with plain Playwright is possible but expensive — the browser overhead dominates.
Q4How do you handle rate limits?
Start with 1 request per second, add exponential backoff on 429, cache responses you've already fetched, and never use unbounded concurrency. Being polite is also being reliable — aggressive scrapers get blocked, not faster.
Q5What's the most common mistake in scraping projects?
Reaching for Playwright before checking the Network tab. Most data is available via a JSON endpoint that would take ten minutes to call. Check the network requests first — it saves hours.
SECTION 07Test yourself — Playwright vs traditional tools
Five questions. No sign-up.
0 / 5Pick an answer to see why it is right or wrong.
SECTION 08Frequently asked questions
What should I try first for any scraping task?
requests + BeautifulSoup, after checking the Network tab for a JSON API. It's faster, simpler, and handles most pages. Only move to Playwright if the content genuinely isn't in the initial HTML or in any network response.
Is Playwright harder than Selenium?
It's easier. The API is more modern, auto-wait is built-in, and setup is one command. If you know Selenium, switching takes a day or two.
Can I scrape at scale without Scrapy?
Yes, up to a point. Playwright async or a simple queue with requests handles a few thousand pages. Scrapy becomes the right tool once you need tens of thousands with scheduling, retries and pipelines.
Is scraping legal?
Depends on the site, the data and where you operate. Public data with robots.txt allowed and no personal information is usually fine for research. Scraping private data, bypassing logins, or reselling scraped content is not. Always read robots.txt and the ToS.
What does a data scientist actually need from scraping?
A dataset that's reliable and refreshed on schedule. That means the scraping is the smallest part — validation, storage, scheduling and monitoring make up most of the work.
SECTION 09Related reads
Classroom & online · Noida
Learn scraping the way data teams use it
Our Data Science Training Course includes a data-collection module covering requests, BeautifulSoup, Playwright and pipeline design — with a real project.
₹42,500 · full programme- requests, BS4, Playwright, Scrapy
- Data collection pipeline project
- Mock interviews & portfolio review
- Weekday & weekend batches