写个自动整理照片的脚本

wen 实用脚本 2

本文目录导读:

写个自动整理照片的脚本

  1. 基础版:按日期整理照片
  2. 高级版:按拍摄日期和地点整理
  3. 使用说明

我来帮你写一个自动整理照片的Python脚本,这个脚本会根据照片的拍摄日期自动分类到对应的文件夹中。

基础版:按日期整理照片

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
自动整理照片脚本 - 按拍摄日期分类
功能:根据照片的拍摄日期,自动将其移动到对应月份的文件夹中
"""
import os
import shutil
from datetime import datetime
from PIL import Image
from PIL.ExifTags import TAGS
import argparse
from pathlib import Path
class PhotoOrganizer:
    def __init__(self, source_dir, dest_dir=None, move=False):
        """
        初始化照片整理器
        Args:
            source_dir: 源照片目录
            dest_dir: 目标目录(默认为source_dir)
            move: True=移动文件,False=复制文件
        """
        self.source_dir = Path(source_dir)
        self.dest_dir = Path(dest_dir) if dest_dir else self.source_dir
        self.move = move
        # 支持的图片格式
        self.image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.heic'}
        # 统计信息
        self.stats = {
            'total': 0,
            'organized': 0,
            'failed': 0,
            'already_organized': 0
        }
    def get_exif_date(self, file_path):
        """获取照片的拍摄日期"""
        try:
            # 尝试获取EXIF信息
            with Image.open(file_path) as img:
                exif_data = img._getexif()
                if exif_data:
                    for tag_id, value in exif_data.items():
                        tag_name = TAGS.get(tag_id, tag_id)
                        if tag_name == 'DateTimeOriginal':
                            return datetime.strptime(value, '%Y:%m:%d %H:%M:%S')
        except:
            pass
        # 如果无法获取EXIF,则使用文件修改时间
        mtime = os.path.getmtime(file_path)
        return datetime.fromtimestamp(mtime)
    def get_destination_folder(self, date):
        """根据日期生成目标文件夹路径"""
        year = str(date.year)
        month = f"{date.month:02d}"
        month_name = f"{year}-{month}"
        # 分类结构:年/年-月
        return self.dest_dir / year / month_name
    def organize_photo(self, file_path):
        """整理单张照片"""
        try:
            self.stats['total'] += 1
            # 检查文件扩展名
            if file_path.suffix.lower() not in self.image_extensions:
                print(f"跳过不支持的文件: {file_path.name}")
                return
            # 获取拍摄日期
            photo_date = self.get_exif_date(str(file_path))
            # 生成目标文件夹
            dest_folder = self.get_destination_folder(photo_date)
            dest_folder.mkdir(parents=True, exist_ok=True)
            # 生成目标文件路径
            dest_path = dest_folder / file_path.name
            # 处理重名文件
            if dest_path.exists():
                base_name = dest_path.stem
                extension = dest_path.suffix
                counter = 1
                while dest_path.exists():
                    dest_path = dest_folder / f"{base_name}_{counter}{extension}"
                    counter += 1
            # 执行复制或移动
            if self.move:
                shutil.move(str(file_path), str(dest_path))
                action = "移动"
            else:
                shutil.copy2(str(file_path), str(dest_path))
                action = "复制"
            self.stats['organized'] += 1
            print(f"{action}: {file_path.name} -> {dest_path.relative_to(self.dest_dir)}")
        except Exception as e:
            self.stats['failed'] += 1
            print(f"处理失败 {file_path.name}: {str(e)}")
    def organize_all(self, recursive=True):
        """整理所有照片"""
        print("=" * 50)
        print(f"开始整理照片:")
        print(f"源目录: {self.source_dir}")
        print(f"目标目录: {self.dest_dir}")
        print(f"模式: {'移动' if self.move else '复制'}")
        print("=" * 50)
        # 遍历所有文件
        pattern = '**/*' if recursive else '*'
        for file_path in self.source_dir.glob(pattern):
            if file_path.is_file():
                self.organize_photo(file_path)
        # 打印统计信息
        self.print_stats()
    def print_stats(self):
        """打印整理统计信息"""
        print("\n" + "=" * 50)
        print("整理完成!统计信息:")
        print(f"总文件数: {self.stats['total']}")
        print(f"已整理: {self.stats['organized']}")
        print(f"失败: {self.stats['failed']}")
        print("=" * 50)
def main():
    # 命令行参数解析
    parser = argparse.ArgumentParser(description='自动整理照片脚本')
    parser.add_argument('source', help='源照片目录路径')
    parser.add_argument('-d', '--dest', help='目标目录路径(默认与源目录相同)')
    parser.add_argument('-m', '--move', action='store_true', help='移动文件(默认是复制)')
    parser.add_argument('-n', '--no-recursive', action='store_true', help='不递归子目录')
    args = parser.parse_args()
    # 检查源目录是否存在
    if not os.path.exists(args.source):
        print(f"错误:源目录不存在: {args.source}")
        return
    # 创建整理器并执行
    organizer = PhotoOrganizer(
        source_dir=args.source,
        dest_dir=args.dest,
        move=args.move
    )
    organizer.organize_all(recursive=not args.no_recursive)
if __name__ == "__main__":
    main()

高级版:按拍摄日期和地点整理

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
高级版照片整理脚本
功能:
1. 按拍摄日期分类
2. 按拍摄地点分类(如果有GPS信息)
3. 删除重复文件
4. 生成整理报告
"""
import os
import shutil
import hashlib
import json
from datetime import datetime
from collections import defaultdict
from PIL import Image
from PIL.ExifTags import TAGS, GPSTAGS
from pathlib import Path
import argparse
class AdvancedPhotoOrganizer:
    def __init__(self, source_dir, dest_dir=None, mode='copy', delete_duplicates=True):
        """
        高级照片整理器
        Args:
            source_dir: 源目录
            dest_dir: 目标目录
            mode: copy/move
            delete_duplicates: 是否删除重复文件
        """
        self.source_dir = Path(source_dir)
        self.dest_dir = Path(dest_dir) if dest_dir else self.source_dir
        self.mode = mode
        self.delete_duplicates = delete_duplicates
        self.image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.heic'}
        self.duplicates = []
        self.report = defaultdict(list)
    def get_exif_data(self, image_path):
        """获取完整的EXIF数据"""
        exif_data = {}
        try:
            with Image.open(image_path) as img:
                raw_exif = img._getexif()
                if raw_exif:
                    for tag_id, value in raw_exif.items():
                        tag_name = TAGS.get(tag_id, tag_id)
                        if tag_name == "GPSInfo":
                            gps_data = {}
                            for gps_tag_id, gps_value in value.items():
                                gps_tag_name = GPSTAGS.get(gps_tag_id, gps_tag_id)
                                gps_data[gps_tag_name] = gps_value
                            exif_data['GPSInfo'] = gps_data
                        else:
                            exif_data[tag_name] = value
        except Exception as e:
            print(f"读取EXIF失败 {image_path}: {e}")
        return exif_data
    def get_coordinates(self, gps_info):
        """从GPS信息中提取经纬度坐标"""
        if not gps_info:
            return None, None
        def convert_to_degrees(value):
            d, m, s = value
            return d + (m / 60.0) + (s / 3600.0)
        try:
            lat = convert_to_degrees(gps_info['GPSLatitude'])
            lon = convert_to_degrees(gps_info['GPSLongitude'])
            if gps_info['GPSLatitudeRef'] == 'S':
                lat = -lat
            if gps_info['GPSLongitudeRef'] == 'W':
                lon = -lon
            return lat, lon
        except:
            return None, None
    def calculate_md5(self, file_path, chunk_size=8192):
        """计算文件的MD5哈希值"""
        md5 = hashlib.md5()
        try:
            with open(file_path, 'rb') as f:
                while True:
                    data = f.read(chunk_size)
                    if not data:
                        break
                    md5.update(data)
            return md5.hexdigest()
        except:
            return None
    def find_duplicates(self):
        """查找重复文件"""
        print("查找重复文件...")
        hash_map = defaultdict(list)
        for file_path in self.source_dir.rglob('*'):
            if file_path.is_file() and file_path.suffix.lower() in self.image_extensions:
                file_hash = self.calculate_md5(file_path)
                if file_hash:
                    hash_map[file_hash].append(file_path)
        for hash_value, files in hash_map.items():
            if len(files) > 1:
                self.duplicates.extend(files[1:])  # 保留第一个,其余为重复
                self.report['duplicates'].append({
                    'hash': hash_value,
                    'files': [str(f) for f in files]
                })
        return len(self.duplicates)
    def organize_by_date_location(self):
        """按日期和地点整理照片"""
        print("开始整理照片...")
        for file_path in self.source_dir.rglob('*'):
            if not (file_path.is_file() and file_path.suffix.lower() in self.image_extensions):
                continue
            try:
                # 获取EXIF数据
                exif_info = self.get_exif_data(file_path)
                # 获取拍摄日期
                date_str = exif_info.get('DateTimeOriginal', '')
                if date_str:
                    photo_date = datetime.strptime(date_str, '%Y:%m:%d %H:%M:%S')
                else:
                    # 使用文件修改时间作为备用
                    mtime = os.path.getmtime(file_path)
                    photo_date = datetime.fromtimestamp(mtime)
                # 获取GPS坐标
                gps_info = exif_info.get('GPSInfo', {})
                lat, lon = self.get_coordinates(gps_info)
                # 构建目标路径
                year = str(photo_date.year)
                month = f"{photo_date.month:02d}"
                if lat and lon:
                    # 有GPS信息,按地点分类
                    location_folder = f"{year}-{month}-gps"
                else:
                    # 无GPS信息,只按日期分类
                    location_folder = f"{year}-{month}"
                dest_folder = self.dest_dir / year / location_folder
                dest_folder.mkdir(parents=True, exist_ok=True)
                dest_path = dest_folder / file_path.name
                # 处理重名文件
                if dest_path.exists():
                    base_name = dest_path.stem
                    extension = dest_path.suffix
                    counter = 1
                    while dest_path.exists():
                        dest_path = dest_folder / f"{base_name}_{counter}{extension}"
                        counter += 1
                # 执行复制或移动
                if self.mode == 'move':
                    shutil.move(str(file_path), str(dest_path))
                else:
                    shutil.copy2(str(file_path), str(dest_path))
                self.report['organized'].append({
                    'source': str(file_path),
                    'destination': str(dest_path),
                    'date': photo_date.isoformat(),
                    'gps': f"{lat}, {lon}" if lat and lon else None
                })
                print(f"✓ {file_path.name} -> {dest_path.relative_to(self.dest_dir)}")
            except Exception as e:
                self.report['failed'].append({
                    'file': str(file_path),
                    'error': str(e)
                })
                print(f"✗ 处理失败 {file_path.name}: {e}")
    def clean_duplicates(self):
        """清理重复文件"""
        if not self.delete_duplicates or not self.duplicates:
            return 0
        print(f"\n发现 {len(self.duplicates)} 个重复文件")
        deleted = 0
        for dup_file in self.duplicates:
            try:
                # 移动到回收站而不是永久删除
                backup_dir = self.dest_dir / "重复文件备份"
                backup_dir.mkdir(exist_ok=True)
                backup_path = backup_dir / dup_file.name
                if not backup_path.exists():
                    shutil.move(str(dup_file), str(backup_path))
                    deleted += 1
                    print(f"重复文件已备份: {dup_file.name}")
            except Exception as e:
                print(f"处理重复文件失败 {dup_file}: {e}")
        return deleted
    def generate_report(self):
        """生成整理报告"""
        report_file = self.dest_dir / f"整理报告_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
        report_data = {
            "生成时间": datetime.now().isoformat(),
            "源目录": str(self.source_dir),
            "目标目录": str(self.dest_dir),
            "整理模式": self.mode,
            "统计数据": {
                "整理文件数": len(self.report.get('organized', [])),
                "失败文件数": len(self.report.get('failed', [])),
                "重复文件数": len(self.report.get('duplicates', []))
            },
            "整理详情": self.report
        }
        with open(report_file, 'w', encoding='utf-8') as f:
            json.dump(report_data, f, ensure_ascii=False, indent=2)
        print(f"\n整理报告已保存: {report_file}")
    def run(self):
        """运行完整整理流程"""
        print("=" * 60)
        print("高级照片整理器")
        print("=" * 60)
        # 1. 查找重复文件
        if self.delete_duplicates:
            dup_count = self.find_duplicates()
            print(f"找到 {dup_count} 个重复文件")
        # 2. 整理照片
        self.organize_by_date_location()
        # 3. 清理重复文件
        if self.delete_duplicates and self.duplicates:
            deleted = self.clean_duplicates()
            print(f"已备份 {deleted} 个重复文件")
        # 4. 生成报告
        self.generate_report()
        print("\n整理完成!")
