Python实现照片以拍摄时间命名(自用于QNAP)

-
-
2025-02-18

Qumagie照片排序错误解决

Qumagie照片排序错误解决

Qumagie偶尔出现照片排序错误的问题,如:2018年拍摄的照片,显示在2022年等。
 

原因为:尽管系统取到了照片的拍摄时间(照片的文件名),但Qumagie仍将照片的最后修改时间判断为拍摄时间。

利用Python将文件名格式化后,写入到照片文件的修改时间即可,代码如下:

V0.1

from win32file import CreateFile, SetFileTime, GetFileTime, CloseHandle
from win32file import GENERIC_READ, GENERIC_WRITE, OPEN_EXISTING
from pywintypes import Time
import time
import os,re

def modifyFileTime(filepath, createTime, modifyTime, accessTime,offset):
    """
  用来修改任意文件的相关时间属性,时间格式:20190202000102
    """
    try:
        format = "%Y%m%d%H%M%S" #时间格式
        cTime_t = timeOffsetAndStruct(createTime,format,offset[0])
        mTime_t = timeOffsetAndStruct(modifyTime,format,offset[1])
        aTime_t = timeOffsetAndStruct(accessTime,format,offset[2])

        fh = CreateFile(filepath, GENERIC_READ | GENERIC_WRITE, 0, None, OPEN_EXISTING, 0, 0)
        createTimes, accessTimes, modifyTimes = GetFileTime(fh)
        #print(cTime_t)
        createTimes = Time(int(time.mktime(cTime_t)))
        accessTimes = Time(int(time.mktime(aTime_t)))
        modifyTimes = Time(int(time.mktime(mTime_t)))
        SetFileTime(fh, createTimes, accessTimes, modifyTimes)
        CloseHandle(fh)
        return 0
    except:
        return 1

#结构化时间
def timeOffsetAndStruct(times, format, offset):
    return time.localtime(time.mktime(time.strptime(times, format)) + offset)

# 将文件名中的空格修改为横杠
def space2bar(dirname, basename):
    newname = basename.replace(' ', '-')
    os.rename(os.path.join(dirname, basename), os.path.join(dirname, newname))
    return newname

# 获取文件名中的时间用于修改
def get_time(basename):
    time_str = basename.split(' ')
    temp_str = time_str[0].split('-')
    # 获取temp_str[4]的前6位作为时分秒
    h_m_s = time_str[1].split('.')
    temp_time = temp_str[0]+temp_str[1]+temp_str[2]+h_m_s[0]+h_m_s[1]+h_m_s[2]
    #print(temp_time)
    return temp_time

if __name__ == '__main__':

    expression = r"\d{4}\-\d{2}\-\d{2} \d{2}.\d{2}.\d{2}"  # 文件名格式
    dirname = r'D:\照片' # r'D:\照片'
    offset = (0,1,2)

    basenames = os.listdir(dirname)
    for basename in basenames:

        if re.match(expression, basename):
            filepath = dirname+'\\'+basename

            # 获取文件名中的时间
            temp_time = get_time(basename)
            cTime=mTime=aTime=temp_time

            r = modifyFileTime(filepath, cTime, mTime, aTime,offset)
            if r == 0:
                print(basename+'>>>>'+'修改完成')
            elif r == 1:
                print(basename+'>>>>'+'修改失败')
        else:
            print(basename+'>>>>'+'文件名格式不符合')
            continue

以上代码以照片存放位置为D:照片为例。

2025年2月17日调整:

V0.2

调整内容为:

调整为运行后打开选择目录。

在照片文件夹生成日志文件。

import os
import csv
from PIL import Image, ExifTags
from datetime import datetime
from tkinter import Tk, filedialog
from pillow_heif import register_heif_opener
import piexif

# 注册 HEIC 文件支持
register_heif_opener()

