Modifying and cleaning HTML

Beautiful Soup · Advanced · 12 min

Sanitizing content, rewriting links and preparing HTML for emails or text extraction all require editing the tree.

The tree is mutable, and changes show up when you convert it back to a string.

Removing:

  • tag.decompose() removes a tag and destroys it with its children: use it to strip <script>, <style>, ads or nav before extracting text.
  • tag.extract() removes and returns it, so you can insert it elsewhere.
  • tag.clear() empties the tag's contents but keeps the tag.

Editing:

  • Attributes: tag["href"] = "...", del tag["style"].
  • Text: tag.string = "new text" replaces all contents with that string.
  • Name: tag.name = "strong".

Building and moving:

  • soup.new_tag("a", href="/x") creates a tag (set .string for its text); tag.append(child), tag.insert(i, child), insert_before / insert_after.
  • old.replace_with(new) swaps an element.
  • tag.unwrap() replaces a tag with its children (dropping <span> wrappers while keeping text); tag.wrap(soup.new_tag("div")) does the opposite.

Don't mutate while iterating over a generator of the same tree (like .descendants); collect targets into a list first (find_all already returns one).

Beautiful Soup is not a security sanitizer: removing <script> by hand misses event-handler attributes, javascript: URLs and more. For untrusted HTML use a dedicated allow-list sanitizer such as nh3 or bleach.

Example

from bs4 import BeautifulSoup

html = """<div id="post"><script>track()</script>
<p style="color:red">Read <span>the</span> <a href="/docs">docs</a>.</p>
<div class="ad">Buy now!</div></div>"""
soup = BeautifulSoup(html, "html.parser")

for t in soup.find_all(["script", "style"]) + soup.select(".ad"):
    t.decompose()                                  # drop noise
for t in soup.find_all(style=True):
    del t["style"]                                 # strip inline styles
soup.span.unwrap()                                 # keep text, drop wrapper
a = soup.a
a["href"] = "https://example.com" + a["href"]      # absolute link
a["rel"] = "nofollow"
note = soup.new_tag("em")
note.string = " (updated)"
soup.p.append(note)

print(" ".join(str(soup).split()))
# <div id="post"> <p>Read the <a href="https://example.com/docs" rel="nofollow">docs</a>.<em> (updated)</em></p> </div>

Remember

  • decompose() deletes, extract() removes and returns
  • Edit attributes like a dict; set .string to replace contents
  • new_tag + append/insert to build; replace_with, unwrap, wrap to restructure
  • Use a real sanitizer (nh3, bleach) for untrusted HTML

Watch out for

  • Treating script-tag removal as XSS protection.
  • Modifying the tree while iterating over a live generator like .descendants.

Check yourself

  1. You want to drop <span> wrappers but keep the text inside. Which method?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.crummy.com/software/BeautifulSoup/bs4/doc/#modifying-the-tree