Как конвертировать HTML-документ в JSON: пишем рабочую программу на JavaScript
{"subtitle": "Дерзкая сериализация одного формата в другой — без последствий и проблем с законом."}
У языка гипертекстовой разметки веб-страниц HTML и формата данных JSON есть общая черта: оба они помогают структурировать информацию, хотя в веб-разработке их используют для разных задач. HTML необходим для отображения разметки сайтов, а JSON — для обмена данными с сервером.
То есть, отправляя с сервера информацию в формате JSON, мы делаем её независимой от формата отображения на устройстве пользователя: он может листать экраны нативного мобильного приложения (где не используется HTML), запрашивать данные из консольной утилиты или смотреть в браузере. Да и для отображения в браузере подчас нужна «многоэтажная» JavaScript-логика, которую в сгенерированный сервером HTML просто не упаковать.
Вообще, библиотеки для работы с JSON есть практически в любом современном языке программирования, то есть это универсальный формат. Однако сама аббревиатура JSON изначально является сокращением от JavaScript Object Notation, а потому в JS есть встроенные методы для работы с JSON. Кроме того, вид данных в последнем почти идентичен синтаксису объектов в JavaScript. То есть JavaScript идеально поддерживает свой «дочерний» формат.
Где встречается этот ваш JSON
Итак, многие веб-разработчики предпочитают выдавать по запросу с фронтенда HTML-код, упакованный в JSON, и уже в браузере конвертировать его в обычную HTML-разметку. Так работает и наш сайт — Skillbox Media (мы анализировали это в статье о парсинге данных).

Скриншот: Skillbox Media
Такой же способ подгрузки дополнительного контента реализован на платформе Stack Overflow. Для примера откроем раздел Newest Questions. Если перейти на следующую страницу (например, на страницу 2), на сайте подгрузятся новые вопросы. А если открыть инструменты разработчика и перейти во вкладку Network, среди запросов вы увидите JSON-файлы с данными о вопросах:

