Created
December 30, 2025 00:05
-
-
Save ruofeidu/0dfa7ee0da9720055dc7af284ae8ec25 to your computer and use it in GitHub Desktop.
rename photos by EXIF / file creation date
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import os | |
| import re | |
| from datetime import datetime | |
| from pathlib import Path | |
| from PIL import Image | |
| # Tag IDs for EXIF data | |
| # 36867: DateTimeOriginal (Shutter click) | |
| # 306: DateTime (Last metadata change) | |
| EXIF_DATE_TAGS = (36867, 306) | |
| def get_best_date(entry: os.DirEntry) -> str: | |
| """Retrieves the most accurate date available for a file. | |
| Checks EXIF metadata first, then falls back to the file system creation time. | |
| Returns a string in YYYYMMDD format. | |
| """ | |
| # Attempts to read EXIF data | |
| try: | |
| with Image.open(entry.path) as img: | |
| exif = img.getexif() | |
| if exif: | |
| for tag in EXIF_DATE_TAGS: | |
| val = exif.get(tag) | |
| if val: | |
| # Cleans EXIF format 'YYYY:MM:DD HH:MM:SS' | |
| date_str = str(val)[:10].replace(':', '') | |
| if len(date_str) == 8 and date_str.isdigit(): | |
| return date_str | |
| except Exception: | |
| # Fail silently to allow fallback to file system stats | |
| pass | |
| # Falls back to file system creation time (st_ctime) | |
| # Uses the DirEntry stat to avoid an extra syscall | |
| timestamp = entry.stat().st_ctime | |
| return datetime.fromtimestamp(timestamp).strftime('%Y%m%d') | |
| def rename_files(): | |
| """Scans the current directory and renames images with a date prefix.""" | |
| valid_extensions = {'.jpg', '.jpeg', '.png'} | |
| # Matches 8 digits at the start of a string | |
| date_prefix_regex = re.compile(r"^\d{8}_") | |
| cwd = Path('.') | |
| rename_count = 0 | |
| # Uses scandir for better performance on large directories | |
| with os.scandir(cwd) as entries: | |
| for entry in entries: | |
| if not entry.is_file(): | |
| continue | |
| path = Path(entry.name) | |
| if path.suffix.lower() not in valid_extensions: | |
| continue | |
| # Skips files that already have the YYYYMMDD_ format | |
| if date_prefix_regex.match(entry.name): | |
| continue | |
| date_prefix = get_best_date(entry) | |
| new_name = f"{date_prefix}_{entry.name}" | |
| target_path = cwd / new_name | |
| # Handles naming collisions (e.g., 20251229_photo.jpg already exists) | |
| counter = 1 | |
| while target_path.exists(): | |
| stem = Path(new_name).stem | |
| ext = Path(new_name).suffix | |
| target_path = cwd / f"{stem}_{counter}{ext}" | |
| counter += 1 | |
| try: | |
| os.rename(entry.path, target_path) | |
| print(f"Renamed: {entry.name} -> {target_path.name}") | |
| rename_count += 1 | |
| except OSError as e: | |
| print(f"Failed to rename {entry.name}: {e}") | |
| print(f"\nProcessing complete. {rename_count} files updated.") | |
| if __name__ == "__main__": | |
| rename_files() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment