DOMParser: извлечение данных из HTML и XML без опасной вставки
Автор: Казачкин Даниил Михайлович · Обновлено
DOMParser превращает строку HTML или XML в отдельный документ, по которому можно выполнять обычные DOM-запросы. Разбор синтаксиса помогает извлекать данные, но не делает произвольную разметку безопасной для вставки в действующую страницу.
Среда и безопасная граница упражнения
Сохраните основной блок в parser-demo.html и откройте в современном браузере. Он использует заранее заданную учебную строку без внешних ресурсов, не получает данные из сети и выводит только текст в самостоятельно созданные элементы. В Node.js глобального браузерного DOMParser обычно нет; для этого урока нужна именно браузерная среда.
Наша задача — извлечь названия и продолжительность занятий из экспортированного фрагмента. Мы не собираемся показывать исходную вёрстку со всеми её атрибутами. Такой контракт существенно проще: на выходе нужны обычные данные, а интерфейс создаётся нашим кодом. Это уменьшает поверхность ошибки, но не превращает HTML-парсер в универсальный изолятор недоверенного ввода.
Полный пример: из разметки в данные и текст
<!doctype html>
<html lang="ru">
<meta charset="utf-8">
<title>Импорт плана</title>
<h1>Импорт плана</h1>
<ul id="result"></ul>
<script type="module">
const sample = '<article data-minutes="15"><h2>Циклы & условия</h2></article>' +
'<article data-minutes="25"><h2>Объекты</h2></article>' +
'<article data-minutes="oops"><h2>Ошибка</h2></article>';
const parsed = new DOMParser().parseFromString(sample, 'text/html');
const lessons = [];
for (const article of parsed.querySelectorAll('article[data-minutes]')) {
const title = article.querySelector('h2')?.textContent.trim() ?? '';
const rawMinutes = article.getAttribute('data-minutes');
const minutes = Number(rawMinutes);
if (!title || !rawMinutes?.trim() ||
!Number.isSafeInteger(minutes) || minutes <= 0) continue;
lessons.push({ title, minutes });
}
const output = document.querySelector('#result');
for (const lesson of lessons) {
const item = document.createElement('li');
item.textContent = lesson.title + ' — ' + lesson.minutes + ' мин';
output.append(item);
}
console.log(JSON.stringify(lessons));
</script>
</html>На странице появятся два пункта: Циклы & условия — 15 мин и Объекты — 25 мин. Консоль покажет [{"title":"Циклы & условия","minutes":15},{"title":"Объекты","minutes":25}]. Некорректная продолжительность исключена. HTML-сущность преобразовалась в символ амперсанда при разборе, а при выводе осталась обычным текстом. Исходные article не перемещаются в активную страницу.
В учебном примере неподходящие записи пропускаются. Для реального импорта полезнее вернуть два набора: принятые записи и ошибки с индексом исходной записи. Иначе пользователь не узнает, что часть плана потеряна. Политика ошибок относится к контракту импорта и должна быть определена отдельно от механизма DOMParser.
HTML и XML используют разные правила
Аргумент типа содержимого определяет режим разбора. text/html строит HTML-документ, добавляет необходимую структуру и исправляет многие некорректные сочетания тегов по правилам HTML. Поэтому успешное получение документа не доказывает, что исходная строка строго соответствует вашей схеме. Проверяйте наличие ожидаемых элементов, обязательные поля и ограничения количества записей после разбора.
XML требует корректной вложенности и закрытия элементов. При синтаксической ошибке документ содержит узел parsererror; сам факт возвращения Document не означает успех. Режим application/xml подходит для структурированного XML-экспорта, а не для попытки строже разобрать обычный HTML. Пространства имён также важны: для формата с namespace выбирайте узлы с учётом его URI, а не только видимого префикса.
Если источник контролируется вашим приложением и содержит только данные, JSON может оказаться проще и дешевле. DOMParser полезен, когда вход действительно представляет документ и его структуру нужно использовать. Не превращайте всё в HTML только ради удобных селекторов: это добавляет правила восстановления разметки и лишние вопросы доверия.
Инертный документ не является санитайзером
В отдельно разобранном HTML скрипты и обработчики не выполняются как в обычной активной странице. Однако документ может содержать опасные атрибуты, URL и другие конструкции; перенос необработанных узлов в активный DOM способен создать XSS. Удалить только теги script недостаточно: опасное поведение не ограничивается одним видом элемента.
Кроме того, разбор HTML не следует считать гарантией отсутствия сетевых эффектов: некоторые ресурсы, например изображения или iframe, могут загружаться даже в таком документе. Поэтому пример использует фиксированную строку без ресурсов. Для полностью недоверенного большого документа нужна продуманная изоляция или предварительная обработка подходящим инструментом. Нельзя обещать «безопасный офлайн-парсер» только на основании того, что дерево ещё не добавлено в страницу.
Если задача требует сохранить форматирование внешнего HTML, используйте поддерживаемый санитайзер с конкретным набором разрешённых элементов и атрибутов. Trusted Types помогают обязать опасные точки вставки проходить через проверенную политику, но не выполняют очистку сами. Политика, которая просто возвращает входную строку, не устраняет угрозу. При включённом требовании Trusted Types обычная строка в parseFromString может быть отклонена; интеграция должна использовать существующую корректную политику приложения.
URL тоже являются данными с правилами
Название ссылки можно выводить через textContent, но значение href требует отдельной проверки. Строка, похожая на путь, может оказаться абсолютным URL другой схемы или другого источника. Разбирайте её через new URL(raw, trustedBase), затем проверяйте протокол, источник и при необходимости разрешённый путь. Базовый адрес должен задаваться вашим контрактом, а не извлекаться из недоверенного тега base.
Следующий блок является самостоятельной арифметикой URL и работает как в браузере, так и в современном Node.js. Он только возвращает строки, не выполняя переходы или запросы.
function localHttpsUrl(raw) {
try {
const url = new URL(raw, 'https://academy.example/exports/');
if (url.protocol !== 'https:' || url.origin !== 'https://academy.example' ||
url.username || url.password) return null;
return url.href;
} catch {
return null;
}
}
console.log(localHttpsUrl('../lessons/arrays'));
console.log(localHttpsUrl('javascript:alert(1)'));
console.log(localHttpsUrl('//outside.example/page'));Ожидаются https://academy.example/lessons/arrays, null, null. Это ограниченная политика для учебного сайта, а не универсальная проверка всех безопасных URL. Если проект разрешает внешние ссылки, его правила будут другими. Проверка допустимости адреса и решение, следует ли пользователю доверять содержимому сайта, также не одно и то же.
Практика: проверить ошибочный XML
В консоли браузера разберите строку <plan><lesson></plan> с типом application/xml, найдите parsererror и не продолжайте импорт при его наличии. Затем исправьте строку на <plan><lesson minutes="15"/></plan>. Ожидается ошибка в первом случае и один элемент lesson во втором. Проверку обязательного атрибута выполняйте отдельно: корректный XML может всё равно нарушать предметную схему.
Частые вопросы
Почему парсер добавил html, head и body?
HTML-разбор строит полноценный документ даже из фрагмента. Ищите данные внутри полученной структуры, а не сравнивайте её сериализацию посимвольно с входом. Для точного сохранения исходного форматирования DOM не является подходящим представлением.
Можно ли вставить parsed.body через append?
Для собственной доверенной разметки это технически возможно, но не соответствует безопасной границе извлечения данных из этого урока. Для внешнего содержимого сначала требуется корректная очистка и политика вставки; сам DOMParser её не предоставляет.
Как защититься от слишком большого импорта?
Ограничьте размер входа до разбора и число извлекаемых записей после него, возвращайте понятную ошибку и измерьте расход памяти. Синхронный разбор большого документа может блокировать интерфейс; один дополнительный await не делает его фоновым.