這兩天,Esther 告訴我對岸幣圈大佬和女明星的事情。網路說,這應該是到目前為止,Claude 投資報酬率表現最好的一個案例。洋洋灑灑六千字的自白,幾乎只有 AI 寫得出來。

猶豫許久,其實我沒有用過 Claude,因為想讓子彈飛一會兒。AI 工具蓬勃發展,今日的王者可能就是明日黃花。今天趁著八卦當頭,把 Claude 叫出來看看。
試了約莫半個小時,先用 Claude 搞定我想要的一個小功能。把結果留在這裡,或許喜歡拍照的你 (妳) 也用得到。
一般整理照片時,我都是先建立對應的年份資料夾,然後在年份下繼續建立照片拍攝日期資料夾,再把照片儲存在底下。例如 E:\2026\2026-08-10\IMG_xxxx.cr3 (.cr3 是 Canon 使用的 RAW 檔格式)。原本都是插入 SD 卡手動進行這件事,如建資料夾、選取該天照片、複製到資料夾底下。檔案一多,這件事情就囉嗦了起來。今回直接用小工具解決它。


解法是使用 Windows PowerShell。Powershell 是 Microsoft 開發的命令列殼層與腳本環境,專門用來自動化系統管理與設定工作。最終,Claude 在我的要求下產生了兩個檔案:Sort-PhotosByDate.ps1,以及放在桌面可以隨時執行的 Sync-Camera.bat。
開發的構想很簡單,就是把 SD 上自上次拷貝後新拍的照片,按照日期複製到電腦硬碟上的對應資料夾。而且我有兩台相機 (R 和 R8),程式必須自動識別出是哪一台相機的 SD 卡,然後記錄最後一次拷貝日期。







幾次測試過後,這一支 Sort-PhotosByDate.ps1 程式已經可以正常運作了。我把對應的批次檔 Sync-Camera.bat 放在桌面上,只在需要時點按執行,可以省下不少人工操作。我終於真正用 Claude 做了一件有生產力的事情。




