新增数据库解析,数据库字段说明

This commit is contained in:
xaoyo 2023-09-27 21:08:12 +08:00
parent 5acc8ba83e
commit 87e066d113
4 changed files with 502 additions and 1 deletions

View File

@ -250,6 +250,9 @@ def merge_media_msg_db(db_path: list, save_path: str):
def main(keys: list = None):
decrypted_ROOT = os.path.join(os.getcwd(), "decrypted")
if keys is None:
print("keys is None")
return False
user_dirs = get_wechat_db()
for user, db_path in user_dirs.items(): # 遍历用户
@ -286,7 +289,7 @@ def main(keys: list = None):
merge_copy_msg_db(EmotionDecryptPaths, EmmotionDbPath)
shutil.rmtree(decrypted_path_tmp) # 删除临时文件
return True
if __name__ == '__main__':

246
parse_db/README.md Normal file
View File

@ -0,0 +1,246 @@
# 微信PC端各个数据库简述
* 说明:针对 .../WeChat Files/wxid_xxxxxxxx/Msg下的各个文件解密后的内容进行概述
* 未作特别说明的情况下“聊天记录数据”指代的数据结构上都和Multi文件夹中的完整聊天记录数据相同或类似。
* 本文档仅供学习交流使用,严禁用于商业用途及非法用途,否则后果自负
## 一、微信小程序相关
微信小程序的相关数据,包括但不限于:
* 你使用过的小程序 RecentWxApp
* 星标的小程序 StarWxApp
* 各个小程序的基本信息 WAContact
用处不大不过可以看到你使用过的小程序的名称和图标以及小程序的AppID
## 二、企业微信相关
### BizChat
企业微信联系人数据,包括但不限于:
* 在微信中可以访问的企业微信会话ChatInfo
* 一部分会话的信息ChatSession未确认与ChatInfo的关系这其中的Content字段是最近一条消息疑似用于缓存展示的内容
* 包括群聊在内的聊天涉及的所有企业微信用户身份信息UsrInfo
* 该微信账号绑定的企业微信身份MyUsrInfo
* 特别说明:未经详细查证,这其中的聊天是否包含使用普通微信身份与企业微信用户发起的聊天,还是只包含使用绑定到普通微信的企业微信身份与其它企业微信身份发起的聊天。
### BizChatMsg
* 企业微信聊天记录数据,包括所有和企业微信聊天的数据。
* 与BizChat一样未确定涉及的范围究竟是只有企业微信-企业微信还是同时包含普通微信-企业微信。
* 另外此处的消息与Multi文件夹中真正的微信消息不同的是在于没有拆分数据库。
### OpenIM 前缀
* 这个也是企业微信的数据,包括联系人、企业信息、与企业微信联系人的消息等。
* 这个是普通微信-企业微信的数据上面biz前缀的是企业微信-企业微信
* 这个不常用,而且也没有全新的数据结构,不再详细说了。
### PublicMsg
* 看起来像是企业微信的通知消息,可以理解为企业微信的企业应用消息
## 三、微信功能相关
### Emotion
顾名思义表情包相关,包括但不限于以下内容:
* CustomEmotion顾名思义用户手动上传的GIF表情包含下载链接不过看起来似乎有加密内有aesKey字段但我没测试
* EmotionDes1 和 EmotionItem 应该也是类似的内容,没仔细研究
* EmotionPackageItem账号添加的表情包的集合列表从商店下载的那种
ps用处不大微信的MSG文件夹有表情包的url链接可以直接网络获取聊天记录中的表情包。
### Favorite
* FavItems收藏的消息条目列表
* FavDataItem收藏的具体数据。没有自习去看他的存储逻辑不过大概可以确定以下两点
* 即使只是简单收藏一篇公众号文章也会在 FavDataItem 中有一个对应的记录
* 对于收藏的合并转发类型的消息,合并转发中的每一条消息在 FavDataItem 中都是一个独立的记录
* FavTags为收藏内容添加的标签
### Misc
* 有BizContactHeadImg和ContactHeadImg1两张表应该是二进制格式的各个头像
### Sns
微信朋友圈的相关数据:
* FeedsV20朋友圈的XML数据
* CommentV20朋友圈点赞或评论记录
* NotificationV7朋友圈通知
* SnsConfigV20一些配置信息能读懂的是其中有你的朋友圈背景图
* SnsGroupInfoV5猜测是旧版微信朋友圈可见范围的可见或不可见名单
### FTS搜索
* 前缀为 FTS 的数据库可能都和全文搜索Full-Text Search相关(就是微信那个搜索框)
### FTSContact
有一堆表
* FTSChatroom15_content 和 FTSContact15_content
分别对应的是微信“聊天”界面会展示的消息会话(包括公众号等)和“联系人”界面会出现的所有人(有的时候并不是所有联系人都会出现在“聊天”中),信息包含昵称、备注名和微信号,也和微信支持搜索的字段相匹配。
### FTSFavorite
搜索收藏内容的索引
* 命名方式类似上面一条
ps对于收藏内容通过文字搜索电脑版是把所有东西拼接成一个超长字符串来实现的。这对于文本、链接等没啥问题但是对于合并转发消息就会出现搜索\[图片]
这一关键词。
### MultiSearchChatMsg
* 这个数据库前缀不一样,但是看内容和结构应该还是一个搜索相关,搜索的是聊天记录中的文件
* 存储了文件名和其所在的聊天
* 不过FTSMsgSearch18_content和SessionAttachInfo两张表记录数量有显著差异不确定是哪个少了或是怎样。
### HardLink文件在磁盘存储的位置
* 将文件/图片/视频的文件名指向保存它们的文件夹名称例如2023-04有用但不多。
### Media
* ChatCRVoice和MediaInfo 可能为语音信息
## 三、MicroMsg (联系人核心)
一个数据库,不应该和分类平级,但是我认为这是分析到目前以来最核心的,因此单独来说了。
### AppInfo
一些软件的介绍猜测可能是关于某些直接从手机APP跳转到微信的转发会带有的转发来源小尾巴的信息
### Biz 前缀
与公众号相关的内容,应该主要是账号本身相关。
能确定的是 BizSessionNewFeeds 这张表保存的是订阅号大分类底下的会话信息,包括头像、最近一条推送等。
### ChatInfo
保存“聊天”列表中每个会话最后一次标记已读的时间
### ChatRoom 和 ChatRoomInfo
存储群聊相关信息
* ChatRoom存储每个群聊的用户列表包括微信号列表和群昵称列表和个人群昵称等信息
* ChatRoomInfo群聊相关信息主要是群公告内容与成员无关
顺便再吐槽一下微信这个位置有一个命名出现异常的别的表前缀都是ChatRoom而突然出现一个ChatroomTool
### Contact
顾名思义,联系人。不过这里的联系人并不是指你的好友,而是所有你可能看见的人,除好友外还有所有群聊中的所有陌生人。
* Contact这张表存储的是用户基本信息包括但不限于微信号没有好友的陌生人也能看、昵称、备注名、设置的标签等等甚至还有生成的各种字段的拼音可能是用于方便搜索的吧
* ContactHeadImgUrl头像地址
* ContactLabel好友标签 ID 与名称对照
### PatInfo
存了一部分好友的拍一拍后缀,但是只有几个,我记得我电脑上显示过的拍一拍似乎没有这么少?
### Session
真正的“聊天”栏目显示的会话列表,一个不多一个不少,包括“折叠的群聊”这样子的特殊会话;信息包括名称、未读消息数、最近一条消息等
### TicketInfo
这张表在我这里有百余条数据,但是我实在没搞明白它是什么
## 四、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 与MSG数据库中消息的MsgSvrID一一对应
* Buf silk格式的语音数据
## 六、MSG聊天记录核心数据库
内部主要的两个表是`MSG`和`Name2ID`
### Name2ID
* `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`
相同的消息则最后三位依次递增。需要进一步确认不重复范围是在一个会话内还是所有会话。`CreateTime`
相同的消息则最后三位依次递增。需要进一步确认不重复范围是在一个会话内还是所有会话。
* StatusEx、FlagEx、Status、MsgServerSeq、MsgSequence这五个字段个人暂时没有分析出有效信息
* StrTalker消息发送者的微信号。特别说明从这里来看的话上面的`TalkerId`
字段大概率是指的消息所在的房间ID而非发送者ID当然也可能和`TalkerId`属于重复内容,这一点待确认。
* StrContent字符串格式的数据。特别说明的是除了文本类型的消息外别的大多类型这一字段都会是一段 XML
数据标记一些相关信息。通过解析xml可以得到更多的信息例如图片的宽高、语音的时长等等。
* DisplayContent对于拍一拍保存拍者和被拍者账号信息
* Reserved0~6这些字段也还没有分析出有效信息也有的字段恒为空
* CompressContent字面意思是压缩的数据实际上也就是微信任性不想存在 StrContent
里的数据在这里例如带有引用的文本消息等采用lz4压缩算法压缩
* BytesExtra额外的二进制格式数据
* BytesTrans目前看这是一个恒为空的字段
表1MSG.Type字段数值与含义对照表可能可以扩展到其它数据库中同样标记消息类型这一信息的字段
| 分类`Type` | 子分类`SubType` | 对应类型 |
|----------|--------------|-------------------------------------------------------------|
| 1 | 0 | 文本 |
| 3 | 0 | 图片 |
| 34 | 0 | 语音 |
| 43 | 0 | 视频 |
| 47 | 0 | 动画表情(第三方开发的表情包) |
| 49 | 1 | 类似文字消息而不一样的消息目前只见到一个阿里云盘的邀请注册是这样的。估计和57子类的情况一样 |
| 49 | 5 | 卡片式链接CompressContent 中有标题、简介等BytesExtra 中有本地缓存的封面路径 |
| 49 | 6 | 文件CompressContent 中有文件名和下载链接但不会读BytesExtra 中有本地保存的路径 |
| 49 | 8 | 用户上传的 GIF 表情CompressContent 中有CDN链接不过似乎不能直接访问下载 |
| 49 | 19 | 合并转发的聊天记录CompressContent 中有详细聊天记录BytesExtra 中有图片视频等的缓存 |
| 49 | 33/36 | 分享的小程序CompressContent 中有卡片信息BytesExtra 中有封面缓存位置 |
| 49 | 57 | 带有引用的文本消息(这种类型下 StrContent 为空,发送和引用的内容均在 CompressContent 中) |
| 49 | 63 | 视频号直播或直播回放等 |
| 49 | 87 | 群公告 |
| 49 | 88 | 视频号直播或直播回放等 |
| 49 | 2000 | 转账消息(包括发出、接收、主动退还) |
| 49 | 2003 | 赠送红包封面 |
| 10000 | 0 | 系统通知(居中出现的那种灰色文字) |
| 10000 | 4 | 拍一拍 |
| 10000 | 8000 | 系统通知(特别包含你邀请别人加入群聊) |

