修改Windows文件创建时间,附GUI界面与打包exe教程
📌 前言
在日常工作中,我们经常会遇到需要修改文件创建时间、修改时间、访问时间的场景。比如整理论文实验数据、归档旧文件、批量处理素材等。
Windows自带的属性面板只能查看,不能直接修改文件的创建时间。网上的工具要么收费,要么带广告,还担心有病毒。作为程序员,不如自己动手写一个!
本文将手把手教你用Python实现一个带图形界面的文件属性批量修改工具,支持:
- ✅ 修改文件创建时间(最核心,也是最难实现的)
- ✅ 修改文件修改时间
- ✅ 修改文件访问时间
- ✅ 修改文件名(单文件模式)
- ✅ 单个文件模式
- ✅ 文件夹批量模式(支持递归子文件夹)
- ✅ 完美支持中文路径
- ✅ 打包成独立exe,免安装直接运行
文末附完整源码和打包好的exe下载,建议收藏备用!
🎯 功能展示
核心特性
- 双模式切换:单个文件 / 文件夹批量,一键切换
- 独立控制:四项属性(文件名、创建时间、修改时间、访问时间)可独立勾选,想改哪个改哪个
- 批量递归:文件夹模式下可选择是否递归处理子文件夹
- 实时日志:执行过程实时显示,成功失败一目了然
- 输入校验:自动校验时间格式,避免非法输入
- 中文支持:完美支持中文文件名和中文路径
🔧 技术原理
为什么修改创建时间这么难?
很多人不知道,Python标准库的 os.utime() 只能修改访问时间和修改时间 ,无法修改创建时间!
这是因为:
- Unix/Linux系统的文件系统原生只支持修改atime(访问时间)和mtime(修改时间)
- Windows虽然有创建时间(ctime)的概念,但Python标准库没有提供跨平台的修改接口
- 必须通过调用Windows原生API才能修改文件创建时间
实现方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| pywin32的SetFileTime | 接口简单 | 依赖win32timezone模块,容易出问题 |
| ctypes调用原生API | 不依赖额外模块,稳定可靠 | 需要手动处理数据结构 |
本文采用 ctypes + Windows原生API 的方案,彻底解决win32timezone模块缺失的坑,打包成exe也不会出问题。
核心技术点
- Windows FILETIME结构体:Windows文件时间是以100纳秒为单位,从1601年1月1日开始计数的64位整数
- SetFileTime API:kernel32.dll中的原生函数,可同时设置创建、访问、修改三个时间
- CreateFile获取文件句柄:需要先打开文件获取句柄才能修改属性
- 时区转换:本地时间转UTC时间,避免时区偏差
💻 完整代码
1. 环境准备
需要安装pywin32库(用于获取文件句柄):
bash
pip install pywin32
注:时间设置部分我们用ctypes原生实现,不依赖win32timezone,避免各种奇怪的报错。
2. 完整源码
python
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import os
import win32file
import win32con
import ctypes
from ctypes import wintypes
from datetime import datetime, timezone
# 定义Windows原生FILETIME结构体
class FILETIME(ctypes.Structure):
_fields_ = [
("dwLowDateTime", wintypes.DWORD),
("dwHighDateTime", wintypes.DWORD)
]
def datetime_to_filetime(dt):
"""将本地datetime转换为Windows原生FILETIME结构体,完全不依赖win32timezone"""
# 转换为UTC时间
dt_utc = dt.astimezone(timezone.utc)
# Windows FILETIME基准:1601-01-01 00:00:00 UTC,与Unix纪元相差11644473600秒
epoch_offset = 11644473600
# 转换为100纳秒为单位的整数
total_100ns = int((dt_utc.timestamp() + epoch_offset) * 10_000_000)
# 拆分为高低32位
low = total_100ns & 0xFFFFFFFF
high = (total_100ns >> 32) & 0xFFFFFFFF
return FILETIME(low, high)
def modify_file_info(file_path, rename=False, new_name="",
modify_ctime=False, ctime=None,
modify_mtime=False, mtime=None,
modify_atime=False, atime=None):
"""
修改单个文件的属性:重命名、创建时间、修改时间、访问时间
返回 (是否成功, 提示信息)
"""
try:
# 1. 处理时间修改(通过ctypes直接调用系统API,绕开win32timezone)
if modify_ctime or modify_mtime or modify_atime:
# 获取文件句柄
file_handle = win32file.CreateFile(
file_path,
win32con.GENERIC_WRITE,
win32con.FILE_SHARE_READ | win32con.FILE_SHARE_WRITE,
None,
win32con.OPEN_EXISTING,
win32con.FILE_ATTRIBUTE_NORMAL,
None
)
# 转换时间,未勾选项传None即不修改
ctime_ft = ctypes.byref(datetime_to_filetime(ctime)) if modify_ctime else None
atime_ft = ctypes.byref(datetime_to_filetime(atime)) if modify_atime else None
mtime_ft = ctypes.byref(datetime_to_filetime(mtime)) if modify_mtime else None
# 调用Windows原生SetFileTime API
ctypes.windll.kernel32.SetFileTime(
file_handle.handle,
ctime_ft,
atime_ft,
mtime_ft
)
file_handle.Close()
# 2. 处理文件重命名(仅单个文件模式启用)
if rename and new_name.strip():
dir_path = os.path.dirname(file_path)
new_full_path = os.path.join(dir_path, new_name.strip())
if os.path.exists(new_full_path):
return False, "目标文件名已存在,无法覆盖"
os.rename(file_path, new_full_path)
return True, "修改成功"
except PermissionError:
return False, "权限不足,请以管理员身份运行"
except Exception as e:
return False, f"异常:{str(e)}"
def batch_modify_folder(folder_path, recursive=False, **kwargs):
"""
批量修改文件夹下所有文件的属性
返回结果列表 [(文件路径, 是否成功, 提示信息)]
"""
results = []
if recursive:
# 递归遍历所有子目录
for root, _, files in os.walk(folder_path):
for filename in files:
full_path = os.path.join(root, filename)
success, msg = modify_file_info(full_path, **kwargs)
results.append((full_path, success, msg))
else:
# 仅处理当前目录文件
for item in os.listdir(folder_path):
full_path = os.path.join(folder_path, item)
if os.path.isfile(full_path):
success, msg = modify_file_info(full_path, **kwargs)
results.append((full_path, success, msg))
return results
class FileAttributeTool(tk.Tk):
def __init__(self):
super().__init__()
self.title("文件属性批量修改工具")
self.geometry("760x580")
self.resizable(False, False)
# ========== 全局变量 ==========
# 模式与路径
self.mode_var = tk.StringVar(value="file") # file=单个文件 folder=文件夹批量
self.path_var = tk.StringVar()
self.recursive_var = tk.BooleanVar(value=False)
# 文件名设置
self.rename_enable = tk.BooleanVar(value=False)
self.new_name_var = tk.StringVar()
# 创建时间
self.ctime_enable = tk.BooleanVar(value=False)
self.ctime_y = tk.StringVar(value="2025")
self.ctime_m = tk.StringVar(value="01")
self.ctime_d = tk.StringVar(value="01")
self.ctime_h = tk.StringVar(value="00")
self.ctime_min = tk.StringVar(value="00")
self.ctime_s = tk.StringVar(value="00")
# 修改时间
self.mtime_enable = tk.BooleanVar(value=False)
self.mtime_y = tk.StringVar(value="2025")
self.mtime_m = tk.StringVar(value="01")
self.mtime_d = tk.StringVar(value="01")
self.mtime_h = tk.StringVar(value="00")
self.mtime_min = tk.StringVar(value="00")
self.mtime_s = tk.StringVar(value="00")
# 访问时间
self.atime_enable = tk.BooleanVar(value=False)
self.atime_y = tk.StringVar(value="2025")
self.atime_m = tk.StringVar(value="01")
self.atime_d = tk.StringVar(value="01")
self.atime_h = tk.StringVar(value="00")
self.atime_min = tk.StringVar(value="00")
self.atime_s = tk.StringVar(value="00")
# 构建界面
self.build_ui()
self.refresh_widget_state()
def build_ui(self):
"""构建所有界面元素"""
time_labels = ["年", "月", "日", "时", "分", "秒"]
# 1. 目标选择区域
frame_path = ttk.LabelFrame(self, text="目标选择")
frame_path.pack(fill="x", padx=12, pady=8)
ttk.Radiobutton(
frame_path, text="单个文件模式",
variable=self.mode_var, value="file",
command=self.refresh_widget_state
).grid(row=0, column=0, padx=8, pady=6)
ttk.Radiobutton(
frame_path, text="文件夹批量模式",
variable=self.mode_var, value="folder",
command=self.refresh_widget_state
).grid(row=0, column=1, padx=8, pady=6)
ttk.Entry(frame_path, textvariable=self.path_var, width=75).grid(
row=1, column=0, columnspan=3, padx=8, pady=6, sticky="w"
)
ttk.Button(frame_path, text="浏览", command=self.browse_target).grid(
row=1, column=3, padx=8, pady=6
)
ttk.Checkbutton(
frame_path, text="包含子文件夹(递归处理)",
variable=self.recursive_var
).grid(row=2, column=0, columnspan=2, padx=8, pady=2, sticky="w")
# 2. 文件名设置
frame_name = ttk.LabelFrame(self, text="文件名设置")
frame_name.pack(fill="x", padx=12, pady=5)
self.ck_rename = ttk.Checkbutton(
frame_name, text="修改文件名",
variable=self.rename_enable, command=self.refresh_widget_state
)
self.ck_rename.grid(row=0, column=0, padx=8, pady=6, sticky="w")
self.entry_name = ttk.Entry(
frame_name, textvariable=self.new_name_var, width=40, state="disabled"
)
self.entry_name.grid(row=0, column=1, padx=8, pady=6, sticky="w")
ttk.Label(
frame_name, text="(仅单个文件可用,需包含扩展名,例如 文档.txt)"
).grid(row=0, column=2, padx=8, pady=6, sticky="w")
# 3. 创建时间设置
frame_ctime = ttk.LabelFrame(self, text="创建时间设置")
frame_ctime.pack(fill="x", padx=12, pady=5)
ttk.Checkbutton(
frame_ctime, text="修改创建时间",
variable=self.ctime_enable, command=self.refresh_widget_state
).grid(row=0, column=0, padx=8, pady=6, sticky="w")
self.ctime_entry_list = []
ctime_vars = [
self.ctime_y, self.ctime_m, self.ctime_d,
self.ctime_h, self.ctime_min, self.ctime_s
]
for idx, (var, lab) in enumerate(zip(ctime_vars, time_labels)):
entry = ttk.Entry(frame_ctime, textvariable=var, width=5, state="disabled")
entry.grid(row=0, column=1 + idx * 2, padx=2, pady=6)
ttk.Label(frame_ctime, text=lab).grid(
row=0, column=2 + idx * 2, padx=1, pady=6, sticky="w"
)
self.ctime_entry_list.append(entry)
# 4. 修改时间设置
frame_mtime = ttk.LabelFrame(self, text="修改时间设置")
frame_mtime.pack(fill="x", padx=12, pady=5)
ttk.Checkbutton(
frame_mtime, text="修改修改时间",
variable=self.mtime_enable, command=self.refresh_widget_state
).grid(row=0, column=0, padx=8, pady=6, sticky="w")
self.mtime_entry_list = []
mtime_vars = [
self.mtime_y, self.mtime_m, self.mtime_d,
self.mtime_h, self.mtime_min, self.mtime_s
]
for idx, (var, lab) in enumerate(zip(mtime_vars, time_labels)):
entry = ttk.Entry(frame_mtime, textvariable=var, width=5, state="disabled")
entry.grid(row=0, column=1 + idx * 2, padx=2, pady=6)
ttk.Label(frame_mtime, text=lab).grid(
row=0, column=2 + idx * 2, padx=1, pady=6, sticky="w"
)
self.mtime_entry_list.append(entry)
# 5. 访问时间设置
frame_atime = ttk.LabelFrame(self, text="访问时间设置")
frame_atime.pack(fill="x", padx=12, pady=5)
ttk.Checkbutton(
frame_atime, text="修改访问时间",
variable=self.atime_enable, command=self.refresh_widget_state
).grid(row=0, column=0, padx=8, pady=6, sticky="w")
self.atime_entry_list = []
atime_vars = [
self.atime_y, self.atime_m, self.atime_d,
self.atime_h, self.atime_min, self.atime_s
]
for idx, (var, lab) in enumerate(zip(atime_vars, time_labels)):
entry = ttk.Entry(frame_atime, textvariable=var, width=5, state="disabled")
entry.grid(row=0, column=1 + idx * 2, padx=2, pady=6)
ttk.Label(frame_atime, text=lab).grid(
row=0, column=2 + idx * 2, padx=1, pady=6, sticky="w"
)
self.atime_entry_list.append(entry)
# 6. 执行按钮
ttk.Button(
self, text="开始执行修改", command=self.run_task, width=22
).pack(pady=12)
# 7. 日志输出区
frame_log = ttk.LabelFrame(self, text="执行日志")
frame_log.pack(fill="both", expand=True, padx=12, pady=5)
self.log_box = tk.Text(frame_log, height=13, wrap="word")
self.log_box.pack(fill="both", expand=True, padx=6, pady=6, side="left")
scrollbar = ttk.Scrollbar(frame_log, command=self.log_box.yview)
scrollbar.pack(side="right", fill="y")
self.log_box.config(yscrollcommand=scrollbar.set)
def refresh_widget_state(self):
"""根据勾选状态动态更新控件可用/禁用"""
# 文件名:仅单个文件模式可用
if self.mode_var.get() == "file":
self.ck_rename.config(state="normal")
state = "normal" if self.rename_enable.get() else "disabled"
self.entry_name.config(state=state)
else:
self.ck_rename.config(state="disabled")
self.entry_name.config(state="disabled")
self.rename_enable.set(False)
# 时间输入框状态
state = "normal" if self.ctime_enable.get() else "disabled"
for entry in self.ctime_entry_list:
entry.config(state=state)
state = "normal" if self.mtime_enable.get() else "disabled"
for entry in self.mtime_entry_list:
entry.config(state=state)
state = "normal" if self.atime_enable.get() else "disabled"
for entry in self.atime_entry_list:
entry.config(state=state)
def browse_target(self):
"""浏览选择文件或文件夹"""
if self.mode_var.get() == "file":
path = filedialog.askopenfilename(
title="选择目标文件", filetypes=[("所有文件", "*.*")]
)
else:
path = filedialog.askdirectory(title="选择目标文件夹")
if path:
self.path_var.set(path)
def parse_datetime(self, y, m, d, h, minute, s):
"""解析时间字符串,合法返回datetime,失败返回None"""
try:
return datetime(int(y), int(m), int(d), int(h), int(minute), int(s))
except ValueError:
return None
def append_log(self, text):
"""向日志框追加内容"""
self.log_box.insert("end", text + "\n")
self.log_box.see("end")
self.update()
def run_task(self):
"""执行修改主逻辑"""
target_path = self.path_var.get().strip()
if not target_path:
messagebox.showwarning("提示", "请先选择目标文件或文件夹")
return
if not os.path.exists(target_path):
messagebox.showerror("错误", "路径不存在,请检查路径是否正确")
return
# 校验至少勾选一项修改
if not any([
self.rename_enable.get(),
self.ctime_enable.get(),
self.mtime_enable.get(),
self.atime_enable.get()
]):
messagebox.showwarning("提示", "请至少勾选一项需要修改的内容")
return
# 校验所有时间格式
ctime = mtime = atime = None
if self.ctime_enable.get():
ctime = self.parse_datetime(
self.ctime_y.get(), self.ctime_m.get(), self.ctime_d.get(),
self.ctime_h.get(), self.ctime_min.get(), self.ctime_s.get()
)
if not ctime:
messagebox.showerror("错误", "创建时间格式不合法,请检查日期")
return
if self.mtime_enable.get():
mtime = self.parse_datetime(
self.mtime_y.get(), self.mtime_m.get(), self.mtime_d.get(),
self.mtime_h.get(), self.mtime_min.get(), self.mtime_s.get()
)
if not mtime:
messagebox.showerror("错误", "修改时间格式不合法,请检查日期")
return
if self.atime_enable.get():
atime = self.parse_datetime(
self.atime_y.get(), self.atime_m.get(), self.atime_d.get(),
self.atime_h.get(), self.atime_min.get(), self.atime_s.get()
)
if not atime:
messagebox.showerror("错误", "访问时间格式不合法,请检查日期")
return
# 清空日志
self.log_box.delete("1.0", "end")
self.append_log("========== 任务开始执行 ==========")
# 组装参数
params = {
"rename": self.rename_enable.get(),
"new_name": self.new_name_var.get(),
"modify_ctime": self.ctime_enable.get(),
"ctime": ctime,
"modify_mtime": self.mtime_enable.get(),
"mtime": mtime,
"modify_atime": self.atime_enable.get(),
"atime": atime,
}
# 执行对应模式
if self.mode_var.get() == "file":
if not os.path.isfile(target_path):
messagebox.showerror("错误", "当前路径不是文件,请切换为文件夹模式")
return
success, msg = modify_file_info(target_path, **params)
status = "✅ 成功" if success else "❌ 失败"
self.append_log(f"{status} | {target_path}:{msg}")
self.append_log("\n========== 任务执行完成 ==========")
else:
if not os.path.isdir(target_path):
messagebox.showerror("错误", "当前路径不是文件夹,请切换为文件模式")
return
results = batch_modify_folder(
target_path, recursive=self.recursive_var.get(), **params
)
success_count = 0
fail_count = 0
for file_path, success, msg in results:
flag = "✅" if success else "❌"
if success:
success_count += 1
else:
fail_count += 1
self.append_log(f"{flag} | {file_path}:{msg}")
self.append_log(f"\n========== 任务执行完成 ==========")
self.append_log(
f"总计处理 {len(results)} 个文件,成功 {success_count} 个,失败 {fail_count} 个"
)
messagebox.showinfo("完成", "执行结束,详细结果请查看日志")
if __name__ == "__main__":
# 解决Windows高DPI下界面模糊问题
try:
from ctypes import windll
windll.shcore.SetProcessDpiAwareness(1)
except Exception:
pass
app = FileAttributeTool()
app.mainloop()
📖 使用教程
1. 单个文件修改
- 选择「单个文件模式」
- 点击「浏览」选择要修改的文件
- 勾选需要修改的项(文件名/创建时间/修改时间/访问时间)
- 输入对应的值
- 点击「开始执行修改」
2. 文件夹批量修改
- 选择「文件夹批量模式」
- 点击「浏览」选择目标文件夹
- 勾选「包含子文件夹」可递归处理所有子目录
- 勾选需要修改的时间项
- 点击「开始执行修改」
3. 注意事项
- ⚠️ 修改前务必备份重要文件,避免误操作
- ⚠️ 修改系统目录(如
C:\Windows、Program Files)的文件时,需要右键以管理员身份运行 - ✅ 支持所有文件类型,无格式限制
- ✅ 完美支持中文路径和中文文件名
📦 打包成独立exe
1. 安装PyInstaller
bash
pip install pyinstaller
2. 添加版本信息(可选,让exe更专业)
新建 version_info.txt 文件,填入以下内容(可自定义发行商、版权等信息):
python
VSVersionInfo(
ffi=FixedFileInfo(
filevers=(1, 0, 0, 0),
prodvers=(1, 0, 0, 0),
mask=0x3f,
flags=0x0,
OS=0x40004,
fileType=0x1,
subtype=0x0,
date=(0, 0)
),
kids=[
StringFileInfo(
[
StringTable(
u'080404B0',
[
StringStruct(u'CompanyName', u'你的名称'),
StringStruct(u'FileDescription', u'文件属性批量修改工具'),
StringStruct(u'FileVersion', u'1.0.0.0'),
StringStruct(u'InternalName', u'FileAttrTool'),
StringStruct(u'LegalCopyright', u'© 2025 你的名称. 保留所有权利.'),
StringStruct(u'OriginalFilename', u'文件属性修改工具.exe'),
StringStruct(u'ProductName', u'文件属性批量修改工具')
])
]),
VarFileInfo([VarStruct(u'Translation', [2052, 1200])])
]
)
3. 执行打包命令
bash
pyinstaller -F -w --version-file version_info.txt --name "文件属性修改工具" file_attr_tool.py
参数说明:
-F:打包为单个独立exe文件-w:不显示控制台窗口,纯GUI界面--version-file:嵌入版本信息--name:自定义exe文件名
打包完成后,在 dist 文件夹中就能找到生成的exe文件,双击即可运行,无需安装Python环境!
4.加密打包
报错核心原因
你使用的 PyArmor 9.2.5 版本 中,pyarmor build 是用于构建加密工程的命令,不负责调用 PyInstaller 打包 EXE,也不支持 -- 分隔符传参,因此所有打包参数都会被识别为无效参数。
PyArmor 9.x 的「代码混淆 + 打包 EXE」一体化功能,是通过 pyarmor gen 命令的 --pack 参数实现的;额外的 PyInstaller 参数需要先通过配置项设置,不能直接拼接在命令末尾。
正确操作方案
方案一:一体化加密打包(推荐,自动处理依赖)
分两步执行:先配置 PyInstaller 参数,再执行打包。
-
设置打包参数到配置项
bashpyarmor cfg pack:pyi_options = " --windowed --name \"文件属性修改工具\" --version-file version_info.txt"注意:引号开头必须有一个空格;中文名称的双引号需要转义包裹。
-
执行一体化加密+打包
bashpyarmor gen --pack onefile file_attr_tool.py
执行完成后,最终的单文件 EXE 会生成在 dist 目录中。
如果后续不需要保留该打包配置,可执行命令清除:
bash
pyarmor cfg -d pack:pyi_options
方案二:两步手动打包(更灵活可控)
你已经成功用 pyarmor gen 完成了代码加密,可直接用 PyInstaller 打包加密后的脚本,无需调整 pyarmor 配置:
bash
pyinstaller --onefile --windowed --name "文件属性修改工具" --version-file version_info.txt dist/file_attr_tool.py
补充说明
- 请确保
version_info.txt在当前执行命令的目录下,若文件在其他位置,请填写完整的绝对路径。 - 若打包后运行提示缺少运行时依赖,优先使用方案一的一体化打包,PyArmor 会自动处理运行时文件的嵌入。
需要我帮你排查打包后运行闪退、体积过大这类常见问题吗?
🐛 踩坑记录
坑1:No module named 'win32timezone'
问题:使用pywin32的SetFileTime时,传入datetime对象会报这个错。
原因:pywin32安装后没有执行后置注册脚本,或者PyInstaller打包时漏了这个模块。
解决方案:用ctypes直接调用Windows原生API,彻底绕开对win32timezone的依赖(本文采用的方案)。
坑2:must be a pywintypes time object (got int)
问题:自己转成整数传给SetFileTime会报类型错误。
原因:win32file.SetFileTime只接受pywintypes的时间对象,不接受原始整数。
解决方案:用ctypes.windll.kernel32.SetFileTime,传入FILETIME结构体指针即可。
坑3:PowerShell中conda activate报错
问题 :CondaError: Run 'conda init' before 'conda activate'
原因:PowerShell默认没有加载conda配置。
解决方案 :执行 conda init powershell 后重启终端,或者直接用Anaconda Prompt。
🎁 资源下载
我已经把工具打包好了,不想自己搭环境的朋友可以直接下载使用:
- 👉 文件属性修改工具.exe(见本文附件资源)
- 绿色免安装,双击直接运行
- 支持Windows 7/10/11
📝 总结
这个工具虽然功能简单,但实用性很强,特别是批量处理实验数据、整理文件归档的时候特别好用。
核心技术点总结:
- Python标准库无法修改文件创建时间,必须调用Windows API
- 使用ctypes可以绕过pywin32的各种依赖问题
- FILETIME是100纳秒为单位的64位整数,基准是1601年1月1日
- 注意时区转换,避免时间偏差
如果觉得有用,欢迎点赞、收藏、关注!有问题可以在评论区留言交流~
版权声明:本文为原创文章,转载请注明出处。工具仅供个人学习使用,请勿用于非法用途。