Технологии и защита 6 мин чтения

Парсер DOM: подробный разбор

Что такое DOM-парсер и как он устроен: построение дерева документа, навигация по узлам и отличия от потоковых и регэксп-подходов.

КP
Команда Parsing.agency
Сбор данных под задачи бизнеса
Опубликовано: 4 февраля 2025

Что такое DOM

DOM (Document Object Model) — это представление HTML- или XML-документа в виде дерева объектов. Каждый тег, атрибут и текстовый фрагмент становится узлом (node), а вложенность тегов задаёт иерархию «родитель → потомок».

Парсер DOM — это компонент, который читает разметку (строку или поток байтов) и строит из неё это дерево в памяти. После построения с деревом можно работать программно: искать элементы, читать атрибуты, изменять, удалять и добавлять узлы, сериализовать обратно в текст.

Как он работает

Парсинг проходит в два основных этапа:

  1. Токенизация — поток символов разбивается на токены (открывающий тег, закрывающий тег, текст, комментарий и т. д.).
  2. Построение дерева — токены собираются в иерархическую структуру с учётом правил вложенности. Для HTML это правила из спецификации WHATWG, которые в том числе «чинят» некорректную разметку (незакрытые теги, неправильную вложенность).

DOM-парсинг против SAX

Существует два принципиально разных подхода к разбору документа.

Подход Память Доступ Когда использовать
DOM Весь документ в памяти Случайный доступ к любому узлу Скрапинг, обработка конфигов, большинство задач
SAX / потоковый Почти не расходуется Только последовательный, по событиям Очень большие XML (сотни МБ — гигабайты)

DOM загружает документ целиком и даёт удобный случайный доступ, но требует памяти, пропорциональной размеру документа.

SAX не строит дерево, а генерирует события («начался тег», «закончился тег», «встретился текст») по мере чтения. Памяти почти не нужно, но писать код сложнее и нет случайного доступа.

Большинство популярных библиотек ниже — это DOM-парсеры.

Способы поиска по дереву

После построения дерева элементы обычно ищут двумя способами.

CSS-селекторы — те же, что в вёрстке: div.post > a, #main .title, ul li:first-child. Привычны фронтендерам, читаемы.

XPath — язык запросов к дереву, мощнее CSS: //div[@class="post"]/a/@href. Поддерживает условия, переход к родителю (..), поиск по тексту (//a[contains(text(),"Купить")]). Чаще используется в XML, но многие HTML-библиотеки тоже его поддерживают.

Правило выбора: если хватает CSS — берите CSS, он читаемее. XPath нужен, когда требуется навигация к родителю/предку, выбор по тексту или сложные условия.


Примеры на разных языках

Во всех примерах решается одна задача: найти ссылки внутри div.post и вывести их текст и href.

Python

Самые популярные библиотеки — BeautifulSoup (простой, прощающий) и lxml (быстрый, на базе C-библиотеки libxml2, с поддержкой XPath).

BeautifulSoup

python
from bs4 import BeautifulSoup

html = """
<html><body>
  <div class="post">
    <h2>Заголовок</h2>
    <a href="https://example.com">Ссылка</a>
  </div>
</body></html>
"""

soup = BeautifulSoup(html, "lxml")   # парсер можно заменить на "html.parser"

# Поиск по тегу
print(soup.find("h2").text)

# CSS-селекторы
for a in soup.select("div.post a"):
    print(a.text, a.get("href"))

lxml с 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 также есть встроенные html.parser и xml.dom.minidom, не требующие установки.

JavaScript / Node.js

В браузере DOM-парсер встроен — DOMParser и 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);
});

В Node.js используют Cheerio (лёгкий, jQuery-подобный API) или jsdom (полная эмуляция DOM).

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

Стандарт де-факто — jsoup, очень удобный для HTML с CSS-селекторами.

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"));
}

Для строгого XML в JDK есть встроенный JAXP (javax.xml.parsers.DocumentBuilder) — DOM-парсер из коробки, а также SAX/StAX для потокового разбора.

PHP

Встроенный класс DOMDocument с поддержкой XPath

php
$dom = new DOMDocument();
@$dom->loadHTML($html);          // @ подавляет предупреждения о «грязном» HTML
$xpath = new DOMXPath($dom);

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

Альтернатива с CSS-селекторами — Symfony DomCrawler

C# / .NET

HtmlAgilityPack — классика, работает через 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 — более современный, со спецификационно-корректным парсингом и CSS-селекторами

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 даёт jQuery-подобный 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)
})

Под капотом goquery использует низкоуровневый официальный пакет golang.org/x/net/html, которым можно пользоваться и напрямую.

Ruby

Nokogiri — поддерживает и CSS, и XPath

ruby
require "nokogiri"

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

Сводная таблица библиотек

Язык Библиотека Селекторы Особенности
Python BeautifulSoup CSS Простая, прощает ошибки разметки
Python lxml CSS + XPath Быстрая (C-бэкенд libxml2)
JS (браузер) DOMParser CSS Встроена в браузер
Node.js Cheerio CSS Лёгкая, jQuery-подобная
Node.js jsdom CSS Полная эмуляция DOM
Java jsoup CSS Стандарт де-факто для HTML
PHP DOMDocument XPath Встроена в PHP
C# HtmlAgilityPack XPath Классика экосистемы .NET
C# AngleSharp CSS Современный, корректный парсинг
Go goquery CSS jQuery-подобная
Ruby Nokogiri CSS + XPath Быстрая (C-бэкенд)

Практические замечания

HTML «грязный» по своей природе. Реальная разметка часто содержит незакрытые теги и ошибки вложенности. HTML-ориентированные парсеры (BeautifulSoup, jsoup, AngleSharp, Nokogiri в режиме HTML) их терпят и исправляют. Строгие XML-парсеры на таком вводе упадут — для XML нужна валидная разметка.

Парсер ≠ браузер. Перечисленные библиотеки разбирают только исходный HTML, пришедший с сервера. Если контент подгружается JavaScript-ом уже в браузере, простой DOM-парсер его не увидит. Для таких сайтов нужны headless-браузеры:

Они сначала исполняют скрипты, а потом отдают готовый DOM.

Производительность. Парсеры на C (lxml, libxml2-бэкенд Nokogiri) на порядок быстрее чисто-питоновских/руби-реализаций. Для огромных XML выбирайте потоковый SAX/StAX вместо DOM, чтобы не упереться в память.

Выбор селекторов. Если хватает CSS — берите CSS, он читаемее. XPath нужен для навигации к родителю/предку, выбора по тексту или сложных условий.