Java自制微信聊天图片自动保存软件

一、前言

这个demo主要是整合了网上的一些方法。这只是个简易的demo,可以在此基础上增加其它的业务逻辑,比如ocr识别等需要搜集微信图片的场景,把图片先存下来再进行其它操作。

实现思路

  1. 可以借助三方的的对话机器人 SDK,如 Wechaty https://wechaty.gitbook.io/wechaty/v/zh/

优点:不用跟微信应用绑定,更灵活,还有收发消息的功能
缺点:不是官方工具,账号可能有风险,最好使用小号;相对可靠的工具会收费

  1. 本文采用该方法:将桌面版微信开启 “保留聊天记录”,“开启文件自动下载”,之后 会话图片会被保存在本地,图片会被保存为 .dat 扩展名的加密文件,不能直接查看,需要解密。 所以只需要监控微信聊天记录保存的目录,有 dat 文件新增时,把它解密成图片格式再存到其它的文件夹里就行了。

优点:无风险,可靠性更高;整合 Spring 等框架更方便,业务更稳定。
缺点:不灵活,程序需要与微信在同一台机器上运行;微信目前没有Linux版本,只能部署在Mac或Windows系统

二、代码

项目为maven项目,jdk1.8

2.1 pom 文件


<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>
    
    <groupId>org.examplegroupId>
    <artifactId>wechat-image-pullerartifactId>
    <version>1.0-SNAPSHOTversion>

    <properties>
        <maven.compiler.source>8maven.compiler.source>
        <maven.compiler.target>8maven.compiler.target>
    properties>
    
    <dependencies>
        
        <dependency>
            <groupId>org.slf4jgroupId>
            <artifactId>slf4j-apiartifactId>
            <version>1.7.36version>
        dependency>
        
        <dependency>
            <groupId>org.slf4jgroupId>
            <artifactId>slf4j-reload4jartifactId>
            <version>1.7.36version>
            <scope>testscope>
        dependency>
        
        <dependency>
            <groupId>commons-iogroupId>
            <artifactId>commons-ioartifactId>
            <version>2.11.0version>
        dependency>
        
        <dependency>
            <groupId>cn.hutoolgroupId>
            <artifactId>hutool-coreartifactId>
            <version>5.8.5version>
        dependency>
        
        <dependency>
            <groupId>cn.hutoolgroupId>
            <artifactId>hutool-logartifactId>
            <version>5.8.5version>
        dependency>
        
        <dependency>
            <groupId>cn.hutoolgroupId>
            <artifactId>hutool-settingartifactId>
            <version>5.8.5version>
        dependency>

    dependencies>

project>

2.2 项目目录结构

Java自制微信聊天图片自动保存软件_第1张图片

2.3 dat文件解析

原理:微信存储图片的时候做了异或加密,然后将后缀修改为了dat。由于文件大小没有变化,可以很容易得到异或值,通过异或值,将文件进行字节码解码,就可以将文件还原成为图片了。
参考:https://blog.csdn.net/a627428179/article/details/95485146

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;

/**
 * 微信 DAT加密文件 转图片
 * 
 * @author: jiangxiangbo
 * @date: 2022/8/3
 */
public class DatFileParseUtil {

    private static final Log log = LogFactory.get();
    
    /**
     * 文件流的前 N个byte的16进制表示 --> 文件扩展名
     */
    private final static Map<String, String> FILE_TYPE_MAP = new HashMap<>();

    static {
        getAllFileType();
    }

    /**
     * dat文件解析
     * 
     * @param file 文件
     * @param targetPath 转换后目录
     * @return void 
     * @author jiangxiangbo
     * @date 2022/8/3
     */
    public static void parse(File file, String targetPath) {
        AtomicReference<Integer> integer = new AtomicReference<>(0);
        AtomicInteger x = new AtomicInteger();
        if (file.isFile()) {
            Object[] xori = getXor(file);
            if (xori != null && xori[1] != null){
                x.set((int)xori[1]);
            }
        }
        Object[] xor = getXor(file);
        if (x.get() == 0 && xor[1] != null && (int) xor[1] != 0) {
            x.set((int) xor[1]);
        }
        xor[1] = xor[1] == null ? x.get() : xor[1];

        // 创建目录及文件
        File desc = getAbsoluteFile(targetPath, file.getName().split("\\.")[0] + (xor[0] != null ? "." + xor[0] : ""));
        try (InputStream reader = Files.newInputStream(file.toPath());
             OutputStream writer = Files.newOutputStream(desc.toPath()))
        {
            byte[] bytes = new byte[1024 * 10];
            int b;
            while ((b = reader.read(bytes)) != -1) {
                for (int i = 0; i < bytes.length; i++) {
                    bytes[i] = (byte) (int) (bytes[i] ^ (int) xor[1]);
                    if (i == (b - 1)) {
                        break;
                    }
                }
                writer.write(bytes, 0, b);
                writer.flush();
            }
            integer.set(integer.get() + 1);
            log.info("【 parse file success, {} size: {} kb 】", file.getName(), ((double) file.length() / 1000));
        } catch (Exception e) {
            log.error("parse file error!", e);
        }
    }