def get_creation_time(file_path):
    """返回元组:(时间对象, 命名依据)"""
    try:
        with Image.open(file_path) as img:
            # 获取 EXIF 数据
            exif_data = img.info.get('exif')
            if exif_data:
                exif_dict = piexif.load(exif_data)
                # 定义可能的日期标签
                date_tags = [piexif.ExifIFD.DateTimeOriginal, piexif.ExifIFD.DateTimeDigitized]
                for date_tag in date_tags:
                    date_str = exif_dict['Exif'].get(date_tag)
                    if date_str:
                        # 尝试多种日期格式
                        date_str = date_str.decode('utf-8')  # 转换为字符串
                        date_formats = ['%Y:%m:%d %H:%M:%S', '%Y/%m/%d %H:%M:%S']
                        for fmt in date_formats:
                            try:
                                dt = datetime.strptime(date_str, fmt)
                                return dt, "根据拍摄日期重命名"
                            except ValueError:
                                continue
                        print(f"无法解析日期格式 {file_path}: {date_str}")
                        break
    except Exception as e:
        print(f"无法读取元数据 {file_path}: {str(e)}")

    # 如果无法获取拍摄时间,则使用修改时间
    mtime = os.path.getmtime(file_path)
    return datetime.fromtimestamp(mtime), "根据修改日期重命名"

def rename_files_in_directory(directory):
    log_data = []
    index = 1

    for filename in os.listdir(directory):
        old_path = os.path.join(directory, filename)

        if not os.path.isfile(old_path):
            continue

        # 获取时间信息
        timestamp, method = get_creation_time(old_path)

        # 生成新文件名
        new_basename = timestamp.strftime('%Y-%m-%d %H.%M.%S')
        new_filename = f"{new_basename}{os.path.splitext(filename)[1]}"
        new_path = os.path.join(directory, new_filename)

        # 如果文件名冲突,添加后缀
        counter = 1
        while os.path.exists(new_path):
            new_filename = f"{new_basename}_{counter}{os.path.splitext(filename)[1]}"
            new_path = os.path.join(directory, new_filename)
            counter += 1

        # 记录日志信息
        log_entry = [index, filename, new_filename, method]

        try:
            os.rename(old_path, new_path)
            print(f"重命名: {filename} -> {new_filename} ({method})")
        except Exception as e:
            print(f"无法重命名 {filename}: {str(e)}")
            log_entry[3] = f"错误: {str(e)}"

        log_data.append(log_entry)
        index += 1

    # 写入日志文件
    if log_data:
        log_path = os.path.join(directory, "rename_log.csv")
        with open(log_path, 'w', newline='', encoding='utf-8-sig') as f:
            writer = csv.writer(f)
            writer.writerow(["序号", "原始名称", "新名称", "方法"])
            writer.writerows(log_data)
        print(f"日志文件已创建: {log_path}")

def select_directory():
    root = Tk()
    root.withdraw()
    return filedialog.askdirectory(title="选择文件夹")

if __name__ == "__main__":
    target_dir = select_directory()
    if target_dir:
        rename_files_in_directory(target_dir)
    else:
        print("未选择任何目录")

V0.3

增加内容:

  1. 使用方便一些,GUI;
  2. 选择对当前目录还是对该目录及所有子目录生效;

界面如下:

import os
import csv
from PIL import Image, ExifTags
from datetime import datetime
from tkinter import Tk, filedialog, Text, Button, Scrollbar, VERTICAL, RIGHT, Y, END, Radiobutton, IntVar, Frame
from pillow_heif import register_heif_opener
import piexif

# 注册 HEIC 文件支持
register_heif_opener()