11
parse_db/__init__.py Normal file
View File

@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-#
# -------------------------------------------------------------------------------
# Name: __init__.py.py
# Description:
# Author: xaoyaoo
# Date: 2023/09/27
# -------------------------------------------------------------------------------
if __name__ == '__main__':
pass

241
parse_db/parse.py Normal file
View File

@ -0,0 +1,241 @@
# -*- coding: utf-8 -*-#
# -------------------------------------------------------------------------------
# Name: parse.py
# Description: 解析数据库内容
# Author: xaoyaoo
# Date: 2023/09/27
# -------------------------------------------------------------------------------
import sqlite3
import pysilk
from io import BytesIO
import wave
import pyaudio
import requests
import hashlib
from PIL import Image
import xml.etree.ElementTree as ET
def get_md5(data):
md5 = hashlib.md5()
md5.update(data)
return md5.hexdigest()
def parse_xml_string(xml_string):
"""
解析 XML 字符串
:param xml_string: 要解析的 XML 字符串
:return: 解析结果以字典形式返回
"""
def parse_xml(element):
"""
递归解析 XML 元素
:param element: 要解析的 XML 元素
:return: 解析结果以字典形式返回
"""
result = {}
# 解析当前元素的属性
for key, value in element.attrib.items():
result[key] = value
# 解析当前元素的子元素
for child in element:
child_result = parse_xml(child)
# 如果子元素的标签已经在结果中存在,则将其转换为列表
if child.tag in result:
if not isinstance(result[child.tag], list):
result[child.tag] = [result[child.tag]]
result[child.tag].append(child_result)
else:
result[child.tag] = child_result
# 如果当前元素没有子元素,则将其文本内容作为值保存
if not result and element.text:
result = element.text
return result
if xml_string is None or not isinstance(xml_string, str):
return None
try:
root = ET.fromstring(xml_string)
except Exception as e:
return xml_string
return parse_xml(root)
def read_img_dat(input_data):
# 常见图片格式的文件头
img_head = {
b"\xFF\xD8\xFF": ".jpg",
b"\x89\x50\x4E\x47": ".png",
b"\x47\x49\x46\x38": ".gif",
b"\x42\x4D": ".BMP",
b"\x49\x49": ".TIFF",
b"\x4D\x4D": ".TIFF",
b"\x00\x00\x01\x00": ".ICO",
b"\x52\x49\x46\x46": ".WebP",
b"\x00\x00\x00\x18\x66\x74\x79\x70\x68\x65\x69\x63": ".HEIC",
}
fomt = "un" # 文件格式
if isinstance(input_data, str):
with open(input_data, "rb") as f:
input_bytes = f.read()
t = 0
for hcode in img_head:
t = input_bytes[0] ^ hcode[0]
for i in range(1, len(hcode)):
if t == input_bytes[i] ^ hcode[i]:
fomt = img_head[hcode]
else:
break
else:
break
else:
return False
if fomt == "un":
print("未知文件格式")
return False
out_bytes = bytearray()
for nowByte in input_bytes: # 读取文件
newByte = nowByte ^ t # 异或解密
out_bytes.append(newByte)
md5 = get_md5(out_bytes)
return fomt, md5, out_bytes
def read_emoji(cdnurl, is_show=False):
headers = {
"User-Agent": "Mozilla/5.0 (Linux; Android 10; Redmi K30 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Mobile Safari/537.36"
}
r1 = requests.get(cdnurl, headers=headers)
rdata = r1.content
if is_show: # 显示表情
img = Image.open(BytesIO(rdata))
img.show()
return rdata
def decompress_CompressContent(data):
"""
解压缩MsgCompressContent内容
:param data:
:return:
"""
if data is None or not isinstance(data, bytes):
return None
i = 0
uncompressed_data = []
while i < len(data):
# 读取第一个字节
byte1 = data[i]
# 从高四位得到无匹配的明文长度Lh
Lh = byte1 >> 4
Li = byte1 & 0x0F # 从低四位得到匹配的数据长度Li
if Lh == 0x0f:
# 继续读取下一个字节L1
i = i + 1
L1 = data[i]
Lh = L1 + 0x0f
while data[i] == 0xFF:
# 继续读取下一个字节,并累加
i = i + 1
Lh += data[i]
i += 1
uncompressed_data.extend(data[i:i + Lh])
i = i + Lh
# 读取匹配的偏移量Offset
bias = data[i:i + 2]
offset = int.from_bytes(bias, byteorder='little')
i = i + 2
# 读取匹配的数据长度Li
if Li != 0x0F:
# 实际的匹配压缩长度即为Li = Li + 4
Li += 4
else:
# 从偏移量后面的可选匹配长度区域读取一个字节M1
M1 = data[i]
Li += M1
while M1 == 0xFF:
# 继续读取下一个字节M2
i += 1
M1 = data[i]
Li += M1
Li += 4
# 复制匹配的数据到解压缩数据缓冲区
uncompressed_data.extend(uncompressed_data[-offset:-offset + Li])
# break
# 转换为字符串
uncompressed_data = bytes(uncompressed_data) # .decode('utf-8')
return uncompressed_data
def read_audio_buf(buf_data, is_play=False, is_wave=False, rate=24000):
silk_file = BytesIO(buf_data) # 读取silk文件
pcm_file = BytesIO() # 创建pcm文件
pysilk.decode(silk_file, pcm_file, rate) # 解码silk文件->pcm文件
pcm_data = pcm_file.getvalue() # 获取pcm文件数据
silk_file.close() # 关闭silk文件
pcm_file.close() # 关闭pcm文件
if is_play: # 播放音频
def play_audio(pcm_data, rate):
p = pyaudio.PyAudio() # 实例化pyaudio
stream = p.open(format=pyaudio.paInt16, channels=1, rate=rate, output=True) # 创建音频流对象
stream.write(pcm_data) # 写入音频流
stream.stop_stream() # 停止音频流
stream.close() # 关闭音频流
p.terminate() # 关闭pyaudio
play_audio(pcm_data, rate)
if is_wave: # 转换为wav文件
wave_file = BytesIO() # 创建wav文件
with wave.open(wave_file, 'wb') as wf:
wf.setparams((1, 2, rate, 0, 'NONE', 'NONE')) # 设置wav文件参数
wf.writeframes(pcm_data) # 写入wav文件
rdata = wave_file.getvalue() # 获取wav文件数据
wave_file.close() # 关闭wav文件
return rdata
return pcm_data
def read_audio(MsgSvrID, is_play=False, is_wave=False, DB_PATH: str = "", rate=24000):
if DB_PATH == "":
return False
DB = sqlite3.connect(DB_PATH)
cursor = DB.cursor()
sql = "select Buf from Media where Reserved0='{}'".format(MsgSvrID)
DBdata = cursor.execute(sql).fetchall()
if len(DBdata) == 0:
return False
data = DBdata[0][0] # [1:] + b'\xFF\xFF'
pcm_data = read_audio_buf(data, is_play, is_wave, rate)
return pcm_data
if __name__ == '__main__':
pass