引言
对于对任何数据都有存档备份习惯的用户来说,微信的聊天记录自然不会放过。在曾经还是短信主导的年代中,我就会备份导出所有的短信,直到现在也一直持续着,因为任何数据都有其自身的价值,随着时间的流逝,这些数据记录了人生不同时间的状态。但是,微信的使用越来越流行,生活中很多讨论都不会再用短信这种传统方式,所以目前大部分交流都会经过微信。微信已经部分取代了短信的功能,并且微信相对于短信更加丰富化,其沟通模式已经超过了传统的文本信息,文字/表情/图片/语音/视频/...都能通过微信传送,备份这种数据自然会比短信更加麻烦。
当我导出微信用户文件夹的时候,发现光图片的数量就超3000张(后来知道群聊贡献了绝大部分),所以需要知道如何解析微信数据,并且抽取我们想要的内容。本文以Windows Phone 8.1为例,简单分析了一下微信APP在WP中是如何存储消息和媒体文件的,这些分析足够我们任意抽取想要的内容,大家可以认为这是一种数据层面的反向工程吧。
前提条件
因为WP系统的安全性设置,用户是不能直接访问大部分系统文件的,微信用户数据当然属于privacy数据,只能被微信APP访问。所以想要能访问这些数据,你的手机必须要越狱并且具有访问文件系统的权限。如果你不满足此前提条件,那么也没必要往下看了。如果有兴趣越狱,可以参考XDA大牛如何越狱的教程。
微信数据分析
数据目录
请确保你的微信安装在手机中(非SD卡),用数据线连接手机,如果手机已经有访问系统文件的权限,那么你可以在如下目录中找到你的微信数据
Phone\Data\Users\DefApps\APPDATA\Local\Packages\TencentWeChatLimited.380366539085C_sdtnhv12zgd7a\LocalState\storage\{Your_WeChat_UserName}
微信不同的用户数据被放在不同的文件夹,Your_WeChat_UserName是指你的微信号(如果有别的用户也在你手机上登陆过微信,就会有其他用户的文件夹)。拷贝所有文件到你的电脑上,就完成了微信数据的完整备份。整个用户文件夹的结构如下。

消息数据库
所有消息、联系人信息等都存储在database.sdf文件中,这是最重要的一个文件了。
Phone\Data\Users\DefApps\APPDATA\Local\Packages\TencentWeChatLimited.380366539085C_sdtnhv12zgd7a\LocalState\storage\{Your_WeChat_UserName}\db\database.sdf
database.sdf是一个SQL Server Compact 3.5的数据库,没有密码,可以通过VS的"SQL Server Compact/SQLite Toolbox"插件来进行查看和编辑。下图是整个数据库的所有表,直接从表名就非常容易每个表大概是做什么用的。最重要就是ChatMsg表了,里面存储了所有聊天记录。

为了备份聊天记录,我们基本上只用关心ChatMsg这个表,当需要知道具体联系人信息时,再根据字段strTalker去join联系人信息表Contact或者群信息表ChatRoom。下图是ChatMsg的所有字段和重要字段标识。

