异想之旅:本人原创博客完全手敲,绝对非搬运,全网不可能有重复;本人无团队,仅为技术爱好者进行分享,所有内容不牵扯广告。本人所有文章仅在CSDN、掘金和个人博客(一定是异想之旅域名)发布,除此之外全部是盗文!


相关内容:


Multi文件夹中的文件解码和之前的其它数据库操作相同。

该文件夹中文件结构比较简单,只有三种:FTSMSGMediaMSGMSG。这里说是三种不是三个,是因为这里的数据库达到一定大小后会拆分。

FTSMSG

看过《总述》一文的应该很熟悉 FTS 这一前缀了——这代表的是搜索时所需的索引。

其内主要的内容是这样的两张表:

  • FTSChatMsg2_content:内有三个字段
    • docid:从1开始递增的数字,相当于当前条目的 ID
    • c0content:搜索关键字(在微信搜索框输入的关键字被这个字段包含的内容可以被搜索到)
    • c1entityId:尚不明确用途,可能是校验相关
  • FTSChatMsg2_MetaData
    • docid:与FTSChatMsg2_content表中的 docid 对应
    • msgId:与MSG数据库中的内容对应
    • entityId:与FTSChatMsg2_content表中的 c1entityId 对应
    • type:可能是该消息的类型
    • 其余字段尚不明确

特别地,表名中的这个数字2,个人猜测可能是当前数据库格式的版本号。

MediaMSG

这里存储了所有的语音消息。数据库中有且仅有Media一张表,内含三个有效字段:

  • Key
  • Reserved0
  • Buf

其中Reserved0字段MSG数据库中消息的MsgSvrID一一对应

第三项即语音的二进制数据,观察头部即可发现这些文件是以 SILK 格式存储的。这是一种微软为 Skype 开发并开源的语音格式,具体可以自行 Google。

下面是将 Buf 字段中的数据导出为文件的代码:

import sqlite3


def writeTofile(data, filename):
    with open(filename, 'wb') as file:
        file.write(data)
    print("Stored blob data into: ", filename, "\n")


def readBlobData(key):
    try:
        sqliteConnection = sqlite3.connect('dbs/decoded_MediaMSG0.db')
        cursor = sqliteConnection.cursor()
        print("Connected to SQLite")

        sql_fetch_blob_query = """SELECT * from Media where Key = ?"""
        cursor.execute(sql_fetch_blob_query, (key, ))
        record = cursor.fetchall()
        for row in record:
            print("Key = ", row[0], "Reserved0 = ", row[1])
            file = row[2]

            print("Storing on disk \n")
            path = f'{row[0]}.silk'
            writeTofile(file, path)

        cursor.close()

    except sqlite3.Error as error:
        print("Failed to read blob data from sqlite table", error)
    finally:
        if sqliteConnection:
            sqliteConnection.close()
            print("sqlite connection is closed")


readBlobData(1099511630953)

如果需要通过MSG数据库中的MsgSvrID找文件,则改一下 SQL 查询语句,再遍历所有数据库即可。

下面是将 silk 文件转为 wav 的代码(实现思路是先转为 pcm 再转 wav;wav 的采样率数据是个人试出来的):

KEY = 1099511630953

import wave
from pathlib import Path

import pilk