這裡貼上程式碼,提供給有興趣的人。只需將整份程式碼貼進 Claude,要它根據你的需求改寫即可。
Sort-PhotosByDate.ps1
<#
.SYNOPSIS
將 SD 卡上的照片,依拍攝日期 (yyyy-MM-dd) 自動分類複製到指定磁碟機,並依 SD 卡容量自動判斷是哪一台相機。
.DESCRIPTION
- 自動判斷相機:讀取來源磁碟機的「總容量」,比對是 64GB 還是 128GB 的卡,
藉此判斷是 R 相機還是 R8 相機的 SD 卡。容量是實體規格,不會因為格式化
而改變,比序號或磁碟區標籤更穩定可靠。
- 遞迴掃描來源磁碟機底下的照片檔案
- 優先讀取 EXIF 拍攝日期 (DateTaken),若無 EXIF (例如 RAW/CR3 檔) 則使用
檔案最後修改日期
- 依日期建立資料夾 (例如 D:\Family\2026\2026-08-28,多一層年份資料夾)
- 複製檔案,若目的地已有同名檔案會自動加上流水號,不會覆蓋
- 依「相機代號」分別記錄上一次執行的時間,之後每次執行只處理該相機
SD 卡上「檔案最後修改時間」晚於上次執行時間的新照片,避免重複處理
.NOTES
容量對照表 (可依實際狀況調整,單位 GB,範圍設寬一點以涵蓋容量計算誤差):
50 ~ 80 GB → 相機 R (64GB 卡)
100 ~ 140 GB → 相機 R8 (128GB 卡)
使用方式:
1. 將任一張 SD 卡插入電腦
2. 直接執行,程式會自動依容量判斷相機:
.\Sort-PhotosByDate.ps1 -SourceDrive "F:\"
3. 若容量無法辨識,或想手動指定相機代號,可加 -Camera 覆寫:
.\Sort-PhotosByDate.ps1 -SourceDrive "F:\" -Camera R
4. 若要用不同的目的地磁碟機,可加參數,例如:
.\Sort-PhotosByDate.ps1 -SourceDrive "F:\" -DestRoot "D:\Family"
5. 若想忽略上次複製紀錄、強制重新掃描該相機 SD 卡上所有照片,加上 -Reset:
.\Sort-PhotosByDate.ps1 -SourceDrive "F:\" -Reset
6. 每台相機的上次執行時間分別記錄在 $DestRoot\.last_sync_<Camera>.txt,
例如 .last_sync_R.txt 和 .last_sync_R8.txt,兩者互不影響。
如需手動清除某台相機的紀錄,直接刪除對應檔案即可(效果等同 -Reset)
#>
param(
[string]$SourceDrive = "F:\",
[string]$DestRoot = "D:\Family",
[string]$Camera = "", # 選用:手動指定相機代號,留空則自動依 SD 卡容量判斷
[switch]$Move, # 加上 -Move 參數則改為「搬移」而非「複製」
[switch]$Reset # 加上 -Reset 參數則忽略上次執行紀錄,處理所有照片
)
# SD 卡容量 (GB) -> 相機代號 對照表,範圍設寬一點以涵蓋容量計算誤差
# 如果你之後又買了新的 SD 卡或相機,在這裡加一筆即可
$CameraCapacityRanges = @(
[PSCustomObject]@{ Camera = "R"; MinGB = 50; MaxGB = 80 } # 64GB 卡
[PSCustomObject]@{ Camera = "R8"; MinGB = 100; MaxGB = 140 } # 128GB 卡
)
if (-not (Test-Path $SourceDrive)) {
Write-Error "找不到來源磁碟機 $SourceDrive,請確認 SD 卡是否已插入電腦,且磁碟機代號正確。"
exit 1
}
if (-not (Test-Path $DestRoot)) {
New-Item -ItemType Directory -Path $DestRoot -Force | Out-Null
}
# 若未手動指定 -Camera,讀取磁碟機總容量,比對對照表判斷相機
if ([string]::IsNullOrWhiteSpace($Camera)) {
$driveRoot = $SourceDrive.Substring(0, 2) + "\" # 例如 "F:"
try {
$driveInfo = New-Object System.IO.DriveInfo($driveRoot)
$totalGB = [math]::Round($driveInfo.TotalSize / 1GB, 1)
}
catch {
Write-Error "無法讀取磁碟機 $SourceDrive 的容量資訊,請改用 -Camera 參數手動指定。"
exit 1
}
$matched = $CameraCapacityRanges | Where-Object { $totalGB -ge $_.MinGB -and $totalGB -le $_.MaxGB } | Select-Object -First 1
if ($matched) {
$Camera = $matched.Camera
Write-Host "自動偵測到容量 $totalGB GB,判斷為相機:$Camera"
}
else {
Write-Error "SD 卡容量為 $totalGB GB,不在對照表的任何範圍內,無法自動判斷相機。`n請改用 -Camera 參數手動指定,例如:`n .\Sort-PhotosByDate.ps1 -SourceDrive `"$SourceDrive`" -Camera R`n或修改腳本開頭的 `$CameraCapacityRanges 對照表。"
exit 1
}
}
# 記錄上次執行時間的狀態檔案,依相機代號分開存放,存放在目的地資料夾底下
# 例如 D:\Family\.last_sync_R.txt、D:\Family\.last_sync_R8.txt
$SafeCameraName = ($Camera -replace '[\\/:*?"<>|]', '_')
$StateFile = Join-Path $DestRoot ".last_sync_$SafeCameraName.txt"
# 支援的照片副檔名,可依需求增減 (例如加入 .heic, .dng 等)
$PhotoExtensions = @(
".jpg", ".jpeg", ".png", ".tif", ".tiff", ".bmp", ".heic",
".cr2", ".cr3", ".nef", ".arw", ".raf", ".orf", ".rw2", ".dng"
)
# 讀取上次執行時間 (若 -Reset 或狀態檔不存在,視為第一次執行,處理全部照片)
$lastSyncTime = $null
if (-not $Reset -and (Test-Path $StateFile)) {
$raw = Get-Content $StateFile -Raw -ErrorAction SilentlyContinue
if ($raw) {
try {
$lastSyncTime = [datetime]::Parse($raw.Trim())
}
catch {
Write-Warning "無法解析上次執行時間紀錄,將視為第一次執行處理全部照片。"
}
}
}
if ($lastSyncTime) {
Write-Host "[$Camera] 上次執行時間:$($lastSyncTime.ToString('yyyy-MM-dd HH:mm:ss')),只處理此之後新增的照片。"
} else {
Write-Host "[$Camera] 找不到上次執行紀錄(或使用 -Reset),將處理 SD 卡上所有照片。"
}
# 這次執行開始的時間,稍後成功處理完會寫回狀態檔,作為下次比對的基準
$runStartTime = Get-Date
Add-Type -AssemblyName System.Drawing
function Get-PhotoDate {
param([string]$FilePath)
$ext = [System.IO.Path]::GetExtension($FilePath).ToLower()
# 只有點陣圖格式 (jpg/jpeg/tiff 等) 可用 System.Drawing 讀 EXIF
# RAW 格式 (CR2/CR3/NEF 等) 讀不到,會直接 fallback 用檔案修改時間
if ($ext -in @(".jpg", ".jpeg", ".tif", ".tiff")) {
try {
$stream = [System.IO.File]::OpenRead($FilePath)
$img = [System.Drawing.Image]::FromStream($stream, $false, $false)
# EXIF PropertyTag 0x9003 = DateTimeOriginal (拍攝日期)
$propId = 0x9003
if ($img.PropertyIdList -contains $propId) {
$prop = $img.GetPropertyItem($propId)
$dateStr = [System.Text.Encoding]::ASCII.GetString($prop.Value).Trim([char]0)
# EXIF 日期格式為 "yyyy:MM:dd HH:mm:ss"
$parsed = [datetime]::ParseExact($dateStr, "yyyy:MM:dd HH:mm:ss", $null)
$img.Dispose()
$stream.Close()
return $parsed
}
$img.Dispose()
$stream.Close()
}
catch {
# 讀取失敗就往下 fallback
}
}
# Fallback:使用檔案的最後修改時間
return (Get-Item $FilePath).LastWriteTime
}
# 掃描照片檔案
Write-Host "[$Camera] 正在掃描 $SourceDrive 底下的照片檔案..."
$files = Get-ChildItem -Path $SourceDrive -Recurse -File -ErrorAction SilentlyContinue |
Where-Object { $PhotoExtensions -contains $_.Extension.ToLower() }
# 若有上次執行時間,只保留「檔案最後修改時間」晚於該時間的新照片
if ($lastSyncTime) {
$files = $files | Where-Object { $_.LastWriteTime -gt $lastSyncTime }
}
if ($files.Count -eq 0) {
Write-Host "[$Camera] 沒有需要處理的新照片。"
# 即使沒有新照片,仍更新時間紀錄,避免下次重複掃描同一段時間
$runStartTime.ToString("o") | Set-Content -Path $StateFile -Encoding UTF8
exit 0
}
Write-Host "[$Camera] 共找到 $($files.Count) 個檔案,開始處理..."
$counter = 0
foreach ($file in $files) {
$date = Get-PhotoDate -FilePath $file.FullName
$yearFolder = $date.ToString("yyyy")
$dayFolder = $date.ToString("yyyy-MM-dd")
$destFolder = Join-Path (Join-Path $DestRoot $yearFolder) $dayFolder
if (-not (Test-Path $destFolder)) {
New-Item -ItemType Directory -Path $destFolder -Force | Out-Null
}
$destPath = Join-Path $destFolder $file.Name
# 避免同名檔案被覆蓋,自動加上流水號
$i = 1
while (Test-Path $destPath) {
$newName = "{0}_{1}{2}" -f [System.IO.Path]::GetFileNameWithoutExtension($file.Name), $i, $file.Extension
$destPath = Join-Path $destFolder $newName
$i++
}
if ($Move) {
Move-Item -Path $file.FullName -Destination $destPath
} else {
Copy-Item -Path $file.FullName -Destination $destPath
}
$counter++
Write-Progress -Activity "整理照片中" -Status "$counter / $($files.Count)" -PercentComplete (($counter / $files.Count) * 100)
}
# 更新上次執行時間紀錄,下次執行只會處理這個時間點之後的新照片
$runStartTime.ToString("o") | Set-Content -Path $StateFile -Encoding UTF8
Write-Host "[$Camera] 完成!共處理 $counter 個檔案,已依日期分類到 $DestRoot"
Sync-Camera.bat
@echo off
REM ============================================================
REM SD card photo auto sync (auto-detects camera by SD card capacity)
REM If script path or drive letter is different, edit the two
REM lines below (SCRIPT_PATH and SD_DRIVE)
REM ============================================================
set SCRIPT_PATH=E:\Sort-PhotosByDate.ps1
set SD_DRIVE=F:
powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_PATH%" -SourceDrive "%SD_DRIVE%"
echo.
echo ============================================================
echo Done. Please check above for any error messages.
echo This window will not close automatically - press any key.
echo ============================================================
pause >nul