def get_creation_time(file_path, log_text):
    """返回元组:(时间对象, 命名依据)"""
    try:
        with Image.open(file_path) as img:
            # 获取 EXIF 数据
            exif_data = img.info.get('exif')
            if exif_data:
                exif_dict = piexif.load(exif_data)
                # 定义可能的日期标签
                date_tags = [piexif.ExifIFD.DateTimeOriginal, piexif.ExifIFD.DateTimeDigitized]
                for date_tag in date_tags:
                    date_str = exif_dict['Exif'].get(date_tag)
                    if date_str:
                        # 尝试多种日期格式
                        date_str = date_str.decode('utf-8')  # 转换为字符串
                        date_formats = ['%Y:%m:%d %H:%M:%S', '%Y/%m/%d %H:%M:%S']
                        for fmt in date_formats:
                            try:
                                dt = datetime.strptime(date_str, fmt)
                                return dt, "根据拍摄日期重命名"
                            except ValueError:
                                continue
                        log_text.insert(END, f"无法解析日期格式 {file_path}: {date_str}\n")
                        break
    except Exception as e:
        log_text.insert(END, f"无法读取元数据 {file_path}: {str(e)}\n")

    # 如果无法获取拍摄时间,则使用修改时间
    mtime = os.path.getmtime(file_path)
    return datetime.fromtimestamp(mtime), "根据修改日期重命名"

def rename_files_in_directory(directory, log_text, include_subdirs):
    log_data = []
    index = 1

    for root, dirs, files in os.walk(directory):
        if not include_subdirs and root != directory:
            continue

        for filename in files:
            old_path = os.path.join(root, filename)

            if not os.path.isfile(old_path):
                continue

            # 获取时间信息
            timestamp, method = get_creation_time(old_path, log_text)

            # 生成新文件名
            new_basename = timestamp.strftime('%Y-%m-%d %H.%M.%S')
            new_filename = f"{new_basename}{os.path.splitext(filename)[1]}"
            new_path = os.path.join(root, new_filename)

            # 判断文件名是否符合要求
            if filename == new_filename:
                log_text.insert(END, f"无需改名: {filename} (已符合命名要求)\n")
                log_data.append([index, filename, filename, "无需改名"])
                continue

            # 检查是否存在文件名冲突且文件不是同一个
            if new_path != old_path and os.path.exists(new_path):
                counter = 1
                while os.path.exists(new_path):
                    new_filename = f"{new_basename}_{counter}{os.path.splitext(filename)[1]}"
                    new_path = os.path.join(root, new_filename)
                    counter += 1

            # 记录日志信息
            log_entry = [index, filename, new_filename, method]

            try:
                os.rename(old_path, new_path)
                log_text.insert(END, f"重命名: {filename} -> {new_filename} ({method})\n")
            except Exception as e:
                log_text.insert(END, f"无法重命名 {filename}: {str(e)}\n")
                log_entry[3] = f"错误: {str(e)}"

            log_data.append(log_entry)
            index += 1

    # 写入日志文件
    if log_data:
        log_path = os.path.join(directory, "rename_log.csv")
        with open(log_path, 'w', newline='', encoding='utf-8-sig') as f:
            writer = csv.writer(f)
            writer.writerow(["序号", "原始名称", "新名称", "方法"])
            writer.writerows(log_data)
        log_text.insert(END, f"日志文件已创建: {log_path}\n")

def select_directory(log_text, include_subdirs):
    directory = filedialog.askdirectory(title="选择文件夹")
    if directory:
        rename_files_in_directory(directory, log_text, include_subdirs)
    else:
        log_text.insert(END, "未选择任何目录\n")

def create_gui():
    root = Tk()
    root.title("照片及视频重命名工具")

    log_text = Text(root, wrap='word')
    log_text.pack(side='top', fill='both', expand=True)

    scrollbar = Scrollbar(root, orient=VERTICAL, command=log_text.yview)
    scrollbar.pack(side=RIGHT, fill=Y)

    log_text.config(yscrollcommand=scrollbar.set)

    # 创建底部框架用于放置单选按钮和执行按钮
    bottom_frame = Frame(root)
    bottom_frame.pack(side='bottom', fill='x', pady=5)

    # 创建单选按钮变量
    include_subdirs_var = IntVar(value=1)

    # 创建单选按钮
    Radiobutton(bottom_frame, text="对该目录及其子目录均生效", variable=include_subdirs_var, value=1).pack(anchor='w')
    Radiobutton(bottom_frame, text="只对该目录生效", variable=include_subdirs_var, value=0).pack(anchor='w')

    # 创建执行按钮并放置在底部框架
    execute_button = Button(bottom_frame, text="执行", command=lambda: select_directory(log_text, include_subdirs_var.get()))
    execute_button.pack(anchor='w')

    root.mainloop()

