这个demo主要是整合了网上的一些方法。这只是个简易的demo,可以在此基础上增加其它的业务逻辑,比如ocr识别等需要搜集微信图片的场景,把图片先存下来再进行其它操作。
优点:不用跟微信应用绑定,更灵活,还有收发消息的功能
缺点:不是官方工具,账号可能有风险,最好使用小号;相对可靠的工具会收费
优点:无风险,可靠性更高;整合 Spring 等框架更方便,业务更稳定。
缺点:不灵活,程序需要与微信在同一台机器上运行;微信目前没有Linux版本,只能部署在Mac或Windows系统
项目为maven项目,jdk1.8
<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>
原理:微信存储图片的时候做了异或加密,然后将后缀修改为了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");
}
}
借助 commons-io 的 FileAlterationMonitor 监控器。
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();
}
}
简单绘制一个界面,并添加相应的触发函数
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();
}
}
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;
}
}
参考:https://blog.csdn.net/weixin_47160526/article/details/123496190
链接:https://pan.baidu.com/s/1EgVjqgflYo6X7H3ve9qKcw?pwd=a8su
提取码:a8su