iami233
iami233
文章158
标签37
分类4

文章分类

文章归档

QQ 聊天记录 MHT 文件转 HTML

QQ 聊天记录 MHT 文件转 HTML

写在前面

虽然最近博客没有更新,但博主一直在写一些回忆性的文章(都存放在某个赛博空间里)。最近突然回想起一些往事,想翻看以前的聊天记录。尽管那时明月在,曾照彩云归,好在博主没有删掉数据的习惯,消息记录几乎都还保留着。虽然,我们的对话仅仅持续了一年半。

正文

目前,QQ 客户端提供三种导出格式:.bak.mht.txt。凭借一些常识和对导出后文件大小的观察,很容易发现,baktxt 应该只是单纯的纯文本消息,不含任何图文内容。因此,如果想保留消息中的图片或其他资源,毫无疑问,mht 格式便成了唯一的选择。

MHT 它是一种能够将图片、样式表、脚本等多种资源打包成一个独立文件的格式。通过这种方式,所有的信息都能被封装在一个文件内,方便进行管理和导出。

img

当博主尝试导出消息记录时,由于聊天内容过多,客户端一度陷入了假死状态,持续了相当长一段时间。博主此时隐隐地觉得不妙。

导出的文件在用 Edge 打开时,由于体积过大(高达 500MB),导致无法显示任何内容。转而使用 Chrome 时,则出现了如下错误提示(🤣👉🤡):

1
Malformed multipart archive

通过在 file.org 查阅资料,博主才意识到,原来 .mht 文件并不支持所有浏览器。它必须依赖于 Internet ExplorerEdge 才能正常打开。

MHT files are commonly associated with the Internet Explorer Web browser.

这也解释了为什么 Edge 会自动切换到 Internet Explorer 模式(但是,打开一直无响应和转圈圈…)。

找了一些转换工具也没转换成功,没办法,只能自己造轮子了。其实也并不复杂,只需要简单了解一下文件结构~

导出的 .mht 文件结构大致如下:

  • Header 部分:包含文件类型、编码方式、内容类型等元数据。
  • Body 部分:存放页面的 HTML 内容,以及相关的图片、样式表、脚本等资源。
  • 分隔符:各个部分通过边界(boundary)分隔,确保资源能够正确解析。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
From: <Save by Tencent MsgMgr>  <!-- 发件人信息 -->
Subject: Tencent IM Message <!-- 邮件主题 -->
MIME-Version: 1.0 <!-- MIME 版本,指示文件的编码和多部分结构 -->
Content-Type: multipart/related; <!-- 邮件的主要内容类型是 multipart/related,表示文件有多个相关部分 -->
charset="utf-8" <!-- 编码格式为 UTF-8 -->
type="text/html"; <!-- 内容类型为 HTML -->
boundary="----=_NextPart_4938232B_D5C6_4d39_AC69.64B3BD4C94EF" <!-- 分隔不同内容部分的 boundary,标识每个部分的开始和结束 -->

------=_NextPart_4938232B_D5C6_4d39_AC69.64B3BD4C94EF <!-- 内容的第一个部分开始 -->
Content-Type: text/html <!-- 该部分的内容类型是 HTML -->
Content-Transfer-Encoding:7bit <!-- 内容传输编码 -->

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>QQ Message</title>
<style type="text/css">
body {
font-size: 12px;
line-height: 22px;
margin: 2px;
}

td {
font-size: 12px;
line-height: 22px;
}
</style>
</head>