def pcm2wav(pcm_file, wav_file, channels=1, bits=16, sample_rate=24000):
    pcmf = open(pcm_file, 'rb')
    pcmdata = pcmf.read()
    pcmf.close()

    if bits % 8 != 0:
        raise ValueError("bits % 8 must == 0. now bits:" + str(bits))

    wavfile = wave.open(wav_file, 'wb')
    wavfile.setnchannels(channels)
    wavfile.setsampwidth(bits // 8)
    wavfile.setframerate(sample_rate)
    wavfile.writeframes(pcmdata)
    wavfile.close()


duration = pilk.decode(f"{KEY}.silk", f"{KEY}.pcm")
# print("语音时间为:", duration)
Path(f"{KEY}.silk").unlink()

pcm2wav(f"{KEY}.pcm", f"{KEY}.wav")
Path(f"{KEY}.pcm").unlink()

这两个代码没有加详细解释,自己读一下吧。

MSG

终于到了整个文件,不,整个工程最重要的位置了——聊天记录核心数据库

内部主要的两个表是MSGName2ID

其中Name2ID这张表只有一列,内容格式是微信号群聊ID@chatroom,作用是使MSG中的某些字段与之对应。虽然表中没有 ID 这一列,但事实上微信默认了第几行 ID 就是几(从1开始编号)。

下面主要来说MSG这张表(加粗是用于提醒自己内容待补充,并非是重要信息):

  • localId:字面意思消息在本地的 ID,暂未发现其功用
  • TalkerId:消息所在房间的 ID(该信息为猜测,猜测原因见 StrTalker 字段),与Name2ID对应。
  • MsgSvrID:猜测 Srv 可能是 Server 的缩写,代指服务器端存储的消息 ID
  • Type:消息类型,具体对照见表1
  • SubType:消息类型子分类,暂时未见其实际用途
  • IsSender:是否是自己发出的消息,也就是标记消息展示在对话页左边还是右边,取值0或1
  • CreateTime:消息创建时间的秒级时间戳。此处需要进一步实验来确认该时间具体标记的是哪个时间节点,个人猜测的规则如下:
    • 从这台电脑上发出的消息:标记代表的是每个消息点下发送按钮的那一刻
    • 从其它设备上发出的/收到的来自其它用户的消息:标记的是本地从服务器接收到这一消息的时间
  • Sequence:次序,虽然看起来像一个毫秒级时间戳但其实不是。这是 CreateTime 字段末尾接上三位数字组成的,通常情况下为000,如果在出现两条 CreateTime 相同的消息则最后三位依次递增。需要进一步确认不重复范围是在一个会话内还是所有会话
  • StatusEx、FlagEx、Status、MsgServerSeq、MsgSequence:这五个字段个人暂时没有分析出有效信息
  • StrTalker:消息发送者的微信号。特别说明,从这里来看的话,上面的 TalkerId 字段大概率是指的消息所在的房间ID,而非发送者ID,当然也可能和 TalkerId 属于重复内容,这一点待确认
  • StrContent:字符串格式的数据。特别说明的是,除了文本类型的消息外,别的大多类型这一字段都会是一段 XML 数据标记一些相关信息。
  • DisplayContent:对于拍一拍,保存拍者和被拍者账号信息
  • Reserved0~6:这些字段也还没有分析出有效信息,也有的字段恒为空
  • CompressContent:字面意思是压缩的数据,实际上也就是微信任性不想存在 StrContent 里的数据在这里(例如带有引用的文本消息等;这里的消息只能根据二进制分辨出包含什么,但是具体格式规范、如何取出数据本人不知)
  • BytesExtra:额外的二进制格式数据
  • BytesTrans:目前看这是一个恒为空的字段

这里面的猜测内容比较多,还有很多标注了应该进一步实验的还没有完成,是因为由于数据库解开后不可能随新收到的消息实时更新,而每次新发送一条消息也不知道它会出现在拆分后的哪个数据库中,因此实验效率极低。

表1:MSG.Type字段数值与含义对照表(可能可以扩展到其它数据库中同样标记消息类型这一信息的字段)

分类子分类对应类型
10文本
30图片
340语音
430视频
470动画表情(第三方开发的表情包)
491类似文字消息而不一样的消息,目前只见到一个阿里云盘的邀请注册是这样的。估计和 57 子类的情况一样
495卡片式链接,CompressContent 中有标题、简介等,BytesExtra 中有本地缓存的封面路径
496文件,CompressContent 中有文件名和下载链接(但不会读),BytesExtra 中有本地保存的路径
498用户上传的 GIF 表情,CompressContent 中有 CDN 链接,不过似乎不能直接访问下载
4919合并转发的聊天记录,CompressContent 中有详细聊天记录,BytesExtra 中有图片视频等的缓存
4933/36分享的小程序,CompressContent 中有卡片信息,BytesExtra 中有封面缓存位置
4957带有引用的文本消息(这种类型下 StrContent 为空,发送和引用的内容均在 CompressContent 中)
4963视频号直播或直播回放等
4987群公告
4988视频号直播或直播回放等
492000转账消息(包括发出、接收、主动退还)
492003赠送红包封面
100000系统通知(居中出现的那种灰色文字)
100004拍一拍
100008000系统通知(特别包含你邀请别人加入群聊)

本文参考资料(排名不分先后):

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