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

如果对你有帮助...就赞我一下吧

Logo

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

更多推荐