<body>
<table width="100%" cellspacing="0">
<tr>
<td>
<div style="padding-left: 10px"><br /><b>消息记录</b></div>
</td>
</tr>
<tr>
<td>
<div style="padding-left: 10px">消息分组:已删除联系人</div>
</td>
</tr>
<tr>
<td>
<div style="padding-left: 10px">消息对象:iami233</div>
</td>
</tr>
<tr>
<td>
<div style="padding-left: 10px">&nbsp;</div>
</td>
</tr>
<tr>
<td style=" border-bottom-width: 1px; border-bottom-color: #8ec3eb; border-bottom-style: solid; color: #3568bb; font-weight: 700; height: 24px; line-height: 24px; padding-left: 10px; margin-bottom: 5px;" >
日期:2024-02-24
</td>
</tr>
<tr>
<td>
<div style="color: #006efe; padding-left: 10px">
<div style="float: left; margin-right: 6px">iami233</div>
21:09:45
</div>
<div style="padding-left: 20px">
<font style="font-size: 10pt; font-family: &quot" color="000000" >我是一条示例消息捏~</font>
</div>
</td>
</tr>
<tr>
<td>
<div style="color: #42b475; padding-left: 10px">
<div style="float: left; margin-right: 6px">小 iami233</div>
13:44:52
</div>
<div style="padding-left: 20px">
<img src="{E3D98612-5231-47ad-B7E4-1160C3A24C55}.dat" />
</div>
</td>
</tr>
</table>
</body>
</html>

------=_NextPart_4938232B_D5C6_4d39_AC69.64B3BD4C94EF <!-- 内容的第二部分开始 -->
Content-Type: image/gif <!-- 图片类型 -->
Content-Transfer-Encoding: base64 <!-- 图片采用 Base64 编码 -->
Content-Location:{E3D98612-5231-47ad-B7E4-1160C3A24C55}.dat <!-- 图片的唯一标识符 -->

... Base64 encoded data ... <!-- Base64 编码后图片数据 -->

------=_NextPart_4938232B_D5C6_4d39_AC69.64B3BD4C94EF-- <!-- 文件结束 -->

简单了解完文件结构后,我们只需提取出 <html>...</html> 部分,同时将图片资源转存,便能实现所需的核心功能🥰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import re
import base64

with open ( 'example.mht', "r", encoding="utf-8" ) as f:
content = f.read ( )

# 匹配 boundary,获取 boundary 的值
boundary_match = re.search ( r'boundary=" ( [^"]+ ) "', content )
boundary = boundary_match.group ( 1 )

# 查找 HTML 部分,通过 boundary 分割
parts = re.split ( rf"--{boundary} ( ?:-- ) ?\n", content )
html_part = next (( p for p in parts if "Content-Type: text/html" in p ) , None )

# 解析 HTML 内容
html_match = re.search ( r" ( ?: \n\n|\r\n\r\n ) ( .*? ) ( ?=\n--|$ ) ", html_part, re.DOTALL )

print ( html_match.group ( 1 ) .strip ( ) , "\n" )

for part in parts:
# 跳过不包含 Content-Location 的部分
if "Content-Location:" not in part:
continue

headers_str, _, body = part.partition ( "\n\n" )

# 解析 headers
headers = {}
for line in headers_str.split ( "\n" ) :
if ":" in line:
key, value = line.split ( ":", 1 )
headers [key.strip ( ) .lower ( )] = value.strip ( )

# 获取 content-location
content_location = headers.get ( "content-location" )

# 转存资源
content = base64.b64decode ( body.strip ( ))
with open ( content_location, "wb" ) as f:
f.write ( content )
print ( f"{content_location} 已保存" )

剩下的只需要亿点点优化就可以了,比如:

  • 将 HTML 中的内联样式全部提取并转换为 CSS 类,尽可能的减小文件体积和冗余样式
  • 支持将资源文件输出到指定文件夹,并自动更新 HTML 中的资源引用路径。
  • 消息记录无法导出语音,视频等内容,所以需要自动处理空白记录并插入提示文本。

好啦,剩下的直接前往 5ime/mht2html 查看即可。

不过,需特别提醒,此项目针对性非常强,基本只为博主查看聊天记录所用。如果你有其他需求,比如大文件切割什么的,就得自己动手研究啦~

写在后面

在写代码的某一刻,我也曾幻想过,想着将聊天记录中的所有文字提取出来,分词生成词云,甚至训练个模型什么的。可是,随着时光流转,那些想法渐渐褪去光彩,变得毫无意义。

毕竟,那些文字,终究只是过去的碎片罢了。

难道不是吗?

最终,博主依旧没去翻看那些文字。脑海中的记忆早已模糊不清,留下的,或许只剩下空白与迷惘,etc.

本文作者:iami233
本文链接:https://5ime.cn/mht2html.html
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可