Sunday, September 6, 2026
HomeData ScienceWeb Scraping with Python – BeautifulSoup, Scrapy & Playwright 2026

Web Scraping with Python – BeautifulSoup, Scrapy & Playwright 2026

Table of Content

Web scraping is a critical data collection skill — public data on prices, job listings, news, reviews, and social signals is often only accessible through scraping. This guide covers three tools that together handle every scraping scenario: BeautifulSoup for quick static scraping, Scrapy for large-scale crawling, and Playwright for JavaScript-rendered sites.

Ethics and Legality

Always check the site’s robots.txt before scraping (e.g., https://example.com/robots.txt). Respect the Crawl-delay directive. Never scrape personal data without legal basis. Do not overload servers — add delays between requests. Check the site’s Terms of Service. Public data is generally scrape-able; authenticated user data and copyrighted full-text content require more care. When an API exists, use it instead of scraping.

BeautifulSoup for Static Pages

pip install requests beautifulsoup4 lxml

import requests
from bs4 import BeautifulSoup
import pandas as pd
import time
import random

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                  'AppleWebKit/537.36 (KHTML, like Gecko) '
                  'Chrome/120.0.0.0 Safari/537.36'
}

def scrape_page(url: str) -> BeautifulSoup:
    r = requests.get(url, headers=headers, timeout=15)
    r.raise_for_status()
    return BeautifulSoup(r.text, 'lxml')

# Scrape Hacker News front page
soup  = scrape_page('https://news.ycombinator.com')
items = soup.select('.athing')

stories = []
for item in items:
    title_el = item.select_one('.titleline > a')
    score_el = item.find_next_sibling('tr').select_one('.score')
    stories.append({
        'title':  title_el.text if title_el else '',
        'url':    title_el.get('href', '') if title_el else '',
        'score':  score_el.text if score_el else '0 points',
    })

df = pd.DataFrame(stories)
print(df.head(10))

# ── Multi-page scraping ───────────────────────────────────────
all_data = []
for page in range(1, 6):
    url  = f'https://news.ycombinator.com/?p={page}'
    soup = scrape_page(url)
    # ... extract data ...
    all_data.extend(stories)
    time.sleep(random.uniform(1.5, 3.0))   # polite delay

Parsing Techniques

soup = scrape_page('https://example-ecommerce.com/products')

# CSS selectors (most reliable)
prices = soup.select('.product-card .price')
names  = soup.select('h2.product-name')

# By tag + attribute
links  = soup.find_all('a', class_='product-link')
img    = soup.find('img', attrs={'data-testid': 'hero-image'})

# By text content
heading = soup.find('h1', string=lambda t: t and 'Sale' in t)

# Navigate the tree
parent    = soup.find('div', class_='product-info').parent
siblings  = soup.find('h2').find_next_siblings('p')
first_li  = soup.find('ul').find('li')

# Extract attributes
for a in soup.find_all('a', href=True):
    if 'product' in a['href']:
        print(a['href'], a.get_text(strip=True))

# Handle pagination
next_btn = soup.select_one('a[rel="next"]')
next_url = next_btn['href'] if next_btn else None

Scrapy for Large-Scale Crawling

a large snake is laying on the ground
Photo by Louis Tripp on Unsplash
pip install scrapy

# Create a Scrapy project
# scrapy startproject myproject
# cd myproject && scrapy genspider quotes quotes.toscrape.com

# spiders/quotes_spider.py
import scrapy

class QuotesSpider(scrapy.Spider):
    name         = 'quotes'
    start_urls   = ['https://quotes.toscrape.com/']
    custom_settings = {
        'DOWNLOAD_DELAY': 1,
        'RANDOMIZE_DOWNLOAD_DELAY': True,
        'CONCURRENT_REQUESTS': 4,
        'ROBOTSTXT_OBEY': True,
    }

    def parse(self, response):
        # Extract items from the page
        for quote in response.css('div.quote'):
            yield {
                'text':   quote.css('span.text::text').get(),
                'author': quote.css('small.author::text').get(),
                'tags':   quote.css('a.tag::text').getall(),
            }

        # Follow pagination
        next_page = response.css('li.next a::attr(href)').get()
        if next_page:
            yield response.follow(next_page, self.parse)

# Run: scrapy crawl quotes -o quotes.csv
# Run: scrapy crawl quotes -o quotes.json
# Scrapy with Item Pipeline for cleaning
# items.py
import scrapy

class QuoteItem(scrapy.Item):
    text   = scrapy.Field()
    author = scrapy.Field()
    tags   = scrapy.Field()

# pipelines.py
class CleaningPipeline:
    def process_item(self, item, spider):
        item['text'] = item['text'].strip('"').strip('"')
        item['tags'] = [t.lower() for t in item['tags']]
        return item

# settings.py
ITEM_PIPELINES = {'myproject.pipelines.CleaningPipeline': 300}

Playwright for JavaScript Pages

pip install playwright
python -m playwright install chromium

import asyncio
from playwright.async_api import async_playwright
import pandas as pd

async def scrape_js_page():
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        page    = await browser.new_page()

        # Set realistic headers
        await page.set_extra_http_headers({
            'Accept-Language': 'en-US,en;q=0.9'
        })

        # Navigate and wait for content to load
        await page.goto('https://example-spa.com/listings',
                        wait_until='networkidle')

        # Scroll to load lazy content
        for _ in range(5):
            await page.evaluate('window.scrollBy(0, window.innerHeight)')
            await page.wait_for_timeout(1000)

        # Extract data via CSS selectors
        items = await page.query_selector_all('.listing-card')

        results = []
        for item in items:
            title = await item.query_selector('.title')
            price = await item.query_selector('.price')
            results.append({
                'title': await title.inner_text() if title else '',
                'price': await price.inner_text() if price else '',
            })

        await browser.close()
        return pd.DataFrame(results)

df = asyncio.run(scrape_js_page())
print(df.head())

Handling Anti-Scraping Measures

import time, random
from itertools import cycle

# Rotate user agents
USER_AGENTS = [
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...',
    'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36...',
    'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36...',
]
ua_cycle = cycle(USER_AGENTS)

# Use session with retries
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry

session = requests.Session()
retry   = Retry(total=3, backoff_factor=1,
                status_forcelist=[429, 500, 502, 503, 504])
session.mount('http://', HTTPAdapter(max_retries=retry))

def polite_get(url):
    time.sleep(random.uniform(1.0, 3.0))
    headers = {'User-Agent': next(ua_cycle)}
    return session.get(url, headers=headers, timeout=15)

Conclusion

BeautifulSoup is your first tool for any scraping job — fast to set up, easy to debug, perfect for static pages. When you need to scrape thousands of pages systematically, Scrapy’s pipeline architecture and built-in rate limiting make it the right choice. When the target site renders content with JavaScript, Playwright handles what static scrapers cannot. Always scrape responsibly: respect robots.txt, add delays, and prefer official APIs when they exist.

Leave feedback about this

  • Rating

Durgesh Kekare
Durgesh Kekarehttps://www.dataexpertise.in
Durgesh Kekare is a data science educator and founder of DataExpertise.in. With expertise in Python, machine learning, and analytics, he helps 10,000+ learners break into data careers.

Latest Posts

List of Categories