def main():
    parser = argparse.ArgumentParser(description='高级照片整理器')
    parser.add_argument('source', help='源照片目录')
    parser.add_argument('-d', '--dest', help='目标目录(默认与源目录相同)')
    parser.add_argument('-m', '--mode', choices=['copy', 'move'], default='copy', 
                       help='整理模式:copy=复制,move=移动')
    parser.add_argument('--keep-duplicates', action='store_true', 
                       help='保留重复文件(默认自动备份到子目录)')
    args = parser.parse_args()
    if not os.path.exists(args.source):
        print(f"错误:源目录不存在: {args.source}")
        return
    organizer = AdvancedPhotoOrganizer(
        source_dir=args.source,
        dest_dir=args.dest,
        mode=args.mode,
        delete_duplicates=not args.keep_duplicates
    )
    organizer.run()
if __name__ == "__main__":
    main()

使用说明

安装依赖

pip install Pillow

基础版使用方法

# 复制照片到按日期分类的文件夹(默认)
python photo_organizer.py /path/to/photos
# 移动到分类文件夹(不保留原文件)
python photo_organizer.py /path/to/photos --move
# 指定目标目录
python photo_organizer.py /path/to/photos --dest /path/to/destination
# 不递归子目录
python photo_organizer.py /path/to/photos --no-recursive

高级版使用方法

# 复制并整理(默认)
python advanced_photo_organizer.py /path/to/photos
# 移动模式
python advanced_photo_organizer.py /path/to/photos --mode move
# 指定目标目录
python advanced_photo_organizer.py /path/to/photos --dest /path/to/destination
# 保留重复文件
python advanced_photo_organizer.py /path/to/photos --keep-duplicates

功能特点

  1. 自动识别拍摄日期:优先使用EXIF信息,无法读取时使用文件修改时间
  2. 智能分类:按年/月创建文件夹结构
  3. 重复文件检测:通过MD5哈希识别重复文件
  4. GPS信息支持:识别拍摄地点信息(高级版)
  5. 生成报告:记录所有整理操作(高级版)
  6. 安全处理:默认复制而非移动,重复文件备份而非删除

生成的文件结构

整理后的目录/
├── 2023/
│   ├── 2023-01/
│   │   ├── photo1.jpg
│   │   ├── photo2.jpg
│   ├── 2023-02/
│   └── ...
├── 2024/
│   └── ...
└── 整理报告_20240101_120000.json

这个脚本安全可靠,默认使用复制模式,不会删除原始文件,运行前建议先用少量照片测试,确认效果后再批量处理。

抱歉,评论功能暂时关闭!