Некоторые особенности работы с mhtml на PHP


Делаю новый сервис - сохранение страниц сайтов - и сохраняю эти самые страницы в формате mhtml. За сохранение отвечает расширение, отправляет на сервер -–все в порядке, все нормально. А вот потом, после сохранения, сервис дает возможность либо скачать страницы, либо посмотреть их сразу в браузере, не отходя от кассы. И вот тут есть некоторые нюансы, о которых я и расскажу в заметке ниже.

Сам по себе файл формата mhtml – это грубо говоря обычный текстовый файл, в который засунуты все ресурсы сразу. Не только код страницы, но стили объектов, картинки. Последние версии браузеров «из коробки» умеют рендерить этот файл и отображать его практически 1 в 1 с вариантом на сервере. Ну и я почему-то подумал, что и у меня проблем не возникнет. Однако, они возникли.

Если просто вывести код mhtml на сервере на PHP с помощью команды echo, то мы увидим что-то такое



Здесь я решил не изобретать велосипед, а поискать готовые библиотеки. Для PHP ничего бесплатного и простого не нашел, поэтому решил начать ковырять. Посмотрев исходный код файла mhml, будет видно, то он разделе на блоки, разделяемые

------MultipartBoundary—
Самое первое, что нас интересует – это блок с Content-Type: text/html – очевидно, что это текст страницы. Извлекаем его с помощью своей функции
function extractHtmlFromMhtml($mhtmlContent) {
    // Найти границу
    $boundary = '------MultipartBoundary--';
    if (strpos($mhtmlContent, $boundary) === false) {
        $boundary = '------MultipartBoundary--';
    }

    // Разделить файл на части
    $parts = explode($boundary, $mhtmlContent);

    // Пройтись по частям и найти HTML часть
    foreach ($parts as $part) {
        // Пропустить пустые части
        if (trim($part) === '') {
            continue;
        }

        // Найти заголовок Content-Type
        $contentTypePos = strpos($part, 'Content-Type:');
        if ($contentTypePos !== false) {
            $contentTypeLine = substr($part, $contentTypePos, strpos($part, "\n", $contentTypePos) - $contentTypePos);
            if (strpos($contentTypeLine, 'text/html') !== false) {
                // Найдена HTML часть
                $htmlStart = strpos($part, '<html');
                $htmlEnd = strrpos($part, '</html>') + 7;
                return substr($part, $htmlStart, $htmlEnd - $htmlStart);
            }
        }
    }

    return null;
}
После выделения нужного куска с помощью этой функции, картина меняется на такую



Дальше в исходнике в блоке с html мы видим вот такую строку

Content-Transfer-Encoding: quoted-printable
Очевидно, что это способ кодирования данных. В PHP есть встроенная функция, которая может работать с таким методом. Задействуем её

$mhtml_content = "исходник mhtml";
	
	//извлекаем блок html
	$mhtml_content_html = extractHtmlFromMhtml($mhtml_content);

	//перекодируем
	$mhtml_content_q = quoted_printable_decode($mhtml_content_html);

	echo $mhtml_content_q;
И получаем практически оригинал



Но не совсем. Если посмотреть в исходный код страницы, то увидим, что обращение идет ко внешним файлам – картинок, стилей, скриптов. А это мало того, что небезопасно, так еще и ненадежно – зачем нам хранить у себя копию страницы, если всё равно обращаемся к источнику? А если источник будет недоступен.

Поэтому обрезаем все внешние источники для страницы например так

header("Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:");
Тут мы разрешаем встроенные стили и картинки через data. И теперь снова страница выглядит совсем не очень хорошо:



Сначала встроим стили. Используем например такую функцию:

function extractStylesFromMhtml($mhtmlContent) {
    // Найти границу
    $boundary = '------MultipartBoundary--';

    // Разделить файл на части
    $parts = explode($boundary, $mhtmlContent);

    $style = '<style type="text/css">';

    // Пройтись по частям и найти CSS части
    foreach ($parts as $part) {
        // Пропустить пустые части
        if (trim($part) === '') {
            continue;
        }

        // Найти заголовок Content-Type
        $contentTypePos = strpos($part, 'Content-Type: text/css');
        if ($contentTypePos !== false) {
			$lines = explode("\n", $part);
			$chet = 0;
			foreach ($lines as $line){
				$chet++;
				if ($chet > 4){
					$style .= $line."\n";
				}
			}
            
      
        }
    }

	$style .= "</style>";

    return $style;
}
Смотрим на результат:



Теперь еще извлечем картинки. Для написания функции извлечения картинок из mhtml стоит учитывать исходный формат, а также убрать все переносы строк.

//html
	$mhtml_content_html = extractHtmlFromMhtml($mhtml_content);
	$mhtml_content_q = quoted_printable_decode($mhtml_content_html);

	//извлекаем картинки
	$art = extractImagesFromMhtml($mhtml_content);
	foreach ($art as $img => $base){
		//echo $img."<br>";
		$mhtml_content_q = str_replace($img, $base, $mhtml_content_й);
	}

	echo $mhtml_content_q;

	//стили
	$style = extractStylesFromMhtml($mhtml_content);
	$style = quoted_printable_decode($style);
	echo $style;
И в итоге получаем вот такую страницу у себя:



Как видите, все же не все картинки загрузились. Более того, в некоторых случаях необходимы будут доработки в плане кодировок, но и этот код, что мы уже сделали выведет более-менее корректно наверно процентов 90 сохраненных вами страниц.

Так что не так уж страшен формат mhml. Есть вопросы? Пишите, за небольшую плату с удовольствием проконсультирую.
Автор этого материала - я - Пахолков Юрий. Я оказываю услуги по написанию программ на языках Java, C++, C# (а также консультирую по ним) и созданию сайтов. Работаю с сайтами на CMS OpenCart, WordPress, ModX и самописными. Кроме этого, работаю напрямую с JavaScript, PHP, CSS, HTML - то есть могу доработать ваш сайт или помочь с веб-программированием. Пишите сюда.

тегизаметки, php, mhtml




Отправляя сообщение я подтверждаю, что ознакомлен и согласен с политикой конфиденциальности данного сайта.




Selenium пример на Java
Selenium пример на Java
IP отправителя письма в Thunderbird, яндекс, mail.ru и gmail.com