Android 微信聊天记录、联系人备份并导出为表格
(github代码会及时更新,更完整的代码请参考末文的 github 链接)
最近公司要求做一个项目,实现备份和导出虚拟代表和医生的微信聊天记录的功能,于是想了一下可从以下两个方面入手,并分析了一下他们的优劣势
- 解密微信数据库,直接用 Sql 语句查询导表上传
- 直接操作数据库,联系人和聊天记录完整,不会有遗漏
- 相比自动化更加省时不止一点点...10秒钟与十分钟的差别
- 失败率很低.并能控制只上传某个时间段的聊天记录,直接定位某句话的时间
- 但是手机需要 root
- 通过AccessibilityService在微信界面自动化操作实现获取联系人和聊天记录
- 不需要 root ,仅需要在设置里打开辅助功能就能实现自动化操作
- AccessibilityService会自动滚动聊天列表和聊天详情捕捉元素获取联系人和聊天记录,所以缺点也很多:
- 耗时较长,聊天对象和聊天记录越多,需要滚动的次数越多,越耗时
- 受外界影响很大,来电话可能就直接失败了.....
- 具体聊天记录的时间不好捕捉
- 可能会有重复的联系人和聊天对话(当前屏幕只显示一半时,滚动到下一屏时仍会捕捉显示,当然这个是可以通过程序优化的)
- 受微信版本的影响很大,可能更新微信版本整个程序得重新适配或者没法用了
利用AccessibilityService虽然业务上实现起来不太靠谱,但是可玩性还是很高的,自动化的操作看起来很牛逼很高大上,有灵感的同学可以自己再写一些功能,比如自动回复,自动拉人,自动抢红包等....,下次更博贴AccessibilityService的实现源码
https://blog.csdn.net/zk94_Android/article/details/84652992
所以在业务上还是选择直接操作数据库的方法更加靠谱,下面开始实操
* 微信数据库的加密方式:
1.获取手机的 IME 码
2.获取当前登录微信账号的uin,位置在/data/data/com.tencent.mm/shared_prefs/auth_info_key_prefs.xml
3.拼接IMEI和uin,并进行 MD5 加密
4.取 MD5 加密的前七位(全小写)就是数据库的打开密码
贴代码:
本章代码已经是很老的一个版本的,需要最新源码的可以移步最底部的 github 获取
添加依赖:
implementation 'dom4j:dom4j:1.6.1'
implementation 'net.zetetic:android-database-sqlcipher:3.5.4@aar'
implementation 'com.github.threekilogram:ObjectBus:2.1.3'
implementation 'com.wang.avi:library:1.0.0'
implementation 'com.nineoldandroids:library:2.4.0'
csv 的依赖需要去官网下载 jar 包再 build path,下载地址:
http://commons.apache.org/proper/commons-csv/download_csv.cgi
需要用到的一些成员变量
String WXPackageName = "com.tencent.mm";
private static final ObjectBus task = com.threekilogram.objectbus.bus.ObjectBus.newList();
//微信数据库路径
public final String WX_ROOT_PATH = "/data/data/com.tencent.mm/";
private final String WX_DB_DIR_PATH = WX_ROOT_PATH + "MicroMsg";
private final String WX_DB_FILE_NAME = "EnMicroMsg.db";
//拷贝到sd 卡的路径
private String mCcopyPath = Environment.getExternalStorageDirectory().getPath() + "/";
private final String COPY_WX_DATA_DB = "wx_data.db";
String copyFilePath = mCcopyPath + COPY_WX_DATA_DB;
private SharedPreferences preferences;
private static CSVPrinter contactCsvPrinter;
private static CSVPrinter messageCsvPrinter;
1.获取 root 权限并拷贝,打开数据库
//获取root权限
passwordUtiles.execRootCmd("chmod 777 -R " + WX_ROOT_PATH);
//获取root权限
passwordUtiles.execRootCmd("chmod 777 -R " + copyFilePath);
String password = passwordUtiles.initDbPassword(mActivity);
String uid = passwordUtiles.initCurrWxUin();
try {
String path = WX_DB_DIR_PATH + "/" + Md5Utils.md5Encode("mm" + uid) + "/" + WX_DB_FILE_NAME;
Log.e("path", copyFilePath);
Log.e("path", path);
Log.e("path", password);
//微信原始数据库的地址
File wxDataDir = new File(path);
//将微信数据库拷贝出来,因为直接连接微信的db,会导致微信崩溃
copyFile(wxDataDir.getAbsolutePath(), copyFilePath);
//将微信数据库导出到sd卡操作sd卡上数据库
openWxDb(new File(copyFilePath), mActivity, password);
} catch (Exception e) {
Log.e("path", e.getMessage());
e.printStackTrace();
}
2.passwordUtiles
package com.naxions.www.wechathelper;
import android.content.Context;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.OutputStream;
import java.util.List;
public class passwordUtiles {
public static final String WX_ROOT_PATH = "/data/data/com.tencent.mm/";
private static final String WX_SP_UIN_PATH = WX_ROOT_PATH + "shared_prefs/auth_info_key_prefs.xml";
/**
* 根据imei和uin生成的md5码获取数据库的密码
*
* @return
*/
public static String initDbPassword(Context mContext) {
String imei = initPhoneIMEI(mContext);
String uin = initCurrWxUin();
Log.e("initDbPassword","imei==="+imei);
Log.e("initDbPassword","uin==="+uin);
try {
if (TextUtils.isEmpty(imei) || TextUtils.isEmpty(uin)) {
Log.e("initDbPassword","初始化数据库密码失败:imei或uid为空");
return "";
}
String md5 = Md5Utils.md5Encode(imei + uin);
String password = md5.substring(0, 7).toLowerCase();
Log.e("initDbPassword",password);
return password;
}catch (Exception e){
Log.e("initDbPassword",e.getMessage());
}
return "";
}
/**
* execRootCmd("chmod 777 -R " + WX_ROOT_PATH);
*
* 执行linux指令
*/
public static void execRootCmd(String paramString) {
try {
Process localProcess = Runtime.getRuntime().exec("su");
Object localObject = localProcess.getOutputStream();
DataOutputStream localDataOutputStream = new DataOutputStream((OutputStream) localObject);
String str = String.valueOf(paramString);
localObject = str + "\n";
localDataOutputStream.writeBytes((String) localObject);
localDataOutputStream.flush();
localDataOutputStream.writeBytes("exit\n");
localDataOutputStream.flush();
localProcess.waitFor();
localObject = localProcess.exitValue();
} catch (Exception localException) {
localException.printStackTrace();
}
}
/**
* 获取手机的imei码
*
* @return
*/
private static String initPhoneIMEI(Context mContext) {
TelephonyManager telephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
return telephonyManager.getDeviceId();
}
/**
* 获取微信的uid
* 微信的uid存储在SharedPreferences里面
*/
public static String initCurrWxUin() {
String mCurrWxUin = null;
//存储位置为\data\data\com.tencent.mm\shared_prefs\auth_info_key_prefs.xml
File file = new File(WX_SP_UIN_PATH);
try {
FileInputStream in = new FileInputStream(file);
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(in);
Element root = document.getRootElement();
List<Element> elements = root.elements();
for (Element element : elements) {
if ("_auth_uin".equals(element.attributeValue("name"))) {
mCurrWxUin = element.attributeValue("value");
}
}
return mCurrWxUin;
} catch (Exception e) {
e.printStackTrace();
Log.e("initCurrWxUin","获取微信uid失败,请检查auth_info_key_prefs文件权限");
}
return "";
}
}
3.Md5Utils
package com.naxions.www.wechathelper;
import android.text.TextUtils;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class Md5Utils {
/***
* MD5加密 生成32位md5码
* @param
* @return 返回32位md5码
*/
public static String md5Encode(String inStr) throws Exception {
MessageDigest md5 = null;
try {
md5 = MessageDigest.getInstance("MD5");
} catch (Exception e) {
System.out.println(e.toString());
e.printStackTrace();
return "";
}
byte[] byteArray = inStr.getBytes("UTF-8");
byte[] md5Bytes = md5.digest(byteArray);
StringBuffer hexValue = new StringBuffer();
for (int i = 0; i < md5Bytes.length; i++) {
int val = ((int) md5Bytes[i]) & 0xff;
if (val < 16) {
hexValue.append("0");
}
hexValue.append(Integer.toHexString(val));
}
return hexValue.toString();
}
public static String md5(String string) {
if (TextUtils.isEmpty(string)) {
return "";
}
MessageDigest md5 = null;
try {
md5 = MessageDigest.getInstance("MD5");
byte[] bytes = md5.digest(string.getBytes());
String result = "";
for (byte b : bytes) {
String temp = Integer.toHexString(b & 0xff);
if (temp.length() == 1) {
temp = "0" + temp;
}
result += temp;
}
return result;
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return "";
}
}
一些具体方法
/**
* 复制单个文件
*
* @param oldPath String 原文件路径 如:c:/fqf.txt
* @param newPath String 复制后路径 如:f:/fqf.txt
* @return boolean
*/
public void copyFile(String oldPath, String newPath) {
try {
int byteRead = 0;
File oldFile = new File(oldPath);
if (oldFile.exists()) { //文件存在时
InputStream inStream = new FileInputStream(oldPath); //读入原文件
FileOutputStream fs = new FileOutputStream(newPath);
byte[] buffer = new byte[1444];
while ((byteRead = inStream.read(buffer)) != -1) {
fs.write(buffer, 0, byteRead);
}
inStream.close();
}
} catch (Exception e) {
Log.e("copyFile", "复制单个文件操作出错");
e.printStackTrace();
}
}
/**
* 连接数据库
*/
public void openWxDb(File dbFile, final Context mContext, String mDbPassword) {
SQLiteDatabase.loadLibs(mContext);
SQLiteDatabaseHook hook = new SQLiteDatabaseHook() {
@Override
public void preKey(SQLiteDatabase database) {
}
@Override
public void postKey(SQLiteDatabase database) {
database.rawExecSQL("PRAGMA cipher_migrate;");
}
};
//打开数据库连接
final SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(dbFile, mDbPassword, null, hook);
runRecontact(mContext, db);
}
/**
* 微信好友信息
*
* @param mContext
* @param db
*/
private void runRecontact(final Context mContext, final SQLiteDatabase db) {
task.toPool(new Runnable() {
@Override
public void run() {
getRecontactDate(db,mContext);
}
}).toMain(new Runnable() {
@Override
public void run() {
Toast.makeText(mContext, "文件导出完毕完毕", Toast.LENGTH_LONG).show();
}
}).run();
}
/**
* 获取当前用户的微信所有联系人
*/
private void getRecontactDate(SQLiteDatabase db, Context mContext) {
Cursor c1 = null;
Cursor c2 = null;
try {
//新建文件保存联系人信息
File file1 = new File(Environment.getExternalStorageDirectory().getPath() + "/"+userName+"contact_file" + ".csv");
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file1), "UTF-8"));
contactCsvPrinter = new CSVPrinter(writer, CSVFormat.DEFAULT.withHeader("userName", "nickName", "alias", "conRemark","type"));
//新建文件保存聊天记录
File file2= new File(Environment.getExternalStorageDirectory().getPath() + "/"+userName+"message_file" + ".csv");
BufferedWriter writer2 = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file2), "UTF-8")); // 防止出现乱码
messageCsvPrinter = new CSVPrinter(writer2, CSVFormat.DEFAULT.withHeader("talker", "content", "createTime", "isSend"));
} catch (IOException e) {
e.printStackTrace();
}
try {
// 查询所有联系人verifyFlag!=0:公众号等类型,群里面非好友的类型为4,未知类型2)
c1 = db.rawQuery(
"select * from rcontact where verifyFlag = 0 and type != 4 and type != 2 and type != 0 and type != 33 and nickname != ''",
null);
while (c1.moveToNext()) {
String userName = c1.getString(c1.getColumnIndex("username"));
String nickName = c1.getString(c1.getColumnIndex("nickname"));
String alias = c1.getString(c1.getColumnIndex("alias"));
String conRemark = c1.getString(c1.getColumnIndex("conRemark"));
String type = c1.getString(c1.getColumnIndex("type"));
Log.e("contact", "userName=" + userName + "nickName=" + nickName + "alias=" + alias + "conRemark=" + conRemark + "type=" + type);
//将联系人信息写入 csv 文件
contactCsvPrinter.printRecord(userName,nickName,alias,conRemark,type);
}
contactCsvPrinter.printRecord();
contactCsvPrinter.flush();
//查询聊天记录
c2 = db.rawQuery(
"select * from message where type = 1 and createTime > 1543207160000 ",
null);
while (c2.moveToNext()) {
String content = c2.getString(c2.getColumnIndex("content"));
String talker = c2.getString(c2.getColumnIndex("talker"));
String createTime = c2.getString(c2.getColumnIndex("createTime"));
int isSend = c2.getInt(c2.getColumnIndex("isSend"));
Log.e("chatInfo", "talker=" + talker + "content=" + content+ "isSend=" + isSend);
//将聊天记录写入 csv 文件
messageCsvPrinter.printRecord(talker,content,createTime,isSend);
}
messageCsvPrinter.printRecord();
messageCsvPrinter.flush();
c1.close();
c2.close();
db.close();
} catch (Exception e) {
c1.close();
c2.close();
db.close();
Log.e("openWxDb", "读取数据库信息失败" + e.toString());
}
}
到此就完成了....,导出的效果图:
奉上数据库主要表的具体字段说明:具体需要啥根据说明修改 sql 语句就好
message表
- msgid: 自增的,每段聊天记录的唯一标识
-
type: 消息类型
- 47 表情消息
- 43 视频消息
- 49 分享的网页消息
- 50 语音视频通话
- 1 文字消息
- 3 图片消息
- 34 语音消息
- 1000 撤回消息的通知
-
status: 消息阅读状态
- 2 对方已阅读
- 3 自己通过pc 端阅读该条消息
- 4 自己在手机端阅读该条消息
-
isSend:
- 1 自己发送
- 0 对方发送
-
createtime: 本条消息的时间戳
-
talker: 消息发送人
-
content: 消息具体内容
-
imgPath: 图片 语音 视频消息的路径
rcontact表
-
username: 用户标识,有两种类型
- 微信号
- 微信定义的唯一标识 gh_385a194e4ef1, wxid_f4eiifed3fjx21
-
alias: 微信号 没有设置微信号的用户为空
-
nikname: 联系人昵称
-
conRemark: 联系人备注
-
quanPin: 昵称全拼
还有一些需要注意的事情 :
1.运行软件提示".....not a dataBase" 可以尝试用备份好聊天记录之后卸载安装微信重试
2.重装无效再考虑将数据库拷贝到电脑用sqlcipher.exe(网上很多下载地址)查看是否能打开
3.手机拷贝数据库到电脑的路径:
/data/data/com.tencent.mm/MicroMsg/5d2d988ba1131f31a6c2481156b96331/EnMicroMsg.db
其中的5d2d988ba1131f31a6c2481156b96331文件夹的生成规则是:
字符串"mm"+用户的uin 再 MD5,参考代码:
String path = WX_DB_DIR_PATH + "/" + Md5Utils.md5Encode("mm" + uid) + "/" + WX_DB_FILE_NAME;
到此就结束啦....有时间在上传源码,,,
源码地址:https://github.com/KeZengOo/-wechatHelper
如果对你有帮助...就赞我一下吧