Parsers compared: html.parser, lxml, html5lib

Beautiful Soup · Advanced · 12 min

Different parsers build different trees from the same broken HTML, which explains 'it works on my machine' scraping bugs.

Valid HTML parses the same everywhere. Broken HTML, which is most of the web, doesn't: each parser repairs it with its own rules.

  • html.parser (standard library): decent speed, no dependencies, lenient but not browser-accurate; doesn't add <html>/<body> wrappers.
  • lxml: C-based and the fastest; adds <html><body> around fragments; its repairs can differ from browsers.
  • html5lib: implements the HTML5 parsing algorithm, so the tree matches what a browser builds (including <head>, implicit <tbody> in tables); pure Python and slow.

Example: for <a></p>, html.parser gives <a></a>, lxml gives <html><body><a></a></body></html>, and html5lib gives <html><head></head><body><a><p></p></a></body></html>.

This matters when selectors are written against browser dev tools: the browser inserted a <tbody> that html.parser and lxml don't add, so table > tbody > tr matches nothing. Using html5lib, or selectors that skip tbody (table tr), fixes it.

XML needs lxml's XML mode: BeautifulSoup(data, "xml") (also spelled "lxml-xml"). It preserves case-sensitive tag names, handles namespaces and self-closing tags properly, which HTML parsers don't.

Choose by need: lxml for speed, html5lib for fidelity, html.parser for zero dependencies.

Example

from bs4 import BeautifulSoup

broken = "<a></p>"
for parser in ["html.parser", "lxml", "html5lib"]:
    try:
        print(f"{parser:12}", BeautifulSoup(broken, parser))
    except Exception as e:            # FeatureNotFound if not installed
        print(f"{parser:12} not installed: {type(e).__name__}")
# html.parser  <a></a>
# lxml         <html><body><a></a></body></html>
# html5lib     <html><head></head><body><a><p></p></a></body></html>

table = "<table><tr><td>1</td></tr></table>"
print(BeautifulSoup(table, "html.parser").select("table > tbody > tr"))  # []
print(BeautifulSoup(table, "html.parser").select("table tr"))  # [<tr><td>1</td></tr>]

xml = '<Feed><Item id="1"/><Item id="2"/></Feed>'
print([i["id"] for i in BeautifulSoup(xml, "xml").find_all("Item")])  # ['1', '2']

Remember

  • Parsers agree on valid HTML and differ on broken HTML
  • lxml is fastest; html5lib matches browsers; html.parser needs nothing
  • Browsers add <tbody>; html.parser and lxml don't
  • Use the 'xml' feature (lxml) for XML documents

Watch out for

  • Writing selectors with tbody copied from dev tools and getting empty results.

Check yourself

  1. A selector 'table > tbody > tr' copied from Chrome returns [] with html.parser. Likely cause?

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/#differences-between-parsers