Quick Reference · pull data out of HTML & XML · Python

BeautifulSoup cheat sheet · bs4

Feed it messy markup and a parser turns it into a tree of Python objectsTag nodes and NavigableString text. From there you do three things: navigate the family (parents, children, siblings), search it (find_all & CSS select), and extract what you want (.get_text(), attributes). It doesn’t fetch pages — pair it with requests for that.

setup / parsers objects & tags search: find · select navigate the tree modify the tree extract & output gotcha most common

Introspected from bs4 4.14 & cross-checked across: crummy.com/…/bs4/doc (official) · Real Python · ScrapeOps · DataCamp · tutorialspoint · Soup Sieve (CSS) docs

Markup → a parser builds a tree → navigate, search, extract
BAND A · THE PIPELINE Raw markup a str or bytes of HTML / XML <div><p>Hi <b>there</b></p></div> often requests(url).content Parser html.parser (stdlib) lxml (fast) html5lib (lenient) BeautifulSoup(html, …) Parse tree html head body title p a "Hi" Tag nodes (amber) · text = NavigableString (green) … then you do 4 things SEARCH find/select NAVIGATE family EXTRACT text/attrs MODIFY edit/build soup.find_all("a") · soup.select("a.cls") tag.parent · tag.get_text(strip=True) tag["href"] · tag.decompose() BAND B · THE FOUR OBJECT TYPES YOU’LL MEET IN THE TREE BeautifulSoup the whole parsed document (the root; acts like a Tag) soup = BeautifulSoup(html,     "html.parser") it contains Tags, which contain more Tags and strings… Tag an element: <a href="/x">…</a> .name → "a" .attrs → {"href": "/x"} tag["href"] → "/x" .contents / .children has parents, children, siblings — the searchable, navigable node NavigableString the text inside a tag it IS a Python str subclass "Hello world" no .contents, no find() — a leaf, not a branch get it via .string / .strings / .get_text() Comment & friends: CData, Doctype, Declaration <!-- like this --> special NavigableString subclasses — text that isn’t page content isinstance(s, Comment)
quickstart.py — the canonical requests + BeautifulSoup combo
import requests
from bs4 import BeautifulSoup

html = requests.get("https://example.com").content   # bytes → let BS4 detect encoding
soup = BeautifulSoup(html, "html.parser")             # always name a parser

# SEARCH — CSS selector returns a list
for a in soup.select("a[href]"):
    print(a.get_text(strip=True), "→", a["href"])   # extract text + attribute

# find() returns ONE tag or None (note class_, not class)
h = soup.find("h1", class_="headline")
print(h.get_text(strip=True) if h else "no headline")
01Setup & Importinstall once
02Make the Soup & Parserspick one
03The Four Object Typeswhat's in the tree
04Inspect a Tagname · attrs
05find & find_allthe workhorse
06Search by Attributes & Textfilter the match
07CSS Selectors · selectsoupsieve
08Directional Searchfind near a tag
09Navigate Downinto children
10Navigate Up & Sidewaysparents · siblings
11Extract Textget the words out
12Extract Attributeshref · src · class
13Modify · Edit & Buildchange the tree
14Modify · Delete & Replaceremove nodes
15Output & Formattingtree → string
16Encoding & Unicodebytes in, str out
17Common Recipescopy-paste
18Performance & SoupStrainergo faster

Four pictures that make BeautifulSoup click

How markup becomes a tree, how you move around it, the two ways to search, and what the different text accessors actually return.

1 · From markup to tree

Each tag becomes a Tag node; the text between tags becomes a NavigableString leaf.

<div class="c"> <p>Hi <b>there</b> </p> </div> div p "Hi" b "there" Tag NavigableString div.get_text() → "Hithere" div.b.string → "there"

2 · Moving around the family

Relations are relative to a node. Beware: next_element descends into children; next_sibling stays at the same level (and is often whitespace).

div p (you) p "Hi" b .parent .children .next_sibling .next_element (into b!) ⚠ .next_sibling is often the "\n" between tags → use find_next_sibling()

3 · find_all vs select — same goal

Two syntaxes to reach the same tags: keyword filters, or a CSS selector string.

find_all( … ) name find_all("a", class_="btn", attrs={"data-x":"1"}) Pythonic keywords · regex · funcs select( … ) one CSS string select( "a.btn[data-x='1']" ) combinators · :nth · > ~ + a list of Tags first-only twins: find() · select_one() find() → Tag or None  |  find_all() → [] if none

4 · .string vs .get_text() vs .stripped_strings

For <p>Hello <b>world</b></p> the three text accessors give three different results.

<p>Hello <b>world</b></p> .string None 2+ children → ambiguous .get_text() "Hello world" all descendant text joined .stripped_strings ["Hello",  "world"] each chunk, trimmed Rule of thumb reach for get_text(strip=True) — .string only works on a single-child tag

Worth memorizing

class_ not classfind_all("div", class_="x")class is a reserved word
.string can be Nonea tag with 2+ children → None; use get_text()
class → a listtag["class"] returns ["a","b"], not a string
name the parserBeautifulSoup(html, "html.parser") — else a warning + drift
parsers differlxml fast · html.parser stdlib · html5lib browser-lenient
whitespace siblings.next_sibling is often "\n" — use find_next_sibling()
find vs find_allfind → Tag or None · find_all → list (maybe empty)
soup("a") == find_alland soup.a == soup.find("a") (first only)
pass .contentgive BS4 bytes (not .text) so it can sniff encoding
select → listselect_one() is the first-only twin, like find
string=search by text (the old text= kwarg, renamed)
tag["x"] vs .getbracket raises KeyError; .get("x") returns None
decompose vs extractdestroy in place vs remove-and-return; clear() empties
attrs={} for nameshyphenated / reserved attrs: attrs={"data-id": 7}
it doesn't fetchno HTTP — pair with requests / httpx
next_element ≠ next_siblingelement = parse order (descends); sibling = same level