Windows Phone微信数据导出与解析

by shinichi_wtn 2016-01-03 11:37

引言

对于对任何数据都有存档备份习惯的用户来说,微信的聊天记录自然不会放过。在曾经还是短信主导的年代中,我就会备份导出所有的短信,直到现在也一直持续着,因为任何数据都有其自身的价值,随着时间的流逝,这些数据记录了人生不同时间的状态。但是,微信的使用越来越流行,生活中很多讨论都不会再用短信这种传统方式,所以目前大部分交流都会经过微信。微信已经部分取代了短信的功能,并且微信相对于短信更加丰富化,其沟通模式已经超过了传统的文本信息,文字/表情/图片/语音/视频/...都能通过微信传送,备份这种数据自然会比短信更加麻烦。

当我导出微信用户文件夹的时候,发现光图片的数量就超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,能自己动手搞定也就不是问题了,同时也对微信的文件存储有了一定了解。

更进一步,我们甚至可以添加不存在的消息,编辑已有的消息,创建不存在的聊天记录!那些通过微信截图来散布消息的,真假又有谁知道呢?

(仅用于Gavatar)

  Country flag

biuquote
  • Comment
  • Preview
Loading

About

shinichi_wtnI'm Shinichi_wtn

Software Engineering Manager at Microsoft

[More...]


Month List