if __name__ == "__main__":
    create_gui()

V0.4

调整内容:

修改程序只对照片和视频文件生效,包含以下照片和视频格式:

['.jpg', '.jpeg', '.png', '.heic', '.mp4', '.mov', '.avi', '.mkv']

import os
import csv
from PIL import Image, ExifTags
from datetime import datetime
from tkinter import Tk, filedialog, Text, Button, Scrollbar, VERTICAL, RIGHT, Y, END, Radiobutton, IntVar, Frame
from pillow_heif import register_heif_opener
import piexif

# 注册 HEIC 文件支持
register_heif_opener()

# 定义有效的照片和视频文件扩展名
VALID_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.heic', '.mp4', '.mov', '.avi', '.mkv']

def get_creation_time(file_path, log_text):
    """返回元组:(时间对象, 命名依据)"""
    try:
        with Image.open(file_path) as img:
            # 获取 EXIF 数据
            exif_data = img.info.get('exif')
            if exif_data:
                exif_dict = piexif.load(exif_data)
                # 定义可能的日期标签
                date_tags = [piexif.ExifIFD.DateTimeOriginal, piexif.ExifIFD.DateTimeDigitized]
                for date_tag in date_tags:
                    date_str = exif_dict['Exif'].get(date_tag)
                    if date_str:
                        # 尝试多种日期格式
                        date_str = date_str.decode('utf-8')  # 转换为字符串
                        date_formats = ['%Y:%m:%d %H:%M:%S', '%Y/%m/%d %H:%M:%S']
                        for fmt in date_formats:
                            try:
                                dt = datetime.strptime(date_str, fmt)
                                return dt, "根据拍摄日期重命名"
                            except ValueError:
                                continue
                        log_text.insert(END, f"无法解析日期格式 {file_path}: {date_str}\n")
                        break
    except Exception as e:
        log_text.insert(END, f"无法读取元数据 {file_path}: {str(e)}\n")

    # 如果无法获取拍摄时间,则使用修改时间
    mtime = os.path.getmtime(file_path)
    return datetime.fromtimestamp(mtime), "根据修改日期重命名"

def rename_files_in_directory(directory, log_text, include_subdirs):
    log_data = []
    index = 1

    for root, dirs, files in os.walk(directory):
        if not include_subdirs and root != directory:
            continue

        for filename in files:
            old_path = os.path.join(root, filename)

            # 检查文件扩展名是否有效
            if not os.path.isfile(old_path) or os.path.splitext(filename)[1].lower() not in VALID_EXTENSIONS:
                continue

            # 获取时间信息
            timestamp, method = get_creation_time(old_path, log_text)

            # 生成新文件名
            new_basename = timestamp.strftime('%Y-%m-%d %H.%M.%S')
            new_filename = f"{new_basename}{os.path.splitext(filename)[1]}"
            new_path = os.path.join(root, new_filename)

            # 判断文件名是否符合要求
            if filename == new_filename:
                log_text.insert(END, f"无需改名: {filename} (已符合命名要求)\n")
                log_data.append([index, filename, filename, "无需改名"])
                continue

            # 检查是否存在文件名冲突且文件不是同一个
            if new_path != old_path and os.path.exists(new_path):
                counter = 1
                while os.path.exists(new_path):
                    new_filename = f"{new_basename}_{counter}{os.path.splitext(filename)[1]}"
                    new_path = os.path.join(root, new_filename)
                    counter += 1

            # 记录日志信息
            log_entry = [index, filename, new_filename, method]

            try:
                os.rename(old_path, new_path)
                log_text.insert(END, f"重命名: {filename} -> {new_filename} ({method})\n")
            except Exception as e:
                log_text.insert(END, f"无法重命名 {filename}: {str(e)}\n")
                log_entry[3] = f"错误: {str(e)}"

            log_data.append(log_entry)
            index += 1

    # 写入日志文件
    if log_data:
        log_path = os.path.join(directory, "rename_log.csv")
        with open(log_path, 'w', newline='', encoding='utf-8-sig') as f:
            writer = csv.writer(f)
            writer.writerow(["序号", "原始名称", "新名称", "方法"])
            writer.writerows(log_data)
        log_text.insert(END, f"日志文件已创建: {log_path}\n")