    /**
     * 创建目录及文件
     *
     * @param path
     * @param fileName
     * @return java.io.File
     * @author jiangxiangbo
     * @date 2022/7/11
     */
    private static File getAbsoluteFile(String path, String fileName) {
        try {
            File desc = new File(path + File.separator + fileName);
            if (!desc.getParentFile().exists()) {
                desc.getParentFile().mkdirs();
            }
            if (!desc.exists()) {
                desc.createNewFile();
            }
            return desc;
        } catch (IOException e) {
            log.error(e, "create new file error !");
        }
        return null;
    }
    
    /**
     * @param path       图片目录地址
     * @param targetPath 转换后目录
     */
    public static void parse(String path, String targetPath) {
        File[] files = new File(path).listFiles();
        if (files == null) {
            return;
        }
        int size = files.length;
        log.info("file total is {}", size);
        AtomicInteger x = new AtomicInteger();
        for (File file1 : files) {
            if (file1.isFile()) {
                Object[] xori = getXor(file1);
                if (xori != null && xori[1] != null){
                    x.set((int)xori[1]);
                }
                break;
            }
        }
        Arrays.stream(files).parallel().forEach(itemFile -> parse(itemFile, targetPath));
        log.info("file parse success");
    }

    /**
     * 文件异或值
     *
     * @param file
     * @return
     */
    private static Object[] getXor(File file) {
        Object[] xor = null;
        if (file != null) {
            byte[] bytes = new byte[4];
            try (InputStream reader = Files.newInputStream(file.toPath())) {
                reader.read(bytes, 0, bytes.length);
            } catch (Exception e) {
                log.error("getXor error!", e);
            }
            xor = getXor(bytes);
        }
        return xor;
    }

    /**
     * 异或运算
     * 
     * @param bytes
     * @return
     */
    private static Object[] getXor(byte[] bytes) {
        Object[] xorType = new Object[2];
        int[] xors = new int[3];
        for (Map.Entry<String, String> type : FILE_TYPE_MAP.entrySet()) {
            String[] hex = {
                    String.valueOf(type.getKey().charAt(0)) + type.getKey().charAt(1),
                    String.valueOf(type.getKey().charAt(2)) + type.getKey().charAt(3),
                    String.valueOf(type.getKey().charAt(4)) + type.getKey().charAt(5)
            };
            xors[0] = bytes[0] & 0xFF ^ Integer.parseInt(hex[0], 16);
            xors[1] = bytes[1] & 0xFF ^ Integer.parseInt(hex[1], 16);
            xors[2] = bytes[2] & 0xFF ^ Integer.parseInt(hex[2], 16);
            if (xors[0] == xors[1] && xors[1] == xors[2]) {
                xorType[0] = type.getValue();
                xorType[1] = xors[0];
                break;
            }
        }
        return xorType;
    }

