离线html文件及资源文件夹转换为单个mhtml文件

离线html文件及资源文件夹转换为单个mhtml文件

一、前言

旧版本的谷歌浏览器保存网页时默认只有两个选项,一个是网页-仅html文件,一个是网页-全部文件。前者只能保存网页的文字信息而不会保存网页中的图片和修饰样式等资源,后者会将网页的全部信息保存到本地,一般在保存网页时都会选择后者。

选择网页-全部文件保存后,会在本地生成一个html文件和一个名为html文件名_files的文件夹,文件夹中存有图片等资源文件。

当网页所含的资源文件较多时,资源文件夹会有很多零散的小文件,会降低磁盘空间利用率,且在打开、移动html文件时必须保证资源文件夹位于同一目录,管理起来也较为不便,因此需要一种方法将html文件及资源文件夹进行整合。

在新版本谷歌浏览器与Edge浏览器中,保存网页多出了第三个选项,即网页-单个文件,选择此项会将网页保存为单个mhtml文件,文件中包含了网页的文字、图片等全部内容,且mhtml文件与html文件同能被浏览器打开。现在保存网页大多选择此项。

对此本文介绍了将离线的html文件及资源文件夹整合为单个mhtml文件的方法,并给出了python实现的源代码,希望对想要将之前以网页-全部文件保存在本地的网页文件转换为单个mhtml文件的读者有所帮助。

二、转换过程

2.1mhtml文件简介

MHTML(MIME HTML,也称为"Web Archive")是一种将 HTML 内容和相关资源(如图像、样式表、脚本等)打包成单个文件的格式。它的设计目的是使得整个网页及其相关资源可以方便地被保存、传输和共享,类似于将整个网页保存为单个文件或文档。

MHTML 文件通常由以下两部分组成:

  1. HTML 内容部分: 这部分包含了网页的 HTML 内容,通常是一个完整的 HTML 页面,包括标签、文本内容、链接等。HTML 内容可以直接嵌入到 MHTML 文件中,也可以作为附件存在。
  2. 相关资源部分: 这部分包含了 HTML 页面中引用的所有相关资源,例如图像、样式表、脚本文件等。这些资源通常以二进制形式或者以其原始文本形式存在于 MHTML 文件中,可以通过 URL 或其他方式在 HTML 内容中引用。

2.2关键要点

2.2.1boundary的使用

在mhtml文件中,boundary是一种分隔符,用于分隔文件中的不同部分,每个部分会被一个特定的boundary字符串分隔。这个boundary字符串在文件的头部定义,并用于识别文件中的各个部分。这种分隔符要确保在文件中是唯一的,不会与文件内容混淆,可以使用随机生成的方式生成。

在Content-Type头中定义一个boundary字符串,分割正文内容的不同部分时要在boundary字符串加两个破折号。

全文结束时以boundary字符串前后各加两个破折号为结束标识。

boundary字符串可以自由定义,但必须保证不会与正文内容混淆,一般都以四个破折号开头。

示例如下:

html 复制代码
Content-Type: multipart/related; boundary="----=_NextPart_001_0001"

------=_NextPart_001_0001
Content-Type: text/html

<html>
<body>
<p>Hello, World!</p>
<img src="cid:image001.jpg@01D12345.67890ABC">
</body>
</html>

------=_NextPart_001_0001
Content-Type: image/jpeg
Content-Transfer-Encoding: base64
Content-ID: <image001.jpg@01D12345.67890ABC>

/9j/4AAQSkZJRgABAQAAAQABAAD...

------=_NextPart_001_0001--

2.2.2Content-Transfer-Encoding的选择