现在来分析nMsgType(消息类型)这个字段,不同的nMsgType表示了不同的消息类型,比如nMsgType==1表明了这是一条纯文本消息,而如果nMsgType==3则表明一条图片消息。不同的nMsgType对应的strContent(消息内容)的格式是不一样,需要用不同的解析函数去解析。下面列出了我微信数据库里出现的所有消息类型的详细分析,基本上看strContent的内容就能猜测出消息种类,可能还会有其他类型(比如我没有使用过的功能)。43和62均为视频,strContent内容格式一样,估计一个小视频,一个是普通视频。
- nMsgType==1(纯文本)
- nMsgType==3(图片)
<msg>
<img aeskey="*******" encryver="*******" cdnthumbaeskey="*******" cdnthumburl="*******" cdnthumblength="*******" cdnthumbheight="*******" cdnthumbwidth="*******" cdnmidimgurl="*******" length="*******" md5="*******">
</msg>
- nMsgType==34(语音)
<msg fromusername="********" encryptusername="********" fromnickname="********" content="********" fullpy="********" shortpy="********" imagestatus="********" scene="********" country="********" province="********" city="********" sign="********" percard="********" sex="********" alias="********" weibo="********" weibonickname="********" albumflag="********" albumstyle="********" albumbgimgid="********" snsflag="********" snsbgimgid="********" snsbgobjectid="********" mhash="********" mfullhash="********" bigheadimgurl="********" smallheadimgurl="********" ticket="********" opcode="********" googlecontact="********" qrticket="********"><brandlist></brandlist></msg>
- nMsgType==42(用户名片)
<msg bigheadimgurl="*******" smallheadimgurl="*******" username="*******" nickname="*******" fullpy="*******" shortpy="*******" alias="*******" imagestatus="*******" scene="*******" province="*******" city="*******" sign="*******" sex="*******" certflag="*******" certinfo="*******" brandIconUrl="*******" brandHomeUrl="*******" brandSubscriptConfigUrl="*******" brandFlags="*******" regionCode="*******" />
- nMsgType==43(视频)
<msg>
<videomsg aeskey="*******" cdnthumbaeskey="*******" cdnvideourl="*******" cdnthumburl="*******" length="*******" playlength="*******" cdnthumblength="*******" cdnthumbwidth="*******" cdnthumbheight="*******" fromusername="*******" md5="*******" />
</msg>
- nMsgType==47(表情)
<msg><emoji fromusername = "*******" tousername = "*******" type="*******" idbuffer="*******" md5="*******" len = "*******" productid="*******" androidmd5="*******" androidlen="*******" s60v3md5 = "*******" s60v3len="*******" s60v5md5 = "*******" s60v5len="*******" cdnurl = "*******" ></emoji> </msg>
- nMsgType==48(地图)
<msg>
<location x="*******" y="*******" scale="*******" label="*******" maptype="*******" poiname="*******" fromusername="*******" />
</msg>
- nMsgType==49(富文本)
<msg> <appmsg appid="*******" sdkver="*******"> <title><![CDATA[信用卡"*******"账单]]></title> <des><![CDATA[尊敬的*先生:
您****个人信用卡**月账单
账单日期 :****年**月**日
到期还款日:**月**日
人民币账单金额:¥****.**
最低还款额 :¥***.**
美元账单金额 :$0.00
最低还款额 :$0.00
欢迎点击下方菜单★查账-账单分期★
【限时优惠】分期满1万享最高100元还款金,首次分期不限金额赠1000积分!
点详情,可查账单明细。]]></des> <action></action> <type>5</type> <showtype>1</showtype> <content><![CDATA[]]></content> <contentattr>0</contentattr> <url><![CDATA[********]]></url> <lowurl><![CDATA[]]></lowurl> <appattach> <totallen>0</totallen> <attachid></attachid> <fileext></fileext> </appattach> <extinfo></extinfo> <mmreader> <category type="*******" count="*******"> <name><![CDATA[****信用卡]]></name> <topnew> <cover><![CDATA[]]></cover> <width>0</width> <height>0</height> <digest><![CDATA[尊敬的*先生:
您****个人信用卡**月账单
账单日期 :****年**月**日
到期还款日:**月**日
人民币账单金额:¥****.**
最低还款额 :¥***.**
美元账单金额 :$0.00
最低还款额 :$0.00
欢迎点击下方菜单★查账-账单分期★
【限时优惠】分期满1万享最高100元还款金,首次分期不限金额赠1000积分!
点详情,可查账单明细。]]></digest> </topnew> <item> <itemshowtype>4</itemshowtype> <title><![CDATA[信用卡"*******"账单]]></title> <url><![CDATA[********]]></url> <shorturl><![CDATA[]]></shorturl> <longurl><![CDATA[]]></longurl> <pub_time>1441870471</pub_time> <cover><![CDATA[]]></cover> <tweetid></tweetid> <digest><![CDATA[尊敬的*先生:
您****个人信用卡**月账单
账单日期 :****年**月**日
到期还款日:**月**日
人民币账单金额:¥****.**
最低还款额 :¥***.**
美元账单金额 :$0.00
最低还款额 :$0.00
欢迎点击下方菜单★查账-账单分期★
【限时优惠】分期满1万享最高100元还款金,首次分期不限金额赠1000积分!
点详情,可查账单明细。]]></digest> <fileid>0</fileid> <sources> <source> <name><![CDATA[****信用卡]]></name> </source> </sources> <styles><topColor><![CDATA[#00CD13]]></topColor>
<style>
<range><![CDATA[{0,7}]]></range>
<font><![CDATA[s]]></font>
<color><![CDATA[#000000]]></color>
</style>
<style>
<range><![CDATA[{9,15}]]></range>
<font><![CDATA[s]]></font>
<color><![CDATA[#000000]]></color>
</style>
<style>
<range><![CDATA[{31,11}]]></range>
<font><![CDATA[s]]></font>
<color><![CDATA[#000000]]></color>
</style>
<style>
<range><![CDATA[{49,6}]]></range>
<font><![CDATA[s]]></font>
<color><![CDATA[#000000]]></color>
</style>
<style>
<range><![CDATA[{56,132}]]></range>
<font><![CDATA[s]]></font>
<color><![CDATA[#000000]]></color>
</style>
</styles> <native_url></native_url> <del_flag>0</del_flag> <contentattr>0</contentattr> </item> </category> <publisher> <username><![CDATA[********]]></username> <nickname><![CDATA[****信用卡]]></nickname> </publisher> <template_header></template_header> <template_detail></template_detail> </mmreader> <thumburl><![CDATA[]]></thumburl> <template_id><![CDATA[AMMnybAIpfuc7TeYIyOTtcP7AUPyuazTHJrP3hdJwxF]]></template_id> </appmsg><fromusername><![CDATA[********]]></fromusername><appinfo><version>0</version><appname><![CDATA[****信用卡]]></appname><isforceupdate>1</isforceupdate></appinfo></msg>
- nMsgType==50(语音聊天)
<voipinvitemsg><roomid>********</roomid><key>>********</</key><status>2</status><invitetype>0</invitetype></voipinvitemsg>
- nMsgType==52(视频聊天)
[视频聊天]对方已拒绝
- nMsgType==62(视频)
同nMsgType==43
- nMsgType==10000(系统提示消息,比如红包接受/XX邀请YY加入群聊)
- nMsgType==10002(消息撤回提示)
消息媒体文件映射
当消息不是简单消息类型的时候,其真实多媒体对象是存放在文件系统里的,缩略图存放在thumbnail目录,图片放在image目录,视频放在video目录,音频放在voice目录。那么怎么从消息映射到具体的媒体文件呢?答案就在字段bytesXmlData里面。这是一个xml序列化好的对象,存储了消息对应的原始图片和缩略图等信息的文件地址,比如下面一条图片消息对应的bytesXmlData,我们可以从中得到图片的真实位置storage/********/image/1122371016_0_recv.jpg,以及缩略图的真实位置storage/********/thumbnail/1122371016.jpg。其他消息类型都类似。
<MsgXmlData xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/MicroMsg.Storage"><isGifRead>false</isGifRead><isVoiceRead>false</isVoiceRead><msgSource></msgSource><strImagePath>storage/********/image/1122371016_0_recv.jpg</strImagePath><strThumbnail>storage/********/thumbnail/1122371016.jpg</strThumbnail></MsgXmlData>
如果需要写程序去反序列化如上内容,只需要使用以下代码即可。
public static T loadFromBuffer(byte[] xmlBuffer) where T : class
{
if (xmlBuffer == null || xmlBuffer.Length <= 0)
{
return default(T);
}
try
{
MemoryStream memoryStream = new MemoryStream(xmlBuffer);
DataContractSerializer dataContractSerializer = new DataContractSerializer(typeof(T));
return dataContractSerializer.ReadObject(memoryStream) as T;
}
catch (Exception ex)
{
Log.e("storage", "StorageXml read objcet fail " + ex);
}
return default(T);
}
[DataContract]
public class MsgXmlData
{
[DataMember]
public bool isVoiceRead;
[DataMember]
public string strThumbnail;
[DataMember]
public string strImagePath;
[DataMember]
public bool isGifRead;
[DataMember]
public string msgSource;
[DataMember]
public ulong svrID64;
}
总结
经过以上分析,我们对微信的数据库结构和文件系统就有了基本认识,基于这些认识,我们可以轻松的写程序去抽取我们想要的内容。本来希望微信程序本身能够提供导出功能,或者消息云端备份与查看(类似于QQ),这样所有消息都不会丢失。然而现在微信只提供了消息从一个设备迁移到另一个设备的功能,也算是一种遗憾吧。Anyway,能自己动手搞定也就不是问题了,同时也对微信的文件存储有了一定了解。
更进一步,我们甚至可以添加不存在的消息,编辑已有的消息,创建不存在的聊天记录!那些通过微信截图来散布消息的,真假又有谁知道呢?