Web scraping is the process of automatically extracting data from websites. Beautiful Soup is a Python library that makes it easy to parse HTML and XML files and pull data out of them. The data science community has used it for years to gather data from the public web.
Estimated Reading Time: 18–20 minutes
Difficulty: Beginner → Intermediate
Prerequisites: Basic Python programming, installing packages with pip, and a basic understanding of HTML tags.
By the end of this chapter, you will be able to:
Loading diagram...
What this diagram shows: The typical web scraping process: fetching website HTML using BeautifulSoup or Playwright, extracting elements (tags, text, attributes), parsing them into a pandas or Polars DataFrame, and preparing them for data analysis or modeling.
Loading diagram...
Before scraping any website, you must check its robots.txt file. This is a simple text file that tells web scrapers (and search engines) which parts of the website they are allowed to access.
What is robots.txt?
Every website has a robots.txt file located at the root URL. For example:
https://example.com/robots.txthttps://amazon.com/robots.txthttps://github.com/robots.txtHow to read robots.txt:
User-agent: *
Disallow: /admin/
Disallow: /private/
Disallow: /temp/
Allow: /public/
Crawl-delay: 5
What it means:
User-agent: * — Rules apply to all bots (including your scraper)Disallow: /admin/ — Don't scrape anything in the /admin/ folderAllow: /public/ — You can scrape the /public/ folderCrawl-delay: 5 — Wait 5 seconds between requestsHow to check a website's robots.txt:
Loading code block...
Or simply visit it in your browser:
Open https://example.com/robots.txt in your web browser to see it directly.
Real-world example:
If a website's robots.txt says:
User-agent: *
Disallow: /
This means don't scrape this website at all. Respecting this keeps you legal and ethical.
Why it matters:
⚠️ Important: Always check a website's
robots.txtfile and Terms of Service before scraping. Respect rate limits. Usetime.sleep()between requests. Consider using official APIs first — they're more reliable and ethical.
| Tool | Best For | Pros | Cons |
|---|---|---|---|
| Beautiful Soup | Static HTML parsing | Simple, lightweight, easy to learn | Can't handle JavaScript-rendered pages |
| Playwright | Modern dynamic sites | Handles JavaScript, browser automation, cross-browser | Heavier, slower |
| Selenium | Complex interactions | Mature, browser control, screenshots | Slow, resource-intensive |
| Scrapy | Large-scale scraping | Full framework, middleware, pipelines | Steep learning curve |
| httpx | Async scraping | Fast, async/await support, HTTP/2 | Parsing still needs Beautiful Soup |
| LLM + Vision APIs | Unstructured content | Can understand images and PDFs, flexible | Expensive, slower |
Web pages are built from HTML tags. Beautiful Soup navigates these tags to extract data.
What this code does: A sample HTML structure displaying document markup, metadata, and basic elements like headings (<h1>), paragraphs (<p>), lists (<ul>, <li>), and hyperlinks (<a>) with attributes.
Loading code block...
Key HTML concepts:
<h1>, <p>, <a>, <ul>, <li> — define the type of contentclass="description", href="..." — provide extra information about a tagSetup: Install the required libraries before running the examples.
You need two main libraries:
What this code does: Install both libraries using pip (Python's package manager).
Loading code block...
That's it! You're ready to start scraping.
What this code does: Parses an HTML string and extracts different parts of it: the page title, heading, paragraph text, and links.
Loading code block...
What this code does: Finds all links (<a> tags) on the page and prints each link's text and URL.
Loading code block...
What this code does: Downloads a real website using requests, checks if it loaded successfully, parses the HTML with Beautiful Soup, and extracts the first 5 book titles.
Loading code block...
| Method | Description | Example |
|---|---|---|
soup.find(tag) | Find the first matching tag | soup.find('h1') |
soup.find_all(tag) | Find all matching tags | soup.find_all('p') |
soup.find(tag, class_='x') | Find by tag and CSS class | soup.find('div', class_='price') |
tag.text | Get the inner text | soup.h1.text |
tag['href'] | Get an attribute value | link['href'] |
tag.get('href', None) | Safely get an attribute | link.get('href', 'N/A') |
tag.parent | Get the parent element | soup.h1.parent |
tag.find_next_sibling() | Get the next sibling element | row.find_next_sibling('tr') |
While find() and find_all() work well, CSS Selectors are often cleaner and more powerful for complex HTML. They use the same syntax as web developers use in CSS.
Key selector patterns:
.classname — Select by class#id — Select by IDtag.classname — Select by tag and classparent > child — Direct childancestor descendant — Any nested elementWhat this code does: Uses CSS selectors instead of find() to extract the same data more cleanly.
Loading code block...
select_one() vs select():
select_one() — Returns the first match (like find())select() — Returns a list of all matches (like find_all())When scraping real websites, you'll run into issues that basic examples don't cover. Here are essential techniques:
If find() doesn't match anything, it returns None. Calling .text on None crashes your script with AttributeError.
The Problem:
Loading code block...
The Solution: Always check if the element exists
Loading code block...
Sometimes you need to find elements based on the text they contain, not their class or ID.
What this code does: Finds links or buttons by their visible text using regular expressions.
Loading code block...
HTML source code includes extra spaces, newlines (\n), and tabs. The .text property captures all of this junk.
The Problem:
Loading code block...
The Solution: Use strip=True
Loading code block...
Beautiful Soup's default html.parser is slow and strict. For production scraping:
Install them first:
Loading code block...
Use them in your code:
Loading code block...
When to use each:
html.parser — Default, works fine for most siteslxml — When speed matters (scraping 10,000+ pages)html5lib — When website HTML is messy or brokenMany websites block the default Python requests library instantly, returning 403 Forbidden. You need to include a User-Agent header to look like a real browser.
The Problem:
Loading code block...
The Solution: Add a User-Agent header
Loading code block...
Best practice: Rotate User-Agents
Some websites track User-Agent and block if it repeats. Rotate between different ones:
Loading code block...
What this code does: Scrapes news headlines and their URLs from Hacker News with proper headers, stores them in a list, and converts them into a pandas DataFrame.
Loading code block...
Output:
| title | url |
|---|---|
| Show HN: I made a search engine using AI vector... | https://example.com/ai-search |
| The End of Moore's Law | https://example.com/moores-law |
| Python 3.13 Released | https://python.org/downloads |
| New Findings in Quantum Computing | https://example.com/quantum |
| GitHub's New AI Features | https://github.com/features |
Notice we used .strip() to clean up text and included headers for better compatibility!
Many modern websites (built with React, Vue, Angular) load content using JavaScript, so Beautiful Soup alone won't work. Playwright automates a real browser to execute JavaScript first, then capture the full rendered page.
Step 1: Install Playwright
Loading code block...
The first command installs the library. The second command downloads a Chromium browser that Playwright controls.
Step 2: Scrape Dynamic Content
What this code does: Opens a virtual browser, visits a website that loads content with JavaScript, waits for content to appear, captures the rendered HTML, and parses it with Beautiful Soup.
Loading code block...
Output:
Loading code block...
When to use Playwright:
Think of web scraping like visiting a library:
robots.txt or Terms of Service may violate website policies.AttributeError exceptions.Question: What is the difference between Beautiful Soup and Playwright? Answer: Beautiful Soup parses static HTML content that's already loaded, making it fast and lightweight. Playwright automates a real browser to execute JavaScript first, then captures the rendered content. Use Beautiful Soup for simple static websites and Playwright for modern sites that load content dynamically with JavaScript.
Question: Why should you always check robots.txt and the website's Terms of Service before scraping?
Answer: Checking robots.txt tells you which parts of the site the owner allows automated access to. The Terms of Service may prohibit scraping. Respecting these rules keeps your scraper legal, ethical, and prevents your IP from being blocked by the website.
Question: What is the difference between find() and find_all()?
Answer: find() returns the first matching element (a single tag object), while find_all() returns a list of all matching elements. Use find() when you want one specific element, and find_all() when you want to loop through multiple matching elements.
Question: When would you use Playwright instead of Beautiful Soup for web scraping? Answer: Use Playwright when websites render content using JavaScript frameworks (React, Vue, Angular), require user interactions like clicking or scrolling, or when you need to capture screenshots or PDFs. Beautiful Soup alone cannot handle JavaScript-rendered content.
Question: Why do you need to add a User-Agent header when scraping?
Answer: Many websites block the default Python requests User-Agent automatically, returning a 403 Forbidden error. Adding a fake User-Agent makes your scraper look like a real browser, allowing you to access the website. This is a standard practice in production scraping.
Question: What should you do if find() returns None and you try to call .text on it?
Answer: This will crash with AttributeError. Always check if the element exists first using an if statement: tag = soup.find(...); text = tag.text if tag else "N/A". This defensive programming prevents crashes when elements are missing.
Basics:
find() for single elements and find_all() for multiple matches.select_one() and select() for CSS Selectors (cleaner syntax for complex HTML)..text or .get_text(strip=True) and attributes with tag['attr_name'].Important for Production Scraping:
User-Agent header to avoid being blocked by websites..get_text(strip=True) to remove extra whitespace automatically.lxml parser for speed or html5lib for broken HTML.Advanced:
string parameter.requests library for simple scraping of static sites.robots.txt and the site's terms of service before scraping.Continue learning with: