写真を日付ごと(撮影日)に自動分類し、サムネイル付きHTMLギャラリーを作る実践例です。pathlibでパス管理、Pillowでサムネイル生成とEXIF読み取り、HTMLはテンプレート文字列で生成します。
前提準備と想定構成
- ライブラリ:
pip install pillow - フォルダ構成(例):
- 入力:
~/Pictures/gallery_src - 出力:
~/Pictures/gallery_outthumbs/(サムネイル格納)albums/(日付別アルバムHTML)
- 入力:
サンプルコード(EXIF読み取り+アルバム分け+HTML生成)
from pathlib import Path
from PIL import Image, ExifTags
import html
from datetime import datetime
import shutil
# 入出力ディレクトリ
src_dir = Path.home() / "Pictures" / "gallery_src"
out_dir = Path.home() / "Pictures" / "gallery_out"
thumb_dir = out_dir / "thumbs"
albums_dir = out_dir / "albums"
# 出力フォルダ作成
out_dir.mkdir(exist_ok=True)
thumb_dir.mkdir(exist_ok=True)
albums_dir.mkdir(exist_ok=True)
# 対象拡張子
extensions = {".jpg", ".jpeg", ".png", ".webp"}
# EXIFタグ名の逆引きマップ作成("DateTimeOriginal" を探す)
EXIF_TAGS = {v: k for k, v in ExifTags.TAGS.items()}
def get_taken_date(img_path: Path) -> str:
"""画像の撮影日(YYYY-MM-DD)を返す。EXIFがなければファイル更新日を使用。"""
try:
with Image.open(img_path) as img:
exif = img._getexif()
if exif:
dto_tag = EXIF_TAGS.get("DateTimeOriginal")
if dto_tag in exif:
raw = exif[dto_tag] # 例: "2023:07:14 12:34:56"
# EXIFの日時を正規化
try:
dt = datetime.strptime(raw, "%Y:%m:%d %H:%M:%S")
return dt.date().isoformat() # "YYYY-MM-DD"
except Exception:
pass
except Exception:
pass
# 代替: ファイルの更新日
mtime = datetime.fromtimestamp(img_path.stat().st_mtime)
return mtime.date().isoformat()
def make_thumbnail(src: Path, dst: Path, size=(240, 240)) -> None:
"""アスペクト比を保ったサムネイルをJPEGで保存(軽量化)。"""
with Image.open(src) as img:
img.thumbnail(size)
if img.mode != "RGB":
img = img.convert("RGB")
dst.parent.mkdir(exist_ok=True)
img.save(dst, "JPEG", quality=85)
# 画像メタの収集とアルバム分け
albums = {} # date -> list[dict(full, thumb, title)]
for img_path in sorted(src_dir.iterdir()):
if not img_path.is_file() or img_path.suffix.lower() not in extensions:
continue
date = get_taken_date(img_path)
thumb_name = f"{img_path.stem}_thumb.jpg"
thumb_path = thumb_dir / date / thumb_name # 日付ごとにサムネイルサブフォルダ
make_thumbnail(img_path, thumb_path)
# 出力側に元画像もコピー(HTMLから相対参照できるように)
copy_full_dir = out_dir / "images" / date
copy_full_dir.mkdir(parents=True, exist_ok=True)
full_copy_path = copy_full_dir / img_path.name
if not full_copy_path.exists():
shutil.copy(img_path, full_copy_path)
item = {
"full": str((Path("images") / date / img_path.name).as_posix()),
"thumb": str((Path("thumbs") / date / thumb_name).as_posix()),
"title": html.escape(img_path.stem),
}
albums.setdefault(date, []).append(item)
# アルバムページ生成(各日付ごと)
def render_album(date: str, items: list[dict]) -> Path:
album_html = f"""<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>アルバム {date}</title>
<style>
body {{ margin:0; font-family: system-ui, sans-serif; background:#111; color:#eee; }}
header {{ padding:16px; text-align:center; font-size:18px; }}
.grid {{
display:grid; grid-template-columns:repeat(auto-fill, minmax(160px,1fr));
gap:12px; padding:12px;
}}
.card {{ background:#1b1b1b; border-radius:8px; overflow:hidden; border:1px solid #2a2a2a; }}
.thumb {{ display:block; width:100%; height:auto; aspect-ratio:1/1; object-fit:cover; background:#222; }}
.meta {{ padding:8px; font-size:12px; color:#bbb; }}
a {{ color:inherit; text-decoration:none; }}
nav {{ padding:8px; text-align:center; }}
a:focus-visible {{ outline:2px solid #4ea3ff; outline-offset:2px; }}
</style>
</head>
<body>
<header>アルバム {date}({len(items)}枚)</header>
<nav><a href="../index.html">一覧に戻る</a></nav>
<main class="grid">
{"".join(f'''
<div class="card">
<a href="{html.escape(item["full"])}" target="_blank" rel="noopener">
<img class="thumb" src="{html.escape(item["thumb"])}" alt="{item["title"]}" loading="lazy">
<div class="meta">{item["title"]}</div>
</a>
</div>
''' for item in items)}
</main>
</body>
</html>
"""
out_path = albums_dir / f"{date}.html"
out_path.write_text(album_html, encoding="utf-8")
return out_path
album_links = []
for date, items in sorted(albums.items()):
page = render_album(date, items)
album_links.append((date, page.name, len(items)))
# インデックスページ生成(全日付へのリンク)
index_html = f"""<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>画像ギャラリー(日付別アルバム)</title>
<style>
body {{ margin:0; font-family: system-ui, sans-serif; background:#111; color:#eee; }}
header {{ padding:16px; text-align:center; font-size:18px; }}
ul {{ list-style:none; padding:0 16px; }}
li {{ margin:8px 0; padding:12px; background:#1b1b1b; border:1px solid #2a2a2a; border-radius:8px; }}
a {{ color:#eee; text-decoration:none; }}
a:hover {{ text-decoration:underline; }}
</style>
</head>
<body>
<header>画像ギャラリー(日付別アルバム)</header>
<ul>
{"".join(f'<li><a href="albums/{html.escape(name)}">{html.escape(date)} のアルバム({count}枚)</a></li>' for date, name, count in album_links)}
</ul>
</body>
</html>
"""
(out_dir / "index.html").write_text(index_html, encoding="utf-8")
print(f"ギャラリーのトップ: {out_dir / 'index.html'}")
print(f"アルバムページ: {albums_dir}")
print(f"サムネイル保存先: {thumb_dir}")
print(f"元画像コピー先: {out_dir / 'images'}")
Python使い方のポイント
- EXIFの撮影日時: 多くのカメラ・スマホは
DateTimeOriginalを持っています。ない場合はファイル更新日を代用しています。 - サムネイル軽量化: JPEGの
quality=85はバランス良い画質。さらに軽量化したい場合は 70–80 を試す。 - 拡張子対応:
extensionsに".gif",".tiff",".heic"(HEICはPillowの追加プラグインが必要)などを追加可能。 - 相対パス: HTMLから参照しやすいように、
out_dir配下へ「images/date/ファイル」をコピーしています。 - Lazy Load:
<img loading="lazy">で初期表示を高速化。
よくある拡張
- 並び順: アルバム内で撮影時刻やファイル名順に並べたい場合は
sorted(items, key=...)を適用。 - EXIF補助:
DateTimeやDateTimeDigitizedをフォールバックに使う。 - メタ表示: EXIFのレンズ名、露出、ISOなどを
metaに表示。 - 月別インデックス:
YYYY-MMごとにまとめたページを追加。 - 重複検出: ハッシュ(
hashlib.md5)で重複画像をスキップ。
まとめ
- pathlib: OS差異を意識せずに直感的なパス操作。
- Pillow: EXIFで撮影日を取得、アスペクト比維持でサムネイル生成。
- HTML自動生成: 日付別アルバムページとトップインデックスを自動作成。


