AI Skill Report Card

Web Scraping with Beautiful Soup

A-88·Oct 31, 2026·Source: Extension-page
15 / 15
Python
from bs4 import BeautifulSoup html_doc = """<html><head><title>The Dormouse's story</title></head> <body> <p class="title"><b>The Dormouse's story</b></p> <p class="story">Once upon a time there were three sisters: <a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>, <a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and <a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>.</p> """ soup = BeautifulSoup(html_doc, "lxml") # prefer lxml for speed # Navigate soup.title.string # "The Dormouse's story" soup.p["class"] # ['title'] # Search soup.find_all("a") # all <a> tags soup.find(id="link3") # single tag by attribute [a["href"] for a in soup.find_all("a")] # extract all URLs # Extract text soup.get_text()
Recommendation▾
Add an example showing pagination/multi-page crawling pattern to round out real-world scraping scenarios
15 / 15

Progress checklist for a typical scraping task:

  • Choose a parser (lxml for speed/leniency, html.parser for no dependencies, html5lib for browser-accurate parsing of broken HTML)
  • Load markup into BeautifulSoup(markup, parser_name)
  • Inspect structure with soup.prettify()
  • Navigate (.parent, .children, .next_sibling, dot-access like soup.div.p) or search (find, find_all, CSS select)
  • Extract data (.string, .get_text(), tag['attr'], tag.get('attr'))
  • Modify if needed (.replace_with(), .decompose(), .unwrap(), attribute assignment)
  • Serialize back with str(soup) or soup.prettify() if output is required

1. Pick the right parser

ParserCallWhen to use
html.parserBeautifulSoup(markup, "html.parser")stdlib only, decent speed
lxmlBeautifulSoup(markup, "lxml")default recommendation — fastest
lxml-xml / xmlBeautifulSoup(markup, "xml")parsing actual XML
html5libBeautifulSoup(markup, "html5lib")malformed HTML, need browser-accurate tree

Always pass the parser name explicitly — never rely on autodetection in production code, since different installs may have different parsers available, producing inconsistent trees.

2. Navigating vs. Searching

  • Use dot notation (soup.title, soup.body.p) only for quick, unambiguous access to the first matching tag.
  • Use find/find_all for anything conditional: by tag name, id, class_, attrs dict, string content, or a function predicate.
  • Use CSS selectors (soup.select("p.story a"), soup.select_one(...)) when the target is easier to express as a selector than a filter.

3. Extracting data safely

Python
tag.get("href") # None if missing — safe tag["href"] # KeyError if missing — use only when attribute is guaranteed tag.get_text(strip=True) # text without leading/trailing whitespace

4. Modifying the tree

Python
tag.string.replace_with("new text") tag.decompose() # remove tag and its contents entirely tag.unwrap() # remove tag but keep its contents tag["class"] = ["new-class"] new_tag = soup.new_tag("a", href="http://example.com") tag.append(new_tag)
Recommendation▾
Include a bad-output example (e.g., KeyError or None-string pitfall) to show contrast, not just best-case code
17 / 20

Example 1 — Extract all links: Input: HTML page with multiple <a> tags. Output:

Python
links = [a.get("href") for a in soup.find_all("a") if a.get("href")]

Example 2 — Extract table rows into dicts: Input: <table> with header row and data rows. Output:

Python
table = soup.find("table") headers = [th.get_text(strip=True) for th in table.find("tr").find_all("th")] rows = [] for tr in table.find_all("tr")[1:]: cells = [td.get_text(strip=True) for td in tr.find_all("td")] rows.append(dict(zip(headers, cells)))

Example 3 — Remove scripts/styles before text extraction: Input: page with <script> and <style> tags polluting get_text(). Output:

Python
for tag in soup(["script", "style"]): tag.decompose() clean_text = soup.get_text(separator=" ", strip=True)

Example 4 — Find by multiple attributes: Input: need <div class="product" data-id="42">. Output:

Python
soup.find("div", class_="product", attrs={"data-id": "42"})
Recommendation▾
Mention requests/session integration briefly since scraping typically involves fetching HTML, not just parsing a static string
  • Always specify the parser explicitly: BeautifulSoup(markup, "lxml").
  • Use .get(attr) instead of tag[attr] when the attribute might not exist.
  • Convert NavigableString to str() before storing long-term — it otherwise keeps the whole parse tree alive in memory.
  • Prefer soup.select() for complex CSS-like queries; prefer find_all() with a function predicate for logic that's hard to express as CSS.
  • Strip scripts/styles/comments before calling get_text() if you want clean body text.
  • For multi-valued attributes like class, remember the value is a list (tag['class'] → ['a', 'b']), not a string.
  • When combining documents or inserting parsed fragments, use replace_with() rather than string concatenation to preserve tree integrity.
  • Use soup.prettify() only for debugging/inspection — it changes whitespace and shouldn't be used for round-tripping markup.
  • Don't let autodetection choose the parser — different parsers produce different trees for invalid markup, breaking reproducibility.
  • Don't assume tag['attr'] is safe; it raises KeyError if missing. Use .get().
  • Don't call .string on a tag containing multiple children — it returns None; use .get_text() instead.
  • Don't keep NavigableString objects around outside Beautiful Soup without casting to str() — causes memory bloat.
  • Don't forget class_ (with trailing underscore) when searching by CSS class — class is a reserved keyword in Python.
  • Don't mutate a tag's .contents list while iterating over it; collect matches first, then modify.
  • Don't use html.parser on badly broken HTML expecting browser-like recovery — use html5lib for that.
0
Grade A-AI Skill Framework
Scorecard
Criteria Breakdown
Quick Start
15/15
Workflow
15/15
Examples
17/20
Completeness
17/20
Format
14/15
Conciseness
14/15