
「影像整理地獄重生記:用 Python 打造聰明的重複照片掃描器」
我從大學開始接觸攝影至今的所有照片,JPG 不用說,連 RAW 檔都還完整保存著。這些無止境增生的照片對我來說是個令人頭痛的問題,嚴格來說是「曾經」是個問題。可能跟很多攝影師一樣,我是個典型的數據囤積者(Digital Hoarder)。
這一切的起因,是因為我一直在切換不同的照片管理方式。一開始,我只是單純把 SD 卡的照片一張不剩地噴到硬碟裡,這導致我手邊有一大堆檔名長得很像、資料夾亂七八糟的檔案。想找張照片?祝我好運。
後來,我跳槽到了 Apple Photos,雖然功能完善用起來也相對順手比起沒有整理已經是大加分,但我還是不習慣所有照片被封閉系統的程式控管,我連最基本的利用照片原檔都困難重重,期間Photos隨著MacOS升版甚至一夕之間喪失了對HDD的支援,我的所有相片就這樣被困在裡頭。
最後,我才終於定居在 Lightroom Classic 的「資料夾管理法」,按照日期歸類。就算脫離了 Lightroom,在檔案系統裡看也一清二楚,不用擔心被綁定在特定的生態系,心情才終於覺得告一段落。
但在這幾次搬家的過程中,我的照片庫徹底大爆炸了。Apple Photos 之前把我的照片全部重新命名成一堆 Hash(雜湊值),導致我根本認不出來誰是誰,整個庫的大小甚至膨脹到原本的三倍大。我曾經嘗試過一格一格手動檢查,但我很快就發現:要把這 10 萬張照片看完,根本是在浪費生命。
身為工程師,遇到這種重複地獄,唯一合理的解決方案就是:花 20 小時寫一個 Python 腳本,來省下 5 小時的手動工作。 (這很工程師,對吧?)
歡迎來到這個系列的第一集。今天,我們要來抓那些最簡單的:「物理級」完全一模一樣的檔案。
關於重複檔案的幻覺
找重複檔案聽起來很簡單,寫個腳本去檢查檔名一不一樣不就好了?
錯。大錯特錯。
當你在 Apple Photos、Lightroom 或 SD 卡之間搬家時,檔名通常是第一個被毀滅的東西。SD 卡裡的 IMG_1234.JPG 拷貝兩次後可能變成 IMG_1234 (1).JPG,而 Apple Photos 更狠,可能直接把它改成 A83J92K-19XJ.JPG。
我們不能相信檔名。我們要看的是檔案的「靈魂」——也就是它的二進位數據(Bytes)。
神探登場:加密雜湊演算法 (SHA-256)
如果我們想知道兩個檔案是否完全相同,理論上可以逐字對比每個位元組。但面對幾萬個檔案,這樣做效率極低,記憶體會先爆掉。這時候我們需要一個觀念:雜湊 (Hashing)。
你可以把雜湊想像成檔案的「數位指紋」。我們把檔案的數據丟進一個數學演算法(這裡我用的是 SHA-256),它會回傳一串固定長度的字串。
雜湊的鐵律:
即便一個 5MB 的檔案中只改動了一個位元組(例如改了一個 EXIF 標籤),產出的雜湊值也會翻天覆地地改變。反過來說,如果兩個檔案的 SHA-256 指紋一模一樣,你幾乎可以 100% 確定它們就是同一個檔案,管它檔名叫什麼。
雜湊的陷阱
但這裡有個伏筆:如果照片「看起來」一樣,但「數據」不一樣呢?
相機在拍照時會寫入 Metadata(像 GPS、快門參數、日期等)。因為 SHA-256 是盲目地計算檔案的每一個位元組,如果某個 App 幫你改了 EXIF,雖然像素點沒變,但雜湊值會完全不同。這時候,這個腳本就會覺得它們是兩個不同的檔案。
實作:抓出那些換了皮的拷貝本
在 Python 裡,用內建的 hashlib 就能輕鬆搞定雜湊。這是我用的掃描函數:
```python
import hashlib
def_sha256(path: str) -> str: h = hashlib.sha256()withopen(path, "rb") as f:# 關鍵:分塊讀取 (1MB chunks)for chunk initer(lambda: f.read(1 << 20), b""): h.update(chunk)return h.hexdigest()
```
記憶體的小撇步
有沒有注意到那個 iter(lambda: f.read(1 << 20), b"")?這非常重要。
我的照片庫裡不只有照片,還有很多 2GB 的 4K 影片。如果你直接用 f.read(),Python 會試著把整個 2GB 塞進你的 RAM 裡。如果你同時跑好幾個處理程序,你的電腦大概會直接起飛。透過每次只讀取 1MB 的小塊,我們在處理超大檔案時,記憶體佔用依然能維持在極低的水準。
那個「Aha!」的瞬間
有了指紋,剩下的就是簡單的分類了。我們掃描每個檔案,算出雜湊值,然後丟進一個字典(Dictionary)裡。
```python
from collections import defaultdict
# 簡化版的邏輯
sha_map = defaultdict(list)
for file in files:
hash_value = _sha256(file)
sha_map[hash_value].append(file)
# 只要同一個 Hash 對應超過一個檔案,恭喜你,抓到重複了!
exact_dupes = {k: v for k, v in sha_map.items() if len(v) > 1}
```
當我第一次在我的 10 萬張檔案跑這段 code 時,那種快感真的難以形容。我抓到了幾十 GB 躲在不同檔名下的重複影片和照片,通通被這個腳本揪出來。
但,我的工程師優越感沒持續太久。
下集待續:那個被忽略的角落
當我翻閱結果時,我發現有些東西不見了。
我有一組家庭旅遊的照片,我很確定有重複,因為一張是家庭Line群組備份的,一張是我從 Google Photos 載下來的。對人類肉眼來說,它們是一模一樣的照片,但我的腳本卻沒抓到它們。
為什麼?因為照片經過 Line 傳輸時會被壓縮。檔案大小變了、畫質微調了。只要改了一個位元組,SHA-256 的指紋就完全不同。
我的腳本現在只會找「位元組」一模一樣的檔案,但我需要它學會像人類一樣「看」照片。
下一集:教 Python 學會看圖——「感知雜湊(Perceptual Hashing)」的神奇魔力。