Как конвертировать HTML-документ в JSON: пишем рабочую программу на JavaScript
{"subtitle": "Дерзкая сериализация одного формата в другой — без последствий и проблем с законом."}


Иллюстрация: Polina Vari для Skillbox Media
У языка гипертекстовой разметки веб-страниц 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
Такой же способ подгрузки дополнительных статей в рубриках отдельных стран реализован и на сайте организации «Репортёры без границ». Выберем случайную страну — пусть это будет Италия. При нажатии на кнопку Show more posts загружаются три более старых материала. Если перейти по ссылке в кнопке, то внутри элемента <textarea> мы увидим JSON-вставку с кодом статей:

Скриншот: Skillbox Media

Скриншот: 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, вы сможете самостоятельно реализовать подобный конвертер. А о том, как выполнить обратную конвертацию, мы расскажем в следующей статье.