    /**
     * 初始化文件头信息 与文件类型映射 map
     * 

* 文件头数据来源于网络;可在此处扩展类型 * * @author jiangxiangbo * @date 2022/8/3 */ private static void getAllFileType() { FILE_TYPE_MAP.put("ffd8ffe000104a464946", "jpg"); FILE_TYPE_MAP.put("89504e470d0a1a0a0000", "png"); FILE_TYPE_MAP.put("47494638396126026f01", "gif"); FILE_TYPE_MAP.put("49492a00227105008037", "tif"); // 16色位图(bmp) FILE_TYPE_MAP.put("424d228c010000000000", "bmp"); // 24位位图(bmp) FILE_TYPE_MAP.put("424d8240090000000000", "bmp"); // 256色位图(bmp) FILE_TYPE_MAP.put("424d8e1b030000000000", "bmp"); FILE_TYPE_MAP.put("255044462d312e360d25", "pdf"); FILE_TYPE_MAP.put("504b0304140006000800", "docx"); } }

2.4 监控新增的dat文件

借助 commons-io 的 FileAlterationMonitor 监控器。

继承 FileAlterationListenerAdaptor,自定义文件监听器

import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import com.xs.puller.config.AppConfig;
import com.xs.puller.util.DatFileParseUtil;
import org.apache.commons.io.monitor.FileAlterationListenerAdaptor;

import java.io.File;

/**
 * 文件监听器
 *
 * @author: jiangxiangbo
 * @date: 2022/8/3
 */
public class DatFileListener extends FileAlterationListenerAdaptor {

    private static final Log log = LogFactory.get();
    
    /**
     * 文件创建时被触发
     * 
     * @param file The file created (ignored)
     * @return void 
     * @author jiangxiangbo
     * @date 2022/8/3
     */
    @Override
    public void onFileCreate(File file) {
        String fileName = file.getName();
        log.info("new file created, name is : {}", fileName);
        String thumbFlag = fileName.substring(fileName.length() - 6);
        if (fileName.contains(AppConfig.scanFileType) && !AppConfig.FILTER_FILE_TAG.equals(thumbFlag)) {
            DatFileParseUtil.parse(file, AppConfig.targetPath);
        }
    }

    /**
     * 文件夹创建时被触发
     *
     * @param directory The directory created (ignored)
     * @return void
     * @author jiangxiangbo
     * @date 2022/8/4
     */
    @Override
    public void onDirectoryCreate(File directory) {
        log.info("new directory created, name is : {}", directory.getName());
    }

}

把监控器做个简单的封装:

import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import com.xs.puller.config.AppConfig;
import com.xs.puller.listener.DatFileListener;
import org.apache.commons.io.monitor.FileAlterationMonitor;
import org.apache.commons.io.monitor.FileAlterationObserver;

import java.util.Arrays;

/**
 * MonitorHolder
 * 
 * @author: jiangxiangbo
 * @date: 2022/8/4
 */
public class FileAlterationMonitorHolder {

    private static final Log log = LogFactory.get();

    /** 根目录文件夹观察者 */
    private final static FileAlterationObserver DIR_OBSERVER = new FileAlterationObserver(AppConfig.scanPath);

    /** 监控器 */
    private static FileAlterationMonitor monitor;
    
    /**
     * 初始化文件监控器
     * 
     * @return void 
     * @author jiangxiangbo
     * @date 2022/8/4
     */
    public static void init() {
        // 文件观察者
        FileAlterationObserver fileObserver = new FileAlterationObserver(AppConfig.scanPath);
        fileObserver.addListener(new DatFileListener());
        monitor = new FileAlterationMonitor(AppConfig.scanInterval, Arrays.asList(fileObserver, DIR_OBSERVER));
        log.info(">>>>>>>>>>>>>>> monitor init success");
    }
    
    /**
     * 启动监控
     * 
     * @return void 
     * @author jiangxiangbo
     * @date 2022/8/4
     */
    public static void start() {
        try {
            monitor.start();
            log.info(">>>>>>>>>>>>>>> monitor start success");
        } catch (Exception e) {
            log.error(e,">>>>>>>>>>>>>>> monitor start error !");
        }
    }
    
    /**
     * 停止监控
     * 
     * @return void 
     * @author jiangxiangbo
     * @date 2022/8/4
     */
    public static void stop() {
        try {
            monitor.stop();
            log.info(">>>>>>>>>>>>>>> monitor stop success");
        } catch (Exception e) {
            log.error(e,">>>>>>>>>>>>>>> monitor stop error !");
        }
    }
    
    private FileAlterationMonitorHolder() {}
}

说明

这里已经可以实现功能了,只要在main方法中执行即可:

import com.xs.puller.frame.AppFrame;

/**
 * 启动类
 * 
 * @author: jiangxiangbo
 * @date: 2022/8/3
 */
public class ImagePullerApplication {

    public static void main(String[] args) {
        /// new AppFrame();
        FileAlterationMonitorHolder.init();
        FileAlterationMonitorHolder.start();
    }
    
}

2.5 简单绘制应用界面

简单绘制一个界面,并添加相应的触发函数

package com.xs.puller.frame;

import com.xs.puller.config.AppConfig;
import com.xs.puller.context.FileAlterationMonitorHolder;

import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.border.TitledBorder;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

/**
 * @author: jiangxiangbo
 * @date: 2022/8/4
 */
public class AppFrame extends JFrame {

    public static final int WIDTH = 450;
    public static final int HEIGHT = 200;
    private JPanel contentPane;
    private JTextField scanPath, targetPath;
    
    public AppFrame() {
        this.setTitle("Wechat Image Puller");
        this.setSize(WIDTH, HEIGHT);
        this.setResizable(false);

        // 设置关闭时退出JVM
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        // 设置窗体居中
        setLocationRelativeTo(null);
        // 内容面板
        contentPane = new JPanel();
        contentPane.setBorder(new EmptyBorder(10, 5, 5, 5));
        // 设置布局
        contentPane.setLayout(new BorderLayout(1, 1));
        setContentPane(contentPane);
        // 3行1列的表格布局
        JPanel panel = new JPanel(new GridLayout(3, 1, 2, 8));
        panel.setBorder(new TitledBorder(null, "", TitledBorder.LEADING, TitledBorder.TOP, null, null));
        // 给panel添加边框
        contentPane.add(panel, BorderLayout.CENTER);

        // 第一行
        JPanel panel_1 = new JPanel();
        panel.add(panel_1);
        JLabel label = new JLabel("扫描路径:");
        panel_1.add(label);
        scanPath = new JTextField();
        panel_1.add(scanPath);
        scanPath.setColumns(25);
        scanPath.setText(AppConfig.scanPath);

        // 第二行
        JPanel panel_2 = new JPanel();
        panel.add(panel_2);
        JLabel label2 = new JLabel("存储文件夹:");
        panel_2.add(label2);
        targetPath = new JTextField();
        panel_2.add(targetPath);
        targetPath.setColumns(25);
        targetPath.setText(AppConfig.targetPath);

        // 第三行
        JPanel panel_3 = new JPanel();
        panel.add(panel_3);
        JButton jbOk = new JButton("开始");
        panel_3.add(jbOk);
        JButton jbStop = new JButton("停止");
        panel_3.add(jbStop);
        jbStop.setEnabled(false);
        
        this.setVisible(true);
        this.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);
            }
        });

        // 给开始按钮添加事件
        jbOk.addActionListener(e -> {
            AppConfig.setScanPath(scanPath.getText());
            AppConfig.setTargetPath(targetPath.getText());

            FileAlterationMonitorHolder.init();
            FileAlterationMonitorHolder.start();
            
            jbOk.setEnabled(false);
            jbStop.setEnabled(true);
            scanPath.setEditable(false);
            targetPath.setEditable(false);
        });

        // 给结束按钮添加事件
        jbStop.addActionListener(e -> {
            FileAlterationMonitorHolder.stop();
            jbOk.setEnabled(true);
            jbStop.setEnabled(false);
            scanPath.setEditable(true);
            targetPath.setEditable(true);
        });
    }
    
}

