QQ 聊天记录 MHT 文件转 HTML
写在前面
虽然最近博客没有更新,但博主一直在写一些回忆性的文章(都存放在某个赛博空间里)。最近突然回想起一些往事,想翻看以前的聊天记录。尽管那时明月在,曾照彩云归,好在博主没有删掉数据的习惯,消息记录几乎都还保留着。虽然,我们的对话仅仅持续了一年半。
正文
目前,QQ 客户端提供三种导出格式:.bak
、.mht
和 .txt
。凭借一些常识和对导出后文件大小的观察,很容易发现,bak
和 txt
应该只是单纯的纯文本消息,不含任何图文内容。因此,如果想保留消息中的图片或其他资源,毫无疑问,mht
格式便成了唯一的选择。
MHT 它是一种能够将图片、样式表、脚本等多种资源打包成一个独立文件的格式。通过这种方式,所有的信息都能被封装在一个文件内,方便进行管理和导出。
当博主尝试导出消息记录时,由于聊天内容过多,客户端一度陷入了假死状态,持续了相当长一段时间。博主此时隐隐地觉得不妙。
导出的文件在用 Edge
打开时,由于体积过大(高达 500MB),导致无法显示任何内容。转而使用 Chrome
时,则出现了如下错误提示(🤣👉🤡):
Malformed multipart archive
通过在 file.org 查阅资料,博主才意识到,原来 .mht
文件并不支持所有浏览器。它必须依赖于 Internet Explorer
或 Edge
才能正常打开。
MHT files are commonly associated with the Internet Explorer Web browser.
这也解释了为什么 Edge
会自动切换到 Internet Explorer
模式(但是,打开一直无响应和转圈圈…)。
找了一些转换工具也没转换成功,没办法,只能自己造轮子了。其实也并不复杂,只需要简单了解一下文件结构~
导出的 .mht
文件结构大致如下:
- Header 部分:包含文件类型、编码方式、内容类型等元数据。
- Body 部分:存放页面的 HTML 内容,以及相关的图片、样式表、脚本等资源。
- 分隔符:各个部分通过边界(boundary)分隔,确保资源能够正确解析。
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"> </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: "" 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>
部分,同时将图片资源转存,便能实现所需的核心功能🥰
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.