Код
#статьи

Как конвертировать 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 (мы анализировали это в статье о парсинге данных).

JSON-ответ при подгрузке статей на страницу Skillbox Media «Код» (после нажатия кнопки «Показать ещё»)
Скриншот: Skillbox Media

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

Произвести конвертацию 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-го уровней повторят ту же структуру во вложениях. Короче, у нас чередуются объекты, вложенные массивы и вложенные в них объекты.

Простое объяснение работы JSON-конвертера
Кадр: сериал «В Филадельфии всегда солнечно»

Теперь, когда мы представили себе результат, разберём реализацию. Цикл перебора 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-форматтере можно просмотреть результат в более читабельном виде.

Результат конвертации HTML в JSON: вид в JSON-форматтере
Скриншот: Skillbox Media

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

Результат конвертации HTML в JSON: вид в валидаторе JSON
Скриншот: Skillbox Media

А в консоли — вот такая красота.

Результат конвертации HTML в JSON: вид уведомлений в консоли
Скриншот: Skillbox Media

Итоги

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

Изучайте IT на практике — бесплатно

Курсы за 2990 0 р.

Я не знаю, с чего начать
Научитесь: Профессия Python-разработчик Узнать больше
Понравилась статья?
Да

Пользуясь нашим сайтом, вы соглашаетесь с тем, что мы используем cookies 🍪

Ссылка скопирована