Скриншот: Skillbox Media
Произвести конвертацию HTML в JSON несложно, если знать особенности обоих форматов. Сначала мы кратко разберём эти особенности, а потом напишем небольшое приложение, которое будет конвертировать один формат в другой.
Кратко о разнице HTML и JSON
Вспомним, что HTML-разметка сайта состоит из элементов, которые, как правило, состоят из трёх частей: открывающий тег, закрывающий тег и контент:
<p>HTML vs. JSON</p>Внутри открывающего тега могут располагаться различные атрибуты в формате attribute="value":
<p class="zag">HTML vs. JSON</p>JSON устроен иначе: он состоит из набора пар "ключ": "значение". Ключи и значения заключены в кавычки, после ключа следует двоеточие, пары разделены запятыми и упакованы внутрь круглых скобок.
В качестве значений в JSON-разметке могут использоваться числа, строки, логические значения, объекты, массивы и null (нам далее пригодится знание этого факта, потому что мы будем создавать пример с объектами и массивами).
{
"key1": "value",
"key2": "value"
}Последняя пара не отделяется запятой — висящие запятые (trailing commas) в JSON запрещены.
Кажется, что различия ощутимые, но это лишь на первый взгляд: на деле представить HTML-код в формате JSON относительно несложно, следите за руками:
<p>Совистика — это наука о совах.</p>{
"p": "Совистика — это наука о совах."
}Любой HTML-элемент легко превращается в JSON-пару "ключ": "значение", где ключом выступает название элемента, а значением — его текстовое содержимое или вложенный элемент следующего уровня.
Теперь попробуем с помощью JavaScript сделать нечто подобное на примере одной страницы сайта.
Проводим конвертацию
Процесс упаковки данных в JSON называется сериализацией, а обратный процесс — десериализацией. Перевести HTML-разметку в JSON можно двумя способами:
- Преобразовать разметку «в лоб», запихнув весь HTML-код в одну пару "ключ": "значение" — то есть как значение ключа <html>, (как на скриншоте разметки Skillbox Media).
- Написать более сложный вариант JSON, чтобы структура JSON-строки полностью соответствовала структуре HTML-документа.
Выбор способа будет зависеть от ваших задач. Мы же в образовательных целях разберём более сложный, второй способ — это позволит нам опробовать возможности языка JavaScript и сохранить структуру страницы. Возьмём для примера следующий код:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Конвертация HTML в JSON</title>
</head>
<body>
<p class="myclass" style="margin: 1px;">Какой-то абзац.</p>
<div>
<p><span>Другой абзац (элемент 3-го уровня)</span></p>
<span>Другой абзац (элемент 2-го уровня)</span>
<span><a href="#">Какая-то ссылка (элемент 3-го уровня).</a></span>
<img src="images/test.png" alt="Тестовое изображение">
</div>
<script src="script.js"></script>
</body>
</html>
Здесь есть все базовые составляющие шаблона HTML-страницы: служебные элементы и контент в теле страницы. У некоторых элементов есть атрибуты и вложенные элементы, что тоже нужно учесть при конвертировании.
Вернёмся к типичной ситуации, как на первых скриншотах: каркас страницы статичен, а при подгрузке с сервера меняется только тело, которое и нужно скормить JSON-конвертеру. Следовательно, нас интересует содержимое элемента <body>:
<p class="myclass" style="margin: 1px;">Какой-то абзац.</p>
<div>
<p><span>Другой абзац (элемент 3-го уровня)</span></p>
<span>Другой абзац (элемент 2-го уровня)</span>
<span><a href="#">Какая-то ссылка (элемент 3-го уровня).</a></span>
<img src="images/test.png" alt="Тестовое изображение">
</div>
<script src="script.js"></script>
Сейчас на странице несколько произвольных абзацев и картинка. Наш учебный конвертер будет почти универсальным и сможет упаковать в JSON эти и любые другие HTML-элементы в теле — вплоть до третьего уровня вложенности.
Алгоритм будет следующим:
- Создадим JavaScript-объект для записи HTML-элементов, их атрибутов и содержимого.
- По очереди запишем в объект все элементы.
- Преобразуем объект в JSON-формат.
Чуть ниже мы рассмотрим указанные шаги в деталях, но для начала нужно создать два файла:
- Первый назовём «HTML в JSON.html» (по ссылке — содержимое файла на сервисе Pastebin) и поместим в него собственно страницу для преобразования.
- Второй будет называться «script.js» (по ссылке — содержимое файла на сервисе Pastebin) — в него мы вынесем написанный на JavaScript конвертер страницы в JSON.
Скрипт будет вызываться после загрузки нашей страницы, поэтому мы поместим в тело HTML-файла элемент <script> со ссылкой на JS-файл (см. первую вставку кода).
Теперь рассмотрим пошагово, как будет устроен наш конвертер в файле script.js.
function convertHTMLtoJSON() {
// Тут будет конвертер
};
// Выводим результат через секунду после загрузки исходной страницы
setTimeout(convertHTMLtoJSON, 1000);
Сначала объявим функцию с понятным названием — «конвертер HTML в JSON». В её теле будет находиться конвертер. После объявления функции заложим её вызов с задержкой в одну секунду с помощью встроенного метода setTimeout(): таким образом, после открытия HTML-файла в браузере мы сначала увидим исходную страницу, а потом она сотрётся и заменится на JSON-строку.
Теперь займёмся телом созданной функции и для красоты на каждом шаге её работы будем выводить понятные уведомления в консоль (скриншот с ними будет ниже). Начнём с этого:
// Очистка консоли для повторного запуска примера
console.clear();
// Показ в консоли названия программы
console.log('= КОНВЕРТЕР HTML В JSON =');
// Уведомление в консоль о начале работы конвертера
console.log('НАЧАТА СЕРИАЛИЗАЦИЯ HTML-КОДА.');
Результаты поиска элементов, их атрибутов и содержимого также будут сопровождаться уведомлениями, чтобы мы не пропустили ничего важного.
Мы уже упоминали, что формат JSON похож на объекты в JavaScript. Именно поэтому, следуя алгоритму, создадим объект и запишем в него HTML-элементы.
let objectToStringify = new Object(); // Создаём объект для записи элементов и их контента (в конце объект будет преобразован в JSON)
После создания объекта начинается главное: цикл перебора и записи элементов и содержимого в объект, и тут самое время поговорить о том, что конкретно мы хотим в итоге получить.
Структура JSON-результата будет такой: название элемента первого уровня, потом его атрибуты, затем его содержимое — вложенные элементы или текст. Если содержимое — это вложенные элементы, то внутри будут их названия, атрибуты и содержимое (также вложенные элементы или текст). Каждая часть структуры будет иметь порядковый номер — начиная с нуля — и указание на уровень элемента.
К примеру, первый элемент в теле нашей страницы после перевода в JSON должен принять такой вид:
{
"element0Level1 (порядковый номер 1-го элемента 1-го уровня)": [
{
"element0Level1Name (название этого элемента 1-го уровня)": "P"
},
{
"element0Level1Attributes (атрибуты этого элемента 1-го уровня)": [
{
"element0Level1attribute0 (первый атрибут этого элемента 1-го уровня)": [
{
"element0Level1attribute0Name (название первого атрибута)": "class"
},
{
"element0Level1attribute0Value (значение первого атрибута)": "myclass"
}
]
},
{
"element0Level1attribute1 (второй атрибут этого элемента 1-го уровня)": [
{
"element0Level1attribute1Name (название второго атрибута)": "style"
},
{
"element0Level1attribute1Value (значение второго атрибута)": "margin: 1px;"
}
]
}
]
},
{
"element0Level1Content (содержимое этого элемента 1-го уровня, в данном случае текст)": "Какой-то абзац."
}
],
}
Переводим на человеческий язык: «элемент 0 (то есть первый — у программистов своя математика) первого уровня, имя P, есть атрибуты. Атрибут 1 class="myclass", атрибут 2 style="margin: 1px;". Содержит текст „Какой-то абзац“».
element0Level1 — это ключ первой пары "ключ": "значение", а всё остальное — содержимое значения этой пары, которое представляет собой массив вложенных объектов — объекта с названием элемента, объекта со всеми атрибутами и объекта с содержимым.
Следующие элементы 1-го уровня будут упакованы в такие же пары, а элементы 2-го и 3-го уровней повторят ту же структуру во вложениях. Короче, у нас чередуются объекты, вложенные массивы и вложенные в них объекты.