在mhtml文件中,Content-Transfer-Encoding用于指定如何对内容进行编码,常用的编码方式包括以下5种:

  1. 7bit

    描述: 表示内容是US-ASCII字符集,且每个字符的最高位是0。通常用于包含只有ASCII字符的文本。

    用途: 适用于不含任何非ASCII字符的纯文本内容。

    特点:编码后存储空间不变。

  2. 8bit

    描述: 表示内容包含8位的字符,允许包含非ASCII字符。每行不超过998个字符。

    用途: 适用于包含扩展字符集的文本(如UTF-8编码的文本)。

    特点:编码后存储空间不变,可用于传输中文,但某些传输路径(如电子邮件服务器)可能不支持 8bit 编码的内容,导致8位字符被截断或无法正确解析。

  3. Binary

    描述: 表示内容可以是任意的二进制数据,不做任何特殊的编码。每行长度不受限制。

    用途: 适用于包含原始二进制数据的部分,如图像或其他非文本数据。

    特点:编码后存储空间不变,但不是所有传输路径(如某些电子邮件服务器或传输协议)都支持 Binary 编码。

  4. Quoted-Printable

    描述: 用于对包含大量ASCII字符且少量非ASCII字符的文本进行编码。将非ASCII字符和某些特殊字符转换为=号后跟两个十六进制数字表示。

    用途: 常用于文本内容,其中大部分是ASCII字符但偶尔有非ASCII字符,如包含特殊字符的电子邮件正文。

    注意:编码要求每行不能超过 76 个字符,如果编码后的内容超过 76 个字符,会在 76 个字符处插入一个软换行符(等号 =),然后在下一行继续。

    特点:适合包含少量中文字符的文本,编码后的数据量不会增加太多。如果包含大量非 ASCII 字符,空间增加明显。

  5. Base64

    描述: 将二进制数据编码为ASCII字符。每个Base64编码后的字符表示6位二进制数据,常用于将二进制数据转换为文本形式。

    用途: 适用于需要传输的二进制数据,如图像、音频文件或其他二进制文件。

    特点:编码后的数据较长,占用空间增长约 33%,但传输和解析时最为安全和兼容。

比较常用的编码方式为后两种,Quoted-Printable和Base64。

Quoted-Printable编码代码如下:

python 复制代码
import quopri
with open(file_path, 'rb') as f:
    code_str = quopri.encodestring(f.read()).decode('utf-8')

Base64编码代码如下:

python 复制代码
import base64
with open(file_path, 'rb') as f:
    code_str = base64.b64encode(f.read()).decode('utf-8')

关于编码解码的详细介绍,可参照本博客的另一篇文章:python中编码与解码简记

2.2.3Content-Location

在 mhtml文件中,Content-Location 是一个 MIME 头字段,用于指定某个部分(部分内容)的相对或绝对 URL。它帮助浏览器和其他解析器找到和关联各部分的内容,尤其是内嵌资源(如图像、样式表等)。

每个被引用部分的 Content-Location 的值必须在文件内是唯一的,且与在html中引用的值相同,这样被引用部分才能够被正确加载。除此之外,Content-Location 的值的格式还要满足以下要求:

  1. 绝对 URL:
    包含完整的 URL,即以协议(http、https、ftp)开头的地址,示例如下:
html 复制代码
------=_NextPart_000_0000
Content-Type: text/html; charset=UTF-8
Content-Location: http://example.com/path/to/example.html

<!DOCTYPE html>
<html>
<head>
    <title>Example Page</title>
</head>
<body>
    <h1>Hello, World!</h1>
    <img src="http://example.com/path/to/image001.png" alt="Example Image">
</body>
</html>

------=_NextPart_000_0000
Content-Type: image/png
Content-Transfer-Encoding: base64
Content-Location: http://example.com/path/to/image001.png

iVBORw0KGgoAAAANSUhEUgAAAAUA
------=_NextPart_000_0000--
  1. cid: URL
    以cid:或CID:开头的唯一性标识符。
    如果使用这种格式的标识符,那么在被引用部分可以使用Content-Location定义标识符,也可以使用Content-ID定义标识符,示例如下:
html 复制代码
------=_NextPart_000_0000
Content-Type: text/html; charset=UTF-8
Content-Location: cid:page@example.com
Content-ID: <page@example.com>

<!DOCTYPE html>
<html>
<head>
    <title>Example Page</title>
</head>
<body>
    <h1>Hello, World!</h1>
    <img src="cid:image001@example.com" alt="Example Image">
</body>
</html>

------=_NextPart_000_0000
Content-Type: image/png
Content-Transfer-Encoding: base64
Content-Location: cid:image001@example.com
Content-ID: <image001@example.com>

iVBORw0KGgoAAAANSUhEUgAAAAUA
------=_NextPart_000_0000--

上述代码中的Content-Location: cid:image001@example.com与Content-ID: image001@example.com只保留一个即可起到引用作用。

经过测试,只有上述两种格式才能正确加载资源文件部分,而将Content-Location的值设置为本地完整路径或本地相对路径都无法正确加载。

2.3转换代码

代码如下:

