经由同个文件多次压缩的文件MD5都不一样问题排查,感慨AI的强大!
开心一刻
今天点了个外卖:牛肉炒饭
外卖到了后,发现并没有牛肉,我找商家理论
我:老板,这个牛肉炒饭的配菜是哪些?
商家:青菜 豆芽 火腿 鸡蛋 葱花
我:没有牛肉?
商家:亲,没有的哦
我:我点的牛肉炒饭没有牛肉,你这不是虚假宣传?
商家:亲,你误会了,牛肉是我们的厨师名字!

问题描述
先跟大家统一一个概念:文件的MD5
,它是一种用于验证文件完整性的哈希值,一个文件的MD5值是固定的。文件的MD5值获取方式有多种,Linux 下可以通过 md5sum
命令获取
[root@k8s-master opt]# md5sum run.sh

Win 下则通过 certutil
命令获取
D:\>certutil -hashfile run.sh MD5

各个开发语言也有对应的获取方式,例如 Java
/**
* 通过 JDK 获取文件的MD5
* @author 青石路
*/
public static String getFileMd5ByJdk(Path path) throws Exception {
MessageDigest digest = MessageDigest.getInstance("MD5");
try (InputStream fis = Files.newInputStream(path)) {
byte[] byteArray = new byte[1024];
int bytesCount = 0;
while ((bytesCount = fis.read(byteArray)) != -1) {
digest.update(byteArray, 0, bytesCount);
}
}
byte[] bytes = digest.digest();
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
但是我们通常会用第三方组件或框架来实现,例如
Apache Commons Codec
/**
* 通过 Apache Commons Codec 获取文件的MD5
* @author 青石路
*/
public static String getFileMd5ByCodec(Path path) throws IOException {
try(InputStream is = Files.newInputStream(path)) {
return DigestUtils.md5Hex(is);
}
}
Guava
/**
* 通过 Guava 获取文件的MD5
* @author 青石路
*/
public static String getFileMd5ByGuava(Path path) throws IOException {
return com.google.common.io.Files.hash(path.toFile(), Hashing.md5()).toString();
}
Hutool
/**
* 通过 Spring 获取文件的MD5
* @author 青石路
*/
public static String getFileMd5BySpring(Path path) throws IOException {
try(InputStream is = Files.newInputStream(path)) {
return org.springframework.util.DigestUtils.md5DigestAsHex(is);
}
}
Spring
/**
* 通过 Hutool 获取文件的MD5
* @author 青石路
*/
public static String getFileMd5ByHutool(Path path) throws IOException {
try(InputStream is = Files.newInputStream(path)) {
return DigestUtil.md5Hex(is);
}
}
这些方式获取的 MD5 值都是一致的,都是 cf51e1e40cd1964827bf02916231be85

至此,相信你们对 文件的MD5
都理解了;接下来回到正题,我先复现下问题,既然是压缩,那就把压缩代码整起来,基于 zip4j
实现 zip 压缩
引入依赖
<dependency>
<groupId>net.lingala.zip4j</groupId>
<artifactId>zip4j</artifactId>
<version>2.11.3</version>
</dependency>
实现 zip 压缩
/**
* zip 压缩
* @author 青石路
* @param destFilePath 压缩文件路径
* @param sources 源文件列表
* @throws IOException 压缩异常
*/
public static void compressZip(String destFilePath, List<File> sources) throws IOException {
try(ZipFile zipFile = new ZipFile(destFilePath)) {
for (File sourceFile : sources) {
ZipParameters param = new ZipParameters();
param.setCompressionMethod(CompressionMethod.DEFLATE);
param.setCompressionLevel(CompressionLevel.NORMAL);
param.setFileNameInZip(sourceFile.getName());
try (FileInputStream is = new FileInputStream(sourceFile)) {
zipFile.addStream(is, param);
}
}
}
}
代码很简单,相信你们都能看懂;照理来说,只要源文件列表(sources
)的顺序是固定的,那么压缩之后得到的zip包文件的MD5就应该是一致的,对不对?我们来看一个案例
public static void main(String[] args) throws Exception {
List<File> sources = new ArrayList<>();
sources.add(new File("D:\\run.sh"));
sources.add(new File("D:\\hello.txt"));
String zip1 = "D:\\qsl1.zip";
String zip2 = "D:\\qsl2.zip";
compressZip(zip1, sources);
TimeUnit.SECONDS.sleep(1);
compressZip(zip2, sources);
System.out.println("zip1 MD5:" + getFileMd5ByCodec(Paths.get(zip1)));
System.out.println("zip2 MD5:" + getFileMd5ByCodec(Paths.get(zip2)));
}
这个代码你们肯定都能看懂,但我还是要强调一下
两次压缩间隔了 1 秒,是模拟实际项目中的两次压缩的时间间隔
实际项目中间隔肯定不止 1 秒,设置成 1 秒是为了达到同样效果的同时快速出结果
执行如上代码,结果如下

两个压缩包的 MD5 不一致

这是为什么?
问题排查
源文件列表 sources
是同一个(文件一致、顺序也一致),打包方法也是同一个(compressZip
),为什么得到的压缩包的MD5会不一致?会不会是 Codec
组件(因为用的 getFileMd5ByCodec
方法获取的压缩包的MD5)的问题,后面切成 getFileMd5ByJdk

结果与 getFileMd5ByCodec
一致,这说明获取文件的MD5是没问题的;莫非是压缩包名的问题?这个我们可以反向验证下,同个文件复制一份,验证下复制文件的MD5与源文件的MD5是不是一致

可以看到,复制文件的MD5与源文件的MD5一致,所以问题应该出在 compressZip
上,具体出在哪,我也没有可排查的方向了;此时,换做是你们,你们会怎么排查?现在 AI
这么火热,不得问问它?

讯飞星火
给出了 4 个方向,我们逐一分析下
时间戳
是指 ZIP 包的创建时间和修改时间
还是指 ZIP 包中文件的创建时间和修改时间
有待进一步分析
压缩算法版本
这个可以排除,因为用的是同个压缩打包方法:
compressZip
,并且从上图可以看出,压缩算法都是:Deflate
,版本都是20
文件系统差异
这个也可以排除,都是基于 Win10 的
FAT
文件系统随机数或唯一标识符
这个也可以排除,没有随机数和唯一标识
所以我们需要重点分析下时间戳,时间戳又分两个方向
压缩包的时间戳
还记得前面压缩包名的验证吗,复制文件和源文件的MD5一致,也变相验证了文件MD5不受压缩包的
创建时间
的影响源文件和复制文件的
创建时间
与访问时间
不一致,但文件MD5一致,说明文件MD5与创建时间
、访问时间
无关所以我们只需要验证下文件MD5是不是与压缩包的
修改时间
有关即可;很好验证,只需要修改复制文件的修改时间,然后再比较两个文件的MD5,实例代码如下public static void main(String[] args) throws Exception {
/*List<File> sources = new ArrayList<>();
sources.add(new File("D:\\run.sh"));
sources.add(new File("D:\\hello.txt"));*/
String zip1 = "D:\\qsl1.zip";
String zip2 = "D:\\qsl1 - 副本.zip";
/*compressZip(zip1, sources);
TimeUnit.SECONDS.sleep(1);
compressZip(zip2, sources);*/
System.out.println("zip1 MD5:" + getFileMd5ByJdk(Paths.get(zip1)));
File file2 = new File(zip2);
Path path2 = file2.toPath();
// 将副本文件的修改时间增加1分钟
Files.setLastModifiedTime(path2, FileTime.fromMillis(file2.lastModified() + (60 * 1000)));
System.out.println("zip2 MD5:" + getFileMd5ByJdk(path2));
}
执行结果如下
所以我们可以得出结论
压缩文件的MD5与压缩包的
修改时间
无关那么再结合前面的
创建时间
、访问时间
,说明压缩包的MD5与压缩包的时间戳无关!引申一个问题:非压缩文件MD5是否与其时间戳有关?
压缩包中文件的时间戳
这个很好验证,源文件打包进压缩包的时候,保留其修改时间即可,代码如下
/**
* zip 压缩
* @author 青石路
* @param destFilePath 压缩文件路径
* @param sources 源文件列表
* @throws IOException 压缩异常
*/
public static void compressZip(String destFilePath, List<File> sources) throws IOException {
try(ZipFile zipFile = new ZipFile(destFilePath)) {
for (File sourceFile : sources) {
ZipParameters param = new ZipParameters();
param.setCompressionMethod(CompressionMethod.DEFLATE);
param.setCompressionLevel(CompressionLevel.NORMAL);
param.setFileNameInZip(sourceFile.getName());
// 保留源文件的修改时间
param.setLastModifiedFileTime(sourceFile.lastModified());
try (FileInputStream is = new FileInputStream(sourceFile)) {
zipFile.addStream(is, param);
}
}
}
}
删除旧压缩包后重新进行打包测试
public static void main(String[] args) throws Exception {
List<File> sources = new ArrayList<>();
sources.add(new File("D:\\run.sh"));
sources.add(new File("D:\\hello.txt"));
String zip1 = "D:\\qsl1.zip";
String zip2 = "D:\\qsl2.zip";
compressZip(zip1, sources);
// 压缩间隔1分钟
TimeUnit.MINUTES.sleep(1);
compressZip(zip2, sources);
System.out.println("zip1 MD5:" + getFileMd5ByJdk(Paths.get(zip1)));
File file2 = new File(zip2);
Path path2 = file2.toPath();
System.out.println("zip2 MD5:" + getFileMd5ByJdk(path2));
}
执行结果如下
所以我们可以得出结论
压缩包的MD5与压缩包中文件的
修改时间
有关压缩包的MD5否与压缩包中文件的
创建时间
、访问时间
有关,这个交由你们去验证了!
问题修复
如何修复,前面已经讲过了,就是增加一个压缩参数
param.setLastModifiedFileTime(sourceFile.lastModified());
保留源文件的 修改时间
即可;既然前面已经讲过了,为什么还要单独拿个章节来讲?仅仅只是强调下,你们要是不服,来打我呀!

小插曲
Win10 文件夹和文件的 修改日期
只显示到分钟,不显示秒

这也导致解压工具打开压缩包的界面也只显示到分钟

这很容易让我们产生错觉
两次压缩的修改时间(压缩包以及压缩包中的文件)为什么是一样的?
修改时间一致,怎么压缩包的MD5还不一致?
然后就开始自我质疑了,到底哪个环节出了问题?

如果你们看的比较细致的话,会发现我将压缩间隔时间从之前的 1 秒调整成了 1 分钟,因为我就产生了错觉,不怕你们笑话,我在这个错觉上还折腾了挺长时间!!!

另外,7z工具可以查看压缩包中文件的修改时间到秒级别

至于Win10,我始终没有折腾出文件夹和文件的 修改日期
显示到秒

总结
- 非压缩文件的MD5与文件的时间戳无关
- 压缩文件的MD5与压缩文件的时间戳无关,但与压缩包中文件的
修改时间
有关 - Win10 文件夹和文件的
修改日期
只显示到分钟,不显示秒,不显示秒,不显示秒!!! - AI 的愈发成熟,带来了便利的同时也带来了挑战,工作经验的优势会越来越弱,
35
这个坎会持续提前!!!
经由同个文件多次压缩的文件MD5都不一样问题排查,感慨AI的强大!的更多相关文章
- 三、Socket之UDP异步传输文件-多文件传输和文件MD5校验
本文接着上一篇文章二.Socket之UDP异步传输文件,在上一篇文章的基础上实现多文件的传输和文件传输完成后进行完整性校验. 要实现多文件的传输,必须要对文(2)中发送文件的数据格式进行改进,必须加入 ...
- MD5算法【计算文件和字符串的MD5值】
1. MD5算法是一种散列(hash)算法(摘要算法,指纹算法),不是一种加密算法(易错).任何长度的任意内容都可以用MD5计算出散列值.MD5的前身:MD2.MD3.MD4.介绍工具:CalcMD5 ...
- 《java入门第一季》之对文件和字符串进行MD5加密工具类
上一篇介绍了MD5加密算法,之前写的代码有些冗余,而且可读性很差.今天把对文本数据的加密,以及获取文件的md5值做一个封装类.代码如下: package com.itydl.utils; import ...
- JAVA 文件读取写入后 md5值不变的方法
假如我们想把某文件读入 StringBuffer 并写入新文件,新文件md5值需要保持不变(写入新文件后保证和源文件一模一样), 我们就需要在操作 StringBuffer 时附加换行符: Strin ...
- 此文件时入口文件index.php
此文件时入口文件index.php <?php //定义一下ThinkPHP框架存放的路径 define('THINK_PATH','./ThinkPHP/'); //定义当前的项目的名称,此处 ...
- s14 第5天 时间模块 随机模块 String模块 shutil模块(文件操作) 文件压缩(zipfile和tarfile)shelve模块 XML模块 ConfigParser配置文件操作模块 hashlib散列模块 Subprocess模块(调用shell) logging模块 正则表达式模块 r字符串和转译
时间模块 time datatime time.clock(2.7) time.process_time(3.3) 测量处理器运算时间,不包括sleep时间 time.altzone 返回与UTC时间 ...
- MYSQL数据库的套接字文件,pid文件,表结构文件
socket文件:当用Unix域套接字方式进行连接时需要的文件. pid文件:MySQL实例的进程ID文件. MySQL表结构文件:用来存放MySQL表结构定义文件. 套接字文件 Unix系统下本地连 ...
- 使用Jacksum对文件夹和文件生成checksum
Jacksum 是一个java开源工具, 用来 给单个文件生成checksum, 也可以给整个文件中所有文件生成checksum,生产的checksum 可以是MD系列,也可sha. 你可以参考 官 ...
- python文件封装成*.exe文件(单文件和多文件)
环境:win10 64位 python3.7 单*.py文件打包Python GUI:程序打包为exe 一.安装Pyinstaller,命令pip install Pyinstaller,(大写的P ...
- tornado上传大文件以及多文件上传
tornado上传大文件问题解决方法 tornado默认上传限制为低于100M,但是由于需要上传大文件需求,网上很多说是用nginx,但我懒,同时不想在搞一个服务了. 解决方法: server = H ...
随机推荐
- 关于 K8s 的一些基础概念整理-补充【k8s系列之五】
〇.前言 本文继续整理下 K8s 的一些基础概念,作为前一篇概念汇总的补充. 前一篇博文链接:https://www.cnblogs.com/hnzhengfy/p/k8s_concept.html. ...
- Python中定位元素包含文本信息的详细解析与代码示例
在Python编程中,特别是在进行网页自动化测试或数据抓取时,定位包含特定文本信息的元素是一个常见的需求.通过合适的工具和库,可以高效地查找和操作这些元素.本文将详细介绍如何在Python中定位包含文 ...
- Solon v3.0.5 发布!(Spring 生态可以退休了吗?)
Solon 框架! 新一代,面向全场景的 Java 应用开发框架.从零开始构建(非 java-ee 架构),有灵活的接口规范与开放生态. 追求: 更快.更小.更简单 提倡: 克制.高效.开放.生态 有 ...
- FFmpeg命令行示例
1 提取视频流/音频流 // 分离视频流和音频流 ffmpeg -i input_file -vcodec copy -an output_file_video ffmpeg -i input_fil ...
- OpenMMLab AI实战营 第六课笔记
OpenMMLab AI实战营 第六课笔记 目录 OpenMMLab AI实战营 第六课笔记 1.什么是语义分割 1.1 语义分割的应用 1.1.1 应用:无人驾驶汽车 1.1.2 应用:人像分割 1 ...
- [Git][基本原理与命令]
引言 Git是工作中最常用的版本控制工具,本文中将介绍其常用的命令. 根据作用的不同,可以分为基本命令.撤销命令.合并命令与远程仓库命令,下面将依次介绍这些命令. 基本原理 git 中提供了底层api ...
- 小程序分享pdf文件(uniapp)
share(){ wx.downloadFile({ url: '', // 下载url success (res) {// 下载完成后转发 wx.shareFileMessage({ filePat ...
- 如何在 ASP.NET Core 中实现速率限制?
在 ASP.NET Core 中实现速率限制(Rate Limiting)中间件可以帮助你控制客户端对 API 的请求频率,防止滥用和过载.速率限制通常用于保护服务器资源,确保服务的稳定性和可用性. ...
- Linux密钥rsa加密原理和ssh使用密钥实现免密码登录
1.公私钥简介与原理 公钥和私钥都属于非对称加密算法的一个实现,这个加密算法的信息交换过程是: 1) 持有公钥的一方(甲)在收到持有私钥的一方(乙)的请求时,甲会在自己的公钥列表中查找是否有乙的公钥, ...
- matlib:图像旋转-缩放
需求 使用MATLAB尝试完成一个自定义的图像攻击软件,功能描述: 1)根据输入参数,完成旋转功能 2)根据输入参数,完成缩放功能 开始 旋转 参数:参数为正,顺时针旋转:参数为负,逆时针旋转 主要代 ...