通过界面启动:

import com.xs.puller.frame.AppFrame;

/**
 * 启动类
 * 
 * @author: jiangxiangbo
 * @date: 2022/8/3
 */
public class ImagePullerApplication {
    public static void main(String[] args) {
        new AppFrame();
///        FileAlterationMonitorHolder.init();
///        FileAlterationMonitorHolder.start();
    }
}

效果:
Java自制微信聊天图片自动保存软件_第2张图片

2.6 其它类代码

applcation.properties

# 扫描文件扩展名
scanFileType=.dat
# 扫描路径
scanPath=C:/Users/xs/Documents/WeChat Files/wxid_zxgfcregd22/FileStorage/MsgAttach
# 扫描间隔 ms
scanInterval=500
# 目标文件夹
targetPath=D:/data/wechat/temp

AppConfig

package com.xs.puller.config;

import cn.hutool.setting.dialect.Props;

/**
 * 应用配置
 * 
 * @author: jiangxiangbo
 * @date: 2022/8/4
 */
public class AppConfig {
    
    /** 过滤缩略图 */
    public final static String FILTER_FILE_TAG = "_t.dat";
    
    private final static Props PROPS =  new Props("application.properties");
    
    /** 扫描文件扩展名 */
    public static String scanFileType = PROPS.getStr("scanFileType");
    
    /** 扫描路径 */
    public static String scanPath = PROPS.getStr("scanPath");
    
    /** 扫描间隔 ms */
    public static Long scanInterval = PROPS.getLong("scanInterval");

    /** 目标路径 */
    public static String targetPath = PROPS.getStr("targetPath");

    public static String getScanFileType() {
        return scanFileType;
    }

    public static void setScanFileType(String scanFileType) {
        AppConfig.scanFileType = scanFileType;
    }

    public static String getScanPath() {
        return scanPath;
    }

    public static void setScanPath(String scanPath) {
        AppConfig.scanPath = scanPath;
    }

    public static Long getScanInterval() {
        return scanInterval;
    }

    public static void setScanInterval(Long scanInterval) {
        AppConfig.scanInterval = scanInterval;
    }

    public static String getTargetPath() {
        return targetPath;
    }

    public static void setTargetPath(String targetPath) {
        AppConfig.targetPath = targetPath;
    }
    
}

2.7 打包并转换为exe可执行文件

参考:https://blog.csdn.net/weixin_47160526/article/details/123496190

2.8 小程序下载链接

链接:https://pan.baidu.com/s/1EgVjqgflYo6X7H3ve9qKcw?pwd=a8su
提取码:a8su

你可能感兴趣的:(经验分享,java,微信,开发语言)