AI Skill Report Card
Web Scraping with Beautiful Soup
Quick Start15 / 15
Pythonfrom 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
Workflow15 / 15
Progress checklist for a typical scraping task:
- Choose a parser (
lxmlfor speed/leniency,html.parserfor no dependencies,html5libfor 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 likesoup.div.p) or search (find,find_all, CSSselect) - Extract data (
.string,.get_text(),tag['attr'],tag.get('attr')) - Modify if needed (
.replace_with(),.decompose(),.unwrap(), attribute assignment) - Serialize back with
str(soup)orsoup.prettify()if output is required
1. Pick the right parser
| Parser | Call | When to use |
|---|---|---|
html.parser | BeautifulSoup(markup, "html.parser") | stdlib only, decent speed |
lxml | BeautifulSoup(markup, "lxml") | default recommendation — fastest |
lxml-xml / xml | BeautifulSoup(markup, "xml") | parsing actual XML |
html5lib | BeautifulSoup(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_allfor 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
Pythontag.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
Pythontag.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
Examples17 / 20
Example 1 — Extract all links:
Input: HTML page with multiple <a> tags.
Output:
Pythonlinks = [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:
Pythontable = 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:
Pythonfor 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:
Pythonsoup.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
Best Practices
- Always specify the parser explicitly:
BeautifulSoup(markup, "lxml"). - Use
.get(attr)instead oftag[attr]when the attribute might not exist. - Convert
NavigableStringtostr()before storing long-term — it otherwise keeps the whole parse tree alive in memory. - Prefer
soup.select()for complex CSS-like queries; preferfind_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.
Common Pitfalls
- 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 raisesKeyErrorif missing. Use.get(). - Don't call
.stringon a tag containing multiple children — it returnsNone; use.get_text()instead. - Don't keep
NavigableStringobjects around outside Beautiful Soup without casting tostr()— causes memory bloat. - Don't forget
class_(with trailing underscore) when searching by CSS class —classis a reserved keyword in Python. - Don't mutate a tag's
.contentslist while iterating over it; collect matches first, then modify. - Don't use
html.parseron badly broken HTML expecting browser-like recovery — usehtml5libfor that.