def select_directory(log_text, include_subdirs):
    directory = filedialog.askdirectory(title="选择文件夹")
    if directory:
        rename_files_in_directory(directory, log_text, include_subdirs)
    else:
        log_text.insert(END, "未选择任何目录\n")

def create_gui():
    root = Tk()
    root.title("照片及视频重命名工具")

    log_text = Text(root, wrap='word')
    log_text.pack(side='top', fill='both', expand=True)

    scrollbar = Scrollbar(root, orient=VERTICAL, command=log_text.yview)
    scrollbar.pack(side=RIGHT, fill=Y)

    log_text.config(yscrollcommand=scrollbar.set)

    # 创建底部框架用于放置单选按钮和执行按钮
    bottom_frame = Frame(root)
    bottom_frame.pack(side='bottom', fill='x', pady=5)

    # 创建单选按钮变量
    include_subdirs_var = IntVar(value=1)

    # 创建单选按钮
    Radiobutton(bottom_frame, text="对该目录及其子目录均生效", variable=include_subdirs_var, value=1).pack(anchor='w')
    Radiobutton(bottom_frame, text="只对该目录生效", variable=include_subdirs_var, value=0).pack(anchor='w')

    # 创建执行按钮并放置在底部框架
    execute_button = Button(bottom_frame, text="执行", command=lambda: select_directory(log_text, include_subdirs_var.get()))
    execute_button.pack(anchor='w')

    root.mainloop()

if __name__ == "__main__":
    create_gui()

V0.5

修复BUG:

在出现无需重命名的文件时,序号显示错误

import os
import csv
from PIL import Image, ExifTags
from datetime import datetime
from tkinter import Tk, filedialog, Text, Button, Scrollbar, VERTICAL, RIGHT, Y, END, Radiobutton, IntVar, Frame
from pillow_heif import register_heif_opener
import piexif

# 注册 HEIC 文件支持
register_heif_opener()

# 定义有效的照片和视频文件扩展名
VALID_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.heic', '.mp4', '.mov', '.avi', '.mkv']

def get_creation_time(file_path, log_text):
    """返回元组:(时间对象, 命名依据)"""
    try:
        with Image.open(file_path) as img:
            # 获取 EXIF 数据
            exif_data = img.info.get('exif')
            if exif_data:
                exif_dict = piexif.load(exif_data)
                # 定义可能的日期标签
                date_tags = [piexif.ExifIFD.DateTimeOriginal, piexif.ExifIFD.DateTimeDigitized]
                for date_tag in date_tags:
                    date_str = exif_dict['Exif'].get(date_tag)
                    if date_str:
                        # 尝试多种日期格式
                        date_str = date_str.decode('utf-8')  # 转换为字符串
                        date_formats = ['%Y:%m:%d %H:%M:%S', '%Y/%m/%d %H:%M:%S']
                        for fmt in date_formats:
                            try:
                                dt = datetime.strptime(date_str, fmt)
                                return dt, "拍摄日期"
                            except ValueError:
                                continue
                        log_text.insert(END, f"无法解析日期格式 {file_path}: {date_str}\n")
                        break
    except Exception as e:
        log_text.insert(END, f"无法读取元数据 {file_path}: {str(e)}\n")

    # 如果无法获取拍摄时间,则使用修改时间
    mtime = os.path.getmtime(file_path)
    return datetime.fromtimestamp(mtime), "修改日期"

