Guides & Basics 6 min read

DOM Parser: How HTML Becomes a Tree of Nodes

What a DOM parser does: turning HTML or XML markup into a node tree you can traverse and query. Compare DOM vs streaming parsers and see them work in code.

ST
Scraping.Pro Team
Data collection for business needs
Published: 1 March 2026

What the DOM is

The DOM (Document Object Model) is a representation of an HTML or XML document as a tree of objects. Every tag, attribute, and text fragment becomes a node, and the nesting of tags defines the parent → child hierarchy.

A DOM parser is the component that reads markup (a string or a byte stream) and builds that tree in memory. Once it's built, you can work with the tree programmatically: search for elements, read attributes, modify, delete, and add nodes, and serialize it back to text.

How it works

Parsing happens in two main stages:

  1. Tokenization — the stream of characters is broken into tokens (opening tag, closing tag, text, comment, and so on).
  2. Tree construction — tokens are assembled into a hierarchical structure according to nesting rules. For HTML those are the WHATWG spec rules, which also "repair" malformed markup (unclosed tags, incorrect nesting).

DOM parsing vs. SAX

There are two fundamentally different approaches to parsing a document.

Approach Memory Access When to use
DOM Whole document in memory Random access to any node Scraping, config processing, most tasks
SAX / streaming Almost none Sequential only, event-driven Very large XML (hundreds of MB to gigabytes)

DOM loads the whole document and gives you convenient random access, but it needs memory proportional to the document's size.

SAX builds no tree; instead it emits events ("tag started," "tag ended," "text encountered") as it reads. It needs almost no memory, but the code is harder to write and there's no random access.

Most of the popular libraries below are DOM parsers.

Ways to search the tree

Once the tree is built, elements are usually found in one of two ways.

CSS selectors — the same ones you use in stylesheets: div.post > a, #main .title, ul li:first-child. Familiar to front-end developers and easy to read.

XPath — a query language for the tree, more powerful than CSS: //div[@class="post"]/a/@href. It supports conditions, navigating to a parent (..), and searching by text (//a[contains(text(),"Buy")]). More common in XML, but many HTML libraries support it too.

Rule of thumb: if CSS is enough, use CSS — it's more readable. Reach for XPath when you need to navigate to a parent/ancestor, select by text, or express complex conditions.


Examples in different languages

Every example solves the same task: find the links inside div.post and print their text and href.

Python

The most popular libraries are BeautifulSoup (simple, forgiving) and lxml (fast, built on the C library libxml2, with XPath support).

BeautifulSoup

python
from bs4 import BeautifulSoup

html = """
<html><body>
  <div class="post">
    <h2>Heading</h2>
    <a href="https://example.com">Link</a>
  </div>
</body></html>
"""

soup = BeautifulSoup(html, "lxml")   # the parser can be swapped for "html.parser"

# Search by tag
print(soup.find("h2").text)

# CSS selectors
for a in soup.select("div.post a"):
    print(a.text, a.get("href"))

lxml with XPath

python
from lxml import html as lxml_html

tree = lxml_html.fromstring(html)
hrefs = tree.xpath('//div[@class="post"]/a/@href')
print(hrefs)  # ['https://example.com']

Python's standard library also includes the built-in html.parser and xml.dom.minidom, which need no installation.

JavaScript / Node.js

In the browser, a DOM parser is built in — DOMParser and querySelector:

javascript
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");

doc.querySelectorAll("div.post a").forEach(a => {
  console.log(a.textContent, a.href);
});

In Node.js people use Cheerio (lightweight, jQuery-like API) or jsdom (full DOM emulation).

javascript
import * as cheerio from "cheerio";

const $ = cheerio.load(html);
$("div.post a").each((i, el) => {
  console.log($(el).text(), $(el).attr("href"));
});

jsdom

javascript
import { JSDOM } from "jsdom";

const dom = new JSDOM(html);
dom.window.document.querySelectorAll("div.post a")
  .forEach(a => console.log(a.textContent, a.href));

Java

The de facto standard is jsoup, very convenient for HTML with CSS selectors.

java
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;

