0%

59- Beautiful Soup 4

Parse HTML and XML documents. Navigate the parse tree. Search for elements. Extract data with ease. The most popular Python library for web scraping.

You have downloaded HTML pages using requests. Now you need to extract data from them. HTML is a hierarchical document with tags, attributes, and text. You could try to parse it with regular expressions, but that path leads to frustration and fragile code. Beautiful Soup is the answer. It parses HTML and creates a parse tree. You can navigate this tree, search for elements by tag name, class, id, or any attribute, and extract the data you need. Beautiful Soup handles poorly formatted HTML (which most web pages are) gracefully. This lesson covers everything you need to know about Beautiful Soup 4: creating soup objects, finding elements with find() and find_all(), using CSS selectors, navigating the tree, and extracting data. By the end, you will be able to scrape most websites with confidence.

🕯️ Magic Note

Beautiful Soup is named after the Lewis Carroll poem “Beautiful Soup” from Alice in Wonderland. The library was created to be “beautiful” to use, turning messy HTML into a well-structured soup that is easy to digest.

Installing Beautiful Soup
Install Beautiful Soup 4 and an optional parser.

Bash

# Install Beautiful Soup

pip install beautifulsoup4

# Optional: Install lxml parser (faster)

pip install lxml

# Optional: Install html5lib (most lenient)

pip install html5lib

Creating a BeautifulSoup Object
Parse HTML from a string or a file.

Python

from bs4 import BeautifulSoup

# From an HTML string

html_string = “<html><body><h1>Hello</h1></body></html>”

soup = BeautifulSoup(html_string, “html.parser”)

# From a file

with open(“index.html”, “r”, encoding=”utf-8″) as f:

soup = BeautifulSoup(f, “html.parser”)

# From a URL (after downloading with requests)

import requests