def rename_files_in_directory(directory, log_text, include_subdirs):
    log_data = []
    index = 1

    for root, dirs, files in os.walk(directory):
        if not include_subdirs and root != directory:
            continue

        for filename in files:
            old_path = os.path.join(root, filename)

            # 检查文件扩展名是否有效
            if not os.path.isfile(old_path) or os.path.splitext(filename)[1].lower() not in VALID_EXTENSIONS:
                continue

            # 获取时间信息
            timestamp, method = get_creation_time(old_path, log_text)

            # 生成新文件名
            new_basename = timestamp.strftime('%Y-%m-%d %H.%M.%S')
            new_filename = f"{new_basename}{os.path.splitext(filename)[1]}"
            new_path = os.path.join(root, new_filename)

            # 判断文件名是否符合要求
            if filename == new_filename:
                log_text.insert(END, f"无需改名: {filename} (已符合命名要求)\n")
                log_data.append([index, filename, filename, "无需改名"])
                index += 1  # Increment index here
                continue

            # 检查是否存在文件名冲突且文件不是同一个
            if new_path != old_path and os.path.exists(new_path):
                counter = 1
                while os.path.exists(new_path):
                    new_filename = f"{new_basename}_{counter}{os.path.splitext(filename)[1]}"
                    new_path = os.path.join(root, new_filename)
                    counter += 1

            # 记录日志信息
            log_entry = [index, filename, new_filename, method]

            try:
                os.rename(old_path, new_path)
                log_text.insert(END, f"重命名: {filename} -> {new_filename} ({method})\n")
            except Exception as e:
                log_text.insert(END, f"无法重命名 {filename}: {str(e)}\n")
                log_entry[3] = f"错误: {str(e)}"

            log_data.append(log_entry)
            index += 1  # Increment index here

    # 写入日志文件
    if log_data:
        log_path = os.path.join(directory, "rename_log.csv")
        with open(log_path, 'w', newline='', encoding='utf-8-sig') as f:
            writer = csv.writer(f)
            writer.writerow(["序号", "原始名称", "新名称", "方法"])
            writer.writerows(log_data)
        log_text.insert(END, f"日志文件已创建: {log_path}\n")

def select_directory(log_text, include_subdirs):
    directory = filedialog.askdirectory(title="选择文件夹")
    if directory:
        rename_files_in_directory(directory, log_text, include_subdirs)
    else:
        log_text.insert(END, "未选择任何目录\n")

def create_gui():
    root = Tk()
    root.title("照片及视频重命名工具")

    log_text = Text(root, wrap='word')
    log_text.pack(side='top', fill='both', expand=True)

    scrollbar = Scrollbar(root, orient=VERTICAL, command=log_text.yview)
    scrollbar.pack(side=RIGHT, fill=Y)

    log_text.config(yscrollcommand=scrollbar.set)

    # 创建底部框架用于放置单选按钮和执行按钮
    bottom_frame = Frame(root)
    bottom_frame.pack(side='bottom', fill='x', pady=5)

    # 创建单选按钮变量
    include_subdirs_var = IntVar(value=1)

    # 创建单选按钮
    Radiobutton(bottom_frame, text="对该目录及其子目录均生效", variable=include_subdirs_var, value=1).pack(anchor='w')
    Radiobutton(bottom_frame, text="只对该目录生效", variable=include_subdirs_var, value=0).pack(anchor='w')

    # 创建执行按钮并放置在底部框架
    execute_button = Button(bottom_frame, text="执行", command=lambda: select_directory(log_text, include_subdirs_var.get()))
    execute_button.pack(anchor='w')

    root.mainloop()

if __name__ == "__main__":
    create_gui()

“您的支持是我持续分享的动力”

微信收款码
微信
支付宝收款码
支付宝

目录