Document doc = Jsoup.parse(html);
for (Element link : doc.select("div.post a")) {
    System.out.println(link.text() + " " + link.attr("href"));
}

For strict XML, the JDK ships JAXP (javax.xml.parsers.DocumentBuilder) — a DOM parser out of the box — plus SAX/StAX for streaming.

PHP

The built-in DOMDocument class with XPath support:

php
$dom = new DOMDocument();
@$dom->loadHTML($html);          // @ suppresses warnings about "dirty" HTML
$xpath = new DOMXPath($dom);

foreach ($xpath->query('//div[@class="post"]/a') as $node) {
    echo $node->textContent . ' ' . $node->getAttribute('href') . PHP_EOL;
}

A CSS-selector alternative is Symfony DomCrawler.

C# / .NET

HtmlAgilityPack — the classic, works through XPath:

c#
var doc = new HtmlAgilityPack.HtmlDocument();
doc.LoadHtml(html);

foreach (var link in doc.DocumentNode.SelectNodes("//div[@class='post']/a"))
{
    Console.WriteLine(link.InnerText + " " + link.GetAttributeValue("href", ""));
}

AngleSharp — more modern, with spec-correct parsing and CSS selectors:

c#
var context = AngleSharp.BrowsingContext.New(AngleSharp.Configuration.Default);
var doc = await context.OpenAsync(req => req.Content(html));

foreach (var a in doc.QuerySelectorAll("div.post a"))
    Console.WriteLine(a.TextContent + " " + a.GetAttribute("href"));

Go

goquery gives you a jQuery-like API:

go
import (
    "fmt"
    "strings"
    "github.com/PuerkitoBio/goquery"
)

doc, _ := goquery.NewDocumentFromReader(strings.NewReader(html))
doc.Find("div.post a").Each(func(i int, s *goquery.Selection) {
    href, _ := s.Attr("href")
    fmt.Println(s.Text(), href)
})

Under the hood, goquery uses the low-level official package golang.org/x/net/html, which you can also use directly.

Ruby

Nokogiri — supports both CSS and XPath:

ruby
require "nokogiri"

doc = Nokogiri::HTML(html)
doc.css("div.post a").each do |link|
  puts "#{link.text} #{link['href']}"
end

Library reference table

Language Library Selectors Notes
Python BeautifulSoup CSS Simple, forgives markup errors
Python lxml CSS + XPath Fast (libxml2 C backend)
JS (browser) DOMParser CSS Built into the browser
Node.js Cheerio CSS Lightweight, jQuery-like
Node.js jsdom CSS Full DOM emulation
Java jsoup CSS De facto standard for HTML
PHP DOMDocument XPath Built into PHP
C# HtmlAgilityPack XPath .NET ecosystem classic
C# AngleSharp CSS Modern, spec-correct parsing
Go goquery CSS jQuery-like
Ruby Nokogiri CSS + XPath Fast (C backend)

Practical notes

HTML is dirty by nature. Real markup often contains unclosed tags and nesting errors. HTML-oriented parsers (BeautifulSoup, jsoup, AngleSharp, Nokogiri in HTML mode) tolerate and repair them. Strict XML parsers will fail on such input — XML requires valid markup.

A parser is not a browser. The libraries listed here only parse the source HTML that came from the server. If the content is loaded by JavaScript in the browser, a plain DOM parser won't see it. For those sites you need a headless browser:

They execute the scripts first and then hand you the finished DOM. If you're deciding between the two approaches, our guide to scraping dynamic websites walks through when a DOM parser is enough and when you truly need a browser.

Performance. C-based parsers (lxml, Nokogiri's libxml2 backend) are an order of magnitude faster than pure-Python/Ruby implementations. For huge XML, choose streaming SAX/StAX over DOM so you don't hit a memory wall.

Selector choice. If CSS is enough, use CSS — it's more readable. XPath is for navigating to a parent/ancestor, selecting by text, or complex conditions.


Choosing a parser and writing selectors is the easy 20% of a scraping project; keeping those selectors alive as sites change layout is the other 80%. If you'd rather receive structured data than maintain parsers, scraping.pro runs extraction as a done-for-you data service — you specify the fields, we handle the DOM.