Кадр: сериал «В Филадельфии всегда солнечно»
Теперь, когда мы представили себе результат, разберём реализацию. Цикл перебора HTML-элементов должен разобрать их на кусочки — как детальки лего. Находить составляющие элементов помогут следующие свойства:
- tagName найдёт названия элементов;
- attributes отдаст коллекцию атрибутов элемента, а обращения к attributes.name и attributes.value вернут названия и значения отдельных атрибутов;
- для получения вложенных элементов любого уровня и текста понадобятся свойства children и textContent соответственно.
На каждом из уровней вложенности HTML-элементов мы сначала будем измерять их количество, а потом перебирать с помощью цикла for.
// Оценка HTML-элементов 1-го уровня
let bodyElemsLength = document.body.children.length; // Считаем количество элементов в body
Здесь и далее считаем количество HTML-элементов или атрибутов с помощью свойства length.
// Перебор всех HTML-элементов 1-го уровня
for (let e = 0; e < bodyElemsLength; e++) {
Название переменной e — это просто понятное сокращение от «element». Вы же можете выбрать любое, на свой вкус.
Самое простое — записать названия элементов. Для этого обращаемся к коллекции потомков элемента <body> и перебираем их, подставляя следующий порядковый номер элемента e. Затем обращаемся к свойству tagName:
// Выбор элемента 1-го уровня
let elementLevel1 = document.body.children[e];
// Запись названия элемента 1-го уровня
let elementLevel1Name = elementLevel1.tagName; // Выбор названия элемента 1-го уровня
console.log('НАЙДЕН ЭЛЕМЕНТ 1-ГО УРОВНЯ <' + elementLevel1Name + '>'); // Уведомление о найденном элементе 1-го уровня
objectToStringify['element' + e + 'Level1'] = [{['element' + e + 'Level1Name']: elementLevel1Name}]; // Запись названия элемента 1-го уровня в объект
Здесь и далее мы конструируем понятные названия ключей в квадратных скобках с помощью комбинации текста в кавычках и названий соответствующих переменных.
С получением атрибутов ситуация усложняется: сначала нужно проверять их наличие у элемента.
// Запись атрибутов элемента 1-го уровня
// Проверка элемента 1-го уровня на наличие атрибутов
if (elementLevel1.attributes.length > 0) {
Реакция конвертера будет отличаться в зависимости от результата проверки. Если атрибуты найдены, нужно сосчитать их количество, перебрать по очереди и красиво записать в нужное место.
Для перебора атрибутов у нас отдельный вложенный цикл, где переменная a является сокращением от слова «attribute».
// Если атрибуты элемента 1-го уровня найдены
let elementLevel1Attributes = elementLevel1.attributes; // Выбор атрибутов элемента 1-го уровня
// Объявление массива для структурированной записи атрибутов элемента 1-го уровня
let saveAttributes = new Array();
// Перебор и запись в корневой объект objectToStringify атрибутов элемента 1-го уровня
for (let a = 0; a < elementLevel1Attributes.length; a++) {
// Уведомление о найденном атрибуте элемента 1-го уровня
console.log('Атрибут элемента 1-го уровня <' + elementLevel1Name + '>: ' + elementLevel1Attributes[a].name + '=' + elementLevel1Attributes[a].value);
let attributeName = elementLevel1Attributes[a].name; // Запись названия атрибута элемента 1-го уровня
let attributeValue = elementLevel1Attributes[a].value; // Запись значения атрибута элемента 1-го уровня
// Запись названия и значения текущего атрибута элемента 1-го уровня в объект
let currentAttribute = {
['element' + e + 'Level1' + 'attribute' + a]: [
{
['element' + e + 'Level1' + 'attribute' + a + 'Name']: attributeName
},
{
['element' + e + 'Level1' + 'attribute' + a + 'Value']: attributeValue
}
]
};
// Сохранение структурированного атрибута элемента 1-го уровня в массив
saveAttributes.push(currentAttribute);
};
// Запись структурированных атрибутов элемента 1-го уровня из массива в корневой объект objectToStringify
objectToStringify['element' + e + 'Level1'][1] = {
['element' + e + 'Level1Attributes']: saveAttributes
};
Если атрибуты не были найдены, записываем пустое значение.
} else {
// Если у элемента 1-го уровня нет атрибутов, вывод уведомления в консоль
console.log('Элемент 1-го уровня <' + elementLevel1Name + '> не имеет атрибутов.');
// Если у элемента 1-го уровня нет атрибутов, то объект с местом под них оставляем с пустым значением
objectToStringify['element' + e + 'Level1'][1] = {
['element' + e + 'Level1Attributes']: ''
};
};
После атрибутов нужно записать содержимое элемента — то есть текст, вложенные элементы или пустое значение. Для этого нужно измерить количество вложенных элементов, а если их нет, то проверить наличие текста. Это делается следующим образом:
// Проверка вида содержимого элемента 1-го уровня
// Проверка на наличие элементов 2-го уровня
if (elementLevel1.children.length > 0) {
// Если элементы 2-го уровня найдены
} else if (elementLevel1.children.length == 0) {
// Если элементы 2-го уровня не найдены, проверка элемента 1-го уровня на наличие текста или пустого значения
if (elementLevel1.textContent) {
// Если есть текст
} else {
// Если содержимого нет
};
};
После проверки происходит запись найденного содержимого элемента первого уровня:
// Запись структурированного содержимого элемента 1-го уровня из массива в корневой объект objectToStringify
objectToStringify['element' + e + 'Level1'][2] = {
['element' + e + 'Level1Content']: elementLevel1Content
};
…если содержимое — это элементы второго уровня.
let elementLevel1Text = elementLevel1.textContent; // Выбор текстового контента внутри элемента 1-го уровня
console.log('Элемент 1-го уровня <' + elementLevel1Name + '> содержит текст: "' + elementLevel1Text + '"');
// Запись текста элемента 1-го уровня в корневой объект objectToStringify
objectToStringify['element' + e + 'Level1'][2] = {
['element' + e + 'Level1Content']: elementLevel1Text
};
…если содержимое — текст.
// Если элемент 1-го уровня не содержит ни вложенных элементов, ни даже текста
console.log('Элемент 1-го уровня <' + elementLevel1Name + '> не содержит текста.');
// Если элемент 1-го уровня не имеет содержимого, то объект с местом под содержимое оставляем с пустым значением
objectToStringify['element' + e + 'Level1'][2] = {
['element' + e + 'Level1Content']: ''
…если нет содержимого.
Не будем душнить и бомбить вас кодом дальше: элементы второго и третьего уровней записываются аналогичным образом во время перебора содержимого их родительских элементов первого уровня — это можно увидеть ниже, в финальной версии кода.
Для второго и третьего уровней в их собственных циклах for элементы перебираются с помощью переменных e2 и e3 соответственно, а в переборе их атрибутов фигурируют переменные a2 и a3 — всё по аналогии с первым уровнем.
Когда циклы перебора/записи в объект элементов всех уровней, их атрибутов и содержимого закончены, мы скармливаем получившийся JavaScript-объект встроенному методу JSON.stringify() для сериализации:
let result = JSON.stringify(objectToStringify); // Сериализуем объект
После этого нужно вывести результат на страницу с помощью команды на перезапись элемента <body> содержимым переменной result:
document.body.innerHTML = result; // Заменяем тело страницы на наш JSONИтоговую версию кода можно посмотреть (и даже скопипастить, если лень разбираться) по ссылке. В результате его выполнения наша страница превращается в JSON-строку и принимает следующий вид:
{"element0Level1":[{"element0Level1Name":"P"},{"element0Level1Attributes":[{"element0Level1attribute0":[{"element0Level1attribute0Name":"class"},{"element0Level1attribute0Value":"myclass"}]},{"element0Level1attribute1":[{"element0Level1attribute1Name":"style"},{"element0Level1attribute1Value":"margin: 1px;"}]}]},{"element0Level1Content":"Какой-то абзац."}],"element1Level1":[{"element1Level1Name":"DIV"},{"element1Level1Attributes":""},{"element1Level1Content":[{"element0Level2":[{"element0Level2Name":"P"},{"element0Level2Attributes":""},{"element0Level2Content":[{"element0Level3":[{"element0Level3Name":"SPAN"},{"element0Level3Attributes":""},{"element0Level3Content":"Другой абзац (элемент 3-го уровня)"}]}]}]},{"element1Level2":[{"element1Level2Name":"SPAN"},{"element1Level2Attributes":""},{"element1Level2Content":"Другой абзац (элемент 2-го уровня)"}]},{"element2Level2":[{"element2Level2Name":"SPAN"},{"element2Level2Attributes":""},{"element2Level2Content":[{"element0Level3":[{"element0Level3Name":"A"},{"element0Level3Attributes":[{"element0Level3attribute0":[{"element0Level3attribute0Name":"href"},{"element0Level3attribute0Value":"#"}]}]},{"element0Level3Content":"Какая-то ссылка (элемент 3-го уровня)."}]}]}]},{"element3Level2":[{"element3Level2Name":"IMG"},{"element3Level2Attributes":[{"element3Level2attribute0":[{"element3Level2attribute0Name":"src"},{"element3Level2attribute0Value":"images/test.png"}]},{"element3Level2attribute1":[{"element3Level2attribute1Name":"alt"},{"element3Level2attribute1Value":"Тестовое изображение"}]}]},{"element3Level2Content":""}]}]}],"element2Level1":[{"element2Level1Name":"SCRIPT"},{"element2Level1Attributes":[{"element2Level1attribute0":[{"element2Level1attribute0Name":"src"},{"element2Level1attribute0Value":"script.js"}]}]},{"element2Level1Content":""}]}
В любом JSON-форматтере можно просмотреть результат в более читабельном виде.

Скриншот: Skillbox Media
На всякий случай прогоним результат и через валидатор JSON.

Скриншот: Skillbox Media
А в консоли — вот такая красота.

Скриншот: Skillbox Media
Итоги
Мы рассмотрели один из способов превращения HTML-файла в JSON-строку. Вариантов реализации может быть много, поэтому вид кода будет сильно зависеть от ваших задач. Руководствуясь указанным алгоритмом и встроенными методами языка JavaScript, вы сможете самостоятельно реализовать подобный конвертер. А о том, как выполнить обратную конвертацию, мы расскажем в следующей статье.