response = requests.get(“https://example.com”)

soup = BeautifulSoup(response.text, “html.parser”)

# Using different parsers

soup = BeautifulSoup(html_string, “html.parser”) # Built-in, good enough

soup = BeautifulSoup(html_string, “lxml”) # Faster, needs lxml

soup = BeautifulSoup(html_string, “html5lib”) # Most lenient, needs html5lib

💡 The built-in “html.parser” is sufficient for most projects. Use “lxml” if you need speed. Use “html5lib” for very broken HTML pages.
Navigating the Parse Tree: Basic Elements
Access elements directly by tag name.

Python

from bs4 import BeautifulSoup

html = “””

<html>

<body>

<h1 id=”main-title” class=”title”>Welcome to My Site</h1>

<p>This is the first paragraph.</p>

<p>This is the second paragraph.</p>

<div>

<a href=”https://example.com”>Example Link</a>

</div>

</body>

</html>

“””

soup = BeautifulSoup(html, “html.parser”)

# Access by tag name (returns the first matching tag)

h1 = soup.h1

print(f”h1 text: {h1.text}”)

print(f”h1 id: {h1.get(‘id’)}”)

print(f”h1 class: {h1.get(‘class’)}”)

# Multiple tags with the same name (returns first only)

first_p = soup.p

print(f”First p text: {first_p.text}”)

# Use find_all() to get all matching tags

all_p = soup.find_all(“p”)

for i, p in enumerate(all_p):

print(f”p[{i}]: {p.text}”)

Finding Elements: find() and find_all()
Search for elements by tag, attributes, or custom filters.

Python

from bs4 import BeautifulSoup

html = “””

<div id=”container” class=”main”>

<ul>

<li class=”item” data-id=”1″>Item 1</li>

<li class=”item” data-id=”2″>Item 2</li>

<li class=”item special” data-id=”3″>Item 3 (special)</li>

</ul>

<p class=”description”>Some description here.</p>

<p>Another paragraph without class.</p>

</div>

“””

soup = BeautifulSoup(html, “html.parser”)

# find() – returns first matching element

first_li = soup.find(“li”)

print(f”First li: {first_li.text}”)

# find_all() – returns list of all matching elements

all_li = soup.find_all(“li”)

print(f”All li: {len(all_li)} found”)

# Find by class

description = soup.find(class_=”description”) # Note: class_ (underscore)

print(f”By class: {description.text}”)

# Find all elements with a specific class

items = soup.find_all(class_=”item”)

print(f”Items: {[item.text for item in items]}”)

# Find by ID

container = soup.find(id=”container”)

print(f”By ID: {container.get(‘class’)}”)

# Find by attribute

item_with_data = soup.find(attrs={“data-id”: “2”})

print(f”By data-id=2: {item_with_data.text}”)

# Find with multiple conditions

special_item = soup.find(“li”, class_=”special”)

print(f”Special item: {special_item.text}”)

🕯️ Magic Note

The find_all() method has many useful parameters: limit to limit results, recursive=False to search only direct children, and string to search by text content. These make complex searches simple.

CSS Selectors with select() and select_one()
Use CSS selector syntax for more complex queries.

Python

from bs4 import BeautifulSoup

html = “””

<div id=”container”>

<ul class=”menu”>

<li><a href=”/home”>Home</a></li>

<li><a href=”/about” class=”active”>About</a></li>

<li><a href=”/contact”>Contact</a></li>

</ul>

<div class=”content”>

<p class=”first”>First paragraph.</p>

<p>Second paragraph.</p>

</div>

</div>

“””

soup = BeautifulSoup(html, “html.parser”)

# Basic CSS selectors

menu_items = soup.select(“ul.menu li”)

print(f”Menu items: {len(menu_items)}”)

# Select by ID (#id)

container = soup.select_one(“#container”)

print(f”Container found: {container is not None}”)

# Select by class (.class)

active_link = soup.select_one(“a.active”)

print(f”Active link: {active_link.get(‘href’)}”)

# Select by attribute

first_para = soup.select_one(“p[class=’first’]”)

print(f”First paragraph: {first_para.text}”)

# Select with multiple conditions

active_menu = soup.select(“ul.menu li a.active”)

if active_menu:

print(f”Active menu: {active_menu[0].get(‘href’)}”)

💡 select() returns a list of all matching elements. select_one() returns the first match. CSS selectors are more concise than nested find() calls for complex paths.
Navigating the Tree: Parent, Children, Siblings
Move up, down, and sideways in the parse tree.

Python

from bs4 import BeautifulSoup

html = “””

<div class=”container”>

<ul>

<li>First</li>

<li>Second</li>

<li>Third</li>

</ul>

<p>Footer text</p>

</div>

“””

soup = BeautifulSoup(html, “html.parser”)

# Start with the second li

second_li = soup.find_all(“li”)[1] # Second li element

print(f”Current: {second_li.text}”)

# Parent

parent_ul = second_li.parent

print(f”Parent: {parent_ul.name}”)

# Grandparent

grandparent = parent_ul.parent

print(f”Grandparent: {grandparent.get(‘class’)}”)

# Previous sibling

prev_sibling = second_li.previous_sibling

if prev_sibling and prev_sibling.name == “li”:

print(f”Previous sibling: {prev_sibling.text}”)

# Next sibling

next_sibling = second_li.next_sibling

if next_sibling and next_sibling.name == “li”:

print(f”Next sibling: {next_sibling.text}”)

# All siblings

for sibling in second_li.previous_siblings:

if sibling.name == “li”:

print(f”Earlier sibling: {sibling.text}”)

Extracting Data: text, attributes, and contents
Different ways to extract data from tags.

Python

from bs4 import BeautifulSoup

html = “””

<div class=”product” id=”prod-123″>

<h2>Python Book</h2>

<p class=”price”>$29.99</p>

<a href=”/products/123″ data-id=”123″ target=”_blank”>View Details</a>

<div>Additional <span>info</span> here.</div>

</div>

“””

soup = BeautifulSoup(html, “html.parser”)

div = soup.find(“div”, class_=”product”)

# Get text (strips HTML tags)

print(f”.text: {div.text.strip()}”)

print(f”.get_text(): {div.get_text(strip=True)}”)

# Get attribute values

product_id = div.get(“id”)

product_class = div.get(“class”)

print(f”id: {product_id}”)

print(f”class: {product_class}”)

# Get attribute with default value

missing = div.get(“data-missing”, “default”)

print(f”Missing attribute: {missing}”)

# Get child elements

children = list(div.children)

print(f”Direct children: {len(children)}”)

# Get all descendants

descendants = list(div.descendants)

print(f”All descendants: {len(descendants)}”)

Searching by Text Content
Find elements that contain specific text.

Python

from bs4 import BeautifulSoup

html = “””

<ul>

<li>Apple</li>

<li>Banana</li>

<li>Cherry</li>

<li>Date</li>

</ul>

“””

soup = BeautifulSoup(html, “html.parser”)

# Find li containing exact text

banana_li = soup.find(“li”, string=”Banana”)

print(f”Exact match: {banana_li}”)

# Find using function

def contains_an(text):

return text and “an” in text.lower()

matching = soup.find_all(“li”, string=contains_an)

print(f”Contains ‘an’: {[item.text for item in matching]}”)

# Find by text regex

import re

pattern = re.compile(r”a”, re.IGNORECASE)

matching = soup.find_all(“li”, string=pattern)

print(f”Contains ‘a’ (case-insensitive): {[item.text for item in matching]}”)

Using BeautifulSoup with Real Websites
A complete example scraping multiple pages with session persistence.

Python

import requests

from bs4 import BeautifulSoup

import time

import csv

def scrape_quotes(base_url=”http://quotes.toscrape.com”):

“””Scrape quotes and authors from quotes.toscrape.com.”””

session = requests.Session()

session.headers.update({

“User-Agent”: “Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36”

})

quotes_data = []

page = 1

while True:

url = f”{base_url}/page/{page}/” if page > 1 else base_url

print(f”Scraping page {page}…”)

try:

response = session.get(url, timeout=10)

response.raise_for_status()

except requests.exceptions.RequestException as e:

print(f”Error fetching {url}: {e}”)

break

soup = BeautifulSoup(response.text, “html.parser”)

quotes = soup.find_all(“div”, class_=”quote”)

if not quotes:

print(“No more quotes found. Stopping.”)

break

for quote_div in quotes:

text = quote_div.find(“span”, class_=”text”)

author = quote_div.find(“small”, class_=”author”)

tags = quote_div.find_all(“a”, class_=”tag”)

quotes_data.append({

“text”: text.text if text else “N/A”,

“author”: author.text if author else “N/A”,

“tags”: “, “.join(tag.text for tag in tags)

})

# Check if there is a next page

next_button = soup.find(“li”, class_=”next”)

if not next_button:

print(“No next page. Finished scraping.”)

break

page += 1

time.sleep(0.5) # Be polite to the server

print(f”Scraped {len(quotes_data)} quotes”)

# Save to CSV

with open(“quotes.csv”, “w”, newline=””, encoding=”utf-8″) as f:

writer = csv.DictWriter(f, fieldnames=[“text”, “author”, “tags”])

writer.writeheader()

writer.writerows(quotes_data)

return quotes_data

if __name__ == “__main__”:

quotes = scrape_quotes()

for quote in quotes[:3]:

print(f”‘{quote[‘text’]}’ – {quote[‘author’]}”)

🕯️ Magic Note

The site quotes.toscrape.com is another safe practice site for web scraping. It has multiple pages and a clear structure, making it perfect for learning.

Common BeautifulSoup Patterns
Reusable patterns for common scraping tasks.

Python

# Pattern 1: Safe element extraction

def extract_text(element, selector, default=”N/A”):

“””Safely extract text from a selector within an element.”””

found = element.select_one(selector)

return found.text.strip() if found else default

# Pattern 2: Extract all links

def get_all_links(soup, base_url=””):

links = []

for a in soup.find_all(“a”, href=True):

href = a[“href”]

if base_url and href.startswith(“/”):

href = base_url + href

links.append({

“text”: a.text.strip(),

“href”: href

})

return links

# Pattern 3: Extract table data

def extract_table(table_element):

headers = []

for th in table_element.find_all(“th”):

headers.append(th.text.strip())

rows = []

for tr in table_element.find_all(“tr”):

cells = tr.find_all(“td”)

if cells:

row = [cell.text.strip() for cell in cells]

rows.append(row)

return {“headers”: headers, “rows”: rows}

Common Mistakes with BeautifulSoup
  • Forgetting to handle missing elements (causing AttributeError)
  • Using find_all() when find() would suffice (less efficient)
  • Not using .strip() on extracted text (capturing whitespace)
  • Parsing JSON or XML with BeautifulSoup (use json module for JSON)
  • Assuming elements will always have the attributes you expect
  • Not handling dynamic JavaScript content (use Selenium for JS-heavy sites)
Check Your Understanding
  • What is the difference between find() and find_all()?
  • How do you find all elements with a specific class?
  • What is the advantage of using CSS selectors with select()?
  • How do you extract the text content of an element without HTML tags?
  • What is the difference between .text and .get_text()?
  • How do you find an element by its ID?

⚡ Whisper

Beautiful Soup turns messy HTML into delicious data. It is patient with broken tags, forgiving with missing attributes, and gentle with malformed documents. You give it a page. It gives you a tree. You search with find_all() and select(). You navigate with parent and children. You extract with .text and .get(). The data emerges. A list of products. A table of prices. A collection of quotes. All ready for your analysis. Beautiful Soup does not scrape—you scrape. Beautiful Soup helps you parse. Learn its methods. Practice on practice sites. Handle missing elements gracefully. Use .strip() for clean text. And remember: Beautiful Soup parses what is there. It cannot run JavaScript. It cannot fill forms. For dynamic sites, you need Selenium (next lesson). But for static HTML, Beautiful Soup is your best friend. Use it well. The web holds endless data. Beautiful Soup helps you collect it.

Related posts