python 复制代码
import os
from bs4 import BeautifulSoup
import base64
import mimetypes
import quopri

def get_mime_type(file_path):
    mime_type, _ = mimetypes.guess_type(file_path)
    return mime_type or 'application/octet-stream'

def convert_to_mhtml(html_file_path, mhtml_file_path):
    base_path = os.path.dirname(html_file_path)
    with open(html_file_path, 'r', encoding='utf-8') as file:
        html_content = file.read()

    soup = BeautifulSoup(html_content, 'html.parser')
    
    resources = []
    for tag in soup.find_all(['img', 'link', 'script']):
        attr = 'src' if tag.name == 'img' or tag.name == 'script' else 'href'
        resource_path = tag.get(attr)
        if resource_path and not resource_path.startswith(('http:', 'https:', 'data:')):
            full_path = os.path.join(base_path, resource_path)
            res_cid = "cid:" + resource_path
            if os.path.exists(full_path):
                with open(full_path, 'rb') as file:
                    res_bytedata = file.read()
                res_type = get_mime_type(full_path)
                resources.append((res_cid, res_type, res_bytedata))
                # Modify the tag to use absolute paths
                tag[attr] = res_cid

    # Reconstruct the HTML with absolute paths
    html_bytedata = str(soup).encode('utf-8')

    # Create MHTML content
    create_mhtml(html_bytedata, resources, mhtml_file_path)

def create_mhtml(html_bytedata, resources, mhtml_file_path):
    boundary = "----=_NextPart_000_0000_00000_000000"
    mhtml_content = [
        "From: ",
        "Subject: ",
        "Date: ",
        "MIME-Version: 1.0",
        f"Content-Type: multipart/related; type=\"text/html\"; boundary=\"{boundary}\""
    ]

    # 添加HTML部分  
    html_encodestr = quopri.encodestring(html_bytedata).decode('utf-8')
    mhtml_content.append(f"\n--{boundary}")
    mhtml_content.append("Content-Type: text/html; charset=\"utf-8\"")
    mhtml_content.append("Content-Transfer-Encoding: quoted-printable")
    mhtml_content.append("")
    mhtml_content.append(html_encodestr)

    # 添加资源部分
    for res_cid, res_type, res_bytedata in resources:
        if res_type is None:
            res_type = "application/octet-stream"
        res_encodestr = base64.b64encode(res_bytedata).decode('utf-8')
        mhtml_content.append(f"\n--{boundary}")
        mhtml_content.append(f"Content-Type: {res_type}")
        mhtml_content.append("Content-Transfer-Encoding: base64")
        mhtml_content.append(f"Content-Location: {res_cid}")
        mhtml_content.append("")
        mhtml_content.append(res_encodestr)

    mhtml_content.append(f"\n--{boundary}--")

    # 写入MHTML文件
    with open(mhtml_file_path, 'w', encoding='utf-8') as f:
        f.write("\n".join(mhtml_content))

if __name__ == "__main__":
    html_file = 'CSDN博客.html'
    mhtml_file = 'CSDN博客.mhtml'
    convert_to_mhtml(html_file, mhtml_file)

注意使用时需将html文件及资源文件夹放在同一目录下。

相关推荐
丕羽3 小时前
【Pytorch】基本语法
人工智能·pytorch·python
bryant_meng3 小时前
【python】Distribution
开发语言·python·分布函数·常用分布
m0_594526304 小时前
Python批量合并多个PDF
java·python·pdf
工业互联网专业5 小时前
Python毕业设计选题:基于Hadoop的租房数据分析系统的设计与实现
vue.js·hadoop·python·flask·毕业设计·源码·课程设计
钱钱钱端5 小时前
【压力测试】如何确定系统最大并发用户数?
自动化测试·软件测试·python·职场和发展·压力测试·postman
慕卿扬5 小时前
基于python的机器学习(二)—— 使用Scikit-learn库
笔记·python·学习·机器学习·scikit-learn
顾菁寒5 小时前
WEB第二次作业
前端·css·html
Json____5 小时前
python的安装环境Miniconda(Conda 命令管理依赖配置)
开发语言·python·conda·miniconda
小袁在上班5 小时前
Python 单元测试中的 Mocking 与 Stubbing:提高测试效率的关键技术
python·单元测试·log4j
白狐欧莱雅5 小时前
使用python中的pygame简单实现飞机大战游戏
经验分享·python·游戏·pygame