$ pip install beautifulsoup4The package isbeautifulsoup4; you import it asbs4.$ pip install lxml html5libOptional faster / more-lenient parsers (see card 02).from bs4 import BeautifulSoup★The one class you need to start.from bs4 import Tag, NavigableString, CommentImport the node types forisinstancechecks while walking.import bs4; bs4.__version__Check the version, e.g.'4.14.3'.
soup = BeautifulSoup(html, "html.parser")★stdlib, no deps, decent — the safe default.BeautifulSoup(html, "lxml")★Fastest; needspip install lxml. Great for big pages.BeautifulSoup(html, "html5lib")Browser-lenient, fixes broken markup like a browser; slowest.BeautifulSoup(xml, "lxml-xml")Parse XML (or passfeatures="xml").BeautifulSoup(open("page.html"), "lxml")Feed a file handle directly, or a str / bytes.# always name the parsergotchaOmitting it triggersGuessedAtParserWarning& can change behaviour across machines.
bs4.BeautifulSoupThe whole document / root. Behaves like a Tag.bs4.Tag★An element — has.name,.attrs, children. What you search & navigate.bs4.NavigableStringText inside a tag — astrsubclass, so string methods work.bs4.Comment<!-- -->; a special NavigableString (also CData, Doctype).isinstance(x, bs4.Tag)Test node type when walking mixed children.x.__class__.__name__All four share thePageElementbase — hence common navigation.
tag.name★The tag's name, e.g."a". Assignable to rename.tag.attrsAll attributes as a dict:{"href": "/x", "id": "c1"}.tag["href"]★One attribute value.KeyErrorif missing.tag.get("href")★Safe access — returnsNoneif absent.tag.has_attr("href")Boolean presence check — cleaner thanin tag.attrs.tag.contents · str(tag)Direct children as a list; the tag rendered back to HTML.
soup.find_all("a")★Every matching tag — a list (empty if none).soup.find("a")★First match only, orNone. Same assoup.a.soup("a")Calling the soup isfind_all— a handy shortcut.find_all(["h1", "h2"])A list matches any of several tag names.find_all(re.compile("^h[1-6]$"))A regex matches tag names by pattern.find_all(string=True)Return NavigableStrings instead of Tags — grab raw text nodes.find_all("a", limit=5, recursive=False)Cap results;recursive=False= direct children only.
find_all("div", class_="lead")★class_(trailing underscore) —classis reserved.find_all("a", class_="btn", id="go")★Stack keyword filters — all must match (AND).find_all(attrs={"data-id": "7"})★Useattrs=for hyphenated / reserved names.find_all("a", href=re.compile("^/docs"))Regex-match an attribute value.find_all(string=re.compile("Sale"))string=filters by text; takes a str, regex, list, orTrue.class_="a b" vs class_="b a"gotchaMulti-class match is order-sensitive; useselect("div.a.b")instead.
find_all(lambda t: t.name=="a" andPass a function — it receives each Tag, returns True to keep.not t.has_attr("id"))★e.g. links that lack anid— logic no kwarg can express.find_all(lambda t: len(t.get("class", [])) > 2)Match tags with more than two CSS classes.def has_price(t): return "$" in t.get_text()Named functions read better for complex rules.find_all(True)TheTruefilter matches every tag — useful with a string/attr filter.find_all(id=lambda v: v and v.startswith("nav"))A function can also filter a single attribute's value.
soup.select("div.content a")★CSS selector → list of tags. Familiar if you know CSS.soup.select_one("h1#title")★First match orNone— thefindof the CSS world.select("ul > li")Child combinator (direct children only).select("p.lead.intro")★Multiple classes the easy way (vs order-sensitiveclass_).select("li:nth-of-type(2)")Pseudo-classes via the bundledsoupsieveengine.table.select("tr td")Callselecton any Tag to scope the search to its subtree.
select("a[href]")★Attribute presence — only<a>that actually have an href.select('a[href^="https"]')★^=starts-with — external links.select('a[href$=".pdf"]')$=ends-with — PDF links.select('a[href*="/blog/"]')*=contains — substring anywhere in the value.select("h2 ~ p") · select("label + input")General (~) & adjacent (+) sibling combinators.select("div:has(> img)"):has()— parents that contain a match.select("p:-soup-contains('Sale')")soupsieve's text selector (the old:contains).
tag.find_parent("div")Nearest ancestor matching;find_parentsfor all.tag.find_next_sibling("td")★Next sibling tag — skips whitespace strings (see card 12).tag.find_previous_sibling(…)The sibling before it.tag.find_next("p") · find_all_next()Anything later in the document (parse order, any depth).tag.find_previous(…) · find_all_previous()Anything earlier in the document.
next_sibling → often '\n'gotchaWhitespace between tags is a NavigableString sibling — usefind_next_sibling().
tag.get_text() · tag.text★All text under the tag, concatenated..textis the property alias.tag.get_text(separator=" ", strip=True)★Join pieces with a separator & trim whitespace — clean output.tag.stringText only if the tag has exactly one string child, elseNone.list(tag.stripped_strings)Each non-empty text chunk as its own list item.tag.get_text(types=(NavigableString,))Restrict which string types count — e.g. skip Comment / CData text..string is None here?gotchaMultiple children →.stringisNone. Reach forget_text().
a["href"]★Direct access — raisesKeyErrorif the attr is absent.a.get("href", "")★Safe access with a default — the scraping-friendly form.img["src"] · a["data-id"]Any attribute by name, includingdata-*.tag["class"] → ["a", "b"]gotchaclassis multi-valued — you get a list, not a string. Alsorel,accept-charset." ".join(tag.get("class", []))Re-join a multi-valued attribute into a string.
tag.name = "div"; tag["class"] = "new"Rename an element; set or (del tag["id"]) delete attributes.tag.string = "new text"Replace all contents with a single string.new = soup.new_tag("a", href="/x")★Build a fresh tag (then set.string/ append it).soup.new_string("hello")Build a standalone text node.tag.append(x) · tag.extend([a, b]) · tag.insert(0, x)Add one / many children at the end, or at an index.tag.insert_before(x) · tag.insert_after(x)Place a node as a sibling.tag.smooth()Consolidate adjacent NavigableStrings after edits.
tag.decompose()★Destroy the tag & its contents in place. Checktag.decomposed.removed = tag.extract()Remove from the tree but return it for reuse.tag.clear()Empty the tag's contents, keep the tag.tag.replace_with(new)Swap this node for another.tag.unwrap()Remove the tag, keep its children (strip a wrapper).tag.wrap(soup.new_tag("div"))Surround a node with a new parent.
print(soup.prettify())★Indented, human-readable HTML — great for debugging.str(soup) · str(tag)★Compact HTML for the whole tree or one node.soup.encode("utf-8")Serialize to bytes in a chosen encoding.tag.decode(formatter="html")Formatters:"minimal"(default),"html","html5",None, or a function.from bs4.formatter import HTMLFormatterprettify(formatter=HTMLFormatter(indent=2))to control indent width.formatter=NoneunsafeEmits no entity escaping — risky if the HTML is untrusted.
BeautifulSoup(resp.content, "lxml")★Pass bytes so BS4 can sniff the encoding itself.BeautifulSoup(data, from_encoding="latin-1")Force an input encoding when detection is wrong.exclude_encodings=["utf-8"]Rule out encodings you know are wrong to help the sniffer.soup.original_encodingWhat BS4 decided the input encoding was.from bs4 import UnicodeDammitUnicodeDammit(data).unicode_markup— standalone encoding guesser.
rows = [[c.get_text(strip=True)Walk a table into a list of row-lists…for c in tr(["td","th"])] for tr in soup.select("table tr")]★…tr([...])istr.find_all([...]).head, *body = rowsSplit header row from data rows.[dict(zip(head, r)) for r in body]★Zip each row against the header → list of dicts.import json; s = soup.find("script",Structured data is often embedded as JSON-LD…type="application/ld+json"); json.loads(s.string)★…pull & parse it in two lines.import pandas; pandas.read_html(str(table))For big tables, hand the HTML to pandas.
soup = BeautifulSoup(requests.get(url).content, "lxml")★Fetch withrequests, parse with BS4 — they're a pair.from urllib.parse import urljoin★urljoin(base, a["href"])→ turn relative links absolute.el = soup.select_one(".price"); el.get_text() if el else None★Always guard — missing elements returnNone, not empty.for s in soup("script"): s.decompose()Strip<script>/<style>before extracting clean page text.# BS4 can't run JavaScriptkeyIt sees only the initial HTML. For JS-rendered pages use Selenium / Playwright, then feed.page_sourceto BS4.# be polite: rate-limit, honour robots.txtSet a real User-Agent and add delays between requests.
[a["href"] for a in soup.select("a[href]")]★Every link on the page.[img["src"] for img in soup.find_all("img")]All image sources.soup.get_text("\n", strip=True)All visible text, one line per chunk."".join(p.find_all(string=True, recursive=False))A tag's direct text only, ignoring nested tags.
# use lxml for speed on big docs★The parser choice is the biggest lever on performance.from bs4 import SoupStrainerParse only part of a document to save time & memory.only = SoupStrainer("a", href=True)Keep just<a href>tags — strainers take the same filters asfind…BeautifulSoup(html, "lxml", parse_only=only)…the rest of the tree is never built.# pip install charset_normalizerSpeeds up & sharpens BS4's encoding detection.str(navstr) # detach textCallstr()on a NavigableString you keep, or it pins the whole tree in memory.