이미지 수집기를 오래 돌리며 배운 것 - 중복·증분·재시도
작성일 2026-07-26 · SameOS Tools
이미지를 자동으로 모아 정리하는 스크립트를 몇 달째 돌리고 있습니다. 지금까지 쌓인 것이 15만 장, 136GB인데 이 규모가 되기까지 세 번 크게 고쳤습니다. 처음 만든 30줄짜리 스크립트는 잘 돌아가는 것처럼 보였지만, 며칠 지나자 같은 사진이 이름만 다르게 수십 장씩 쌓였고, 매번 처음부터 다시 받느라 시간이 배로 들었고, 네트워크가 한 번 끊기면 통째로 멈춰 있었습니다. 이 글은 그 세 가지를 어떻게 고쳤는지에 대한 기록입니다. 수집 대상이 무엇이든 - 내 서버에 흩어진 사진을 정리하든, 공개 API에서 이미지를 받든 - 그대로 쓸 수 있는 이야기입니다.
시작하기 전에 - 무엇을 모을 것인가
기술 이야기에 앞서 한 가지만 짚습니다. 수집 대상은 반드시 내 것이거나, 공개적으로 허용된 것이어야 합니다. 사이트마다 robots.txt(수집 로봇에게 어디까지 허용하는지 적어 둔 파일)와 이용약관이 있고, 로그인이 필요한 화면을 자동으로 긁는 것은 대부분 약관 위반입니다. 남의 사진을 모으는 것은 저작권·초상권 문제로도 이어집니다. 이 글의 예제는 내 서버에 이미 있는 이미지들을 정리하는 상황을 기준으로 합니다 - 여러 폴더에 흩어진 사진을 한곳에 모으고 중복을 걷어내는, 누구나 안전하게 해볼 수 있는 작업입니다. 공개 API를 쓰신다면 그쪽 요청 제한과 이용 조건을 먼저 확인하세요.
문제 1 - 파일명으로는 중복을 못 잡습니다
제일 먼저 부딪힌 문제입니다. 같은 사진인데 이름이 photo.jpg, photo(1).jpg, IMG_2451.jpg로 제각각이면 파일명 비교로는 절대 못 걸러냅니다. 반대로 이름이 같아도 내용은 다른 경우가 있고요. 답은 내용으로 판단하는 것입니다. 파일 안의 바이트를 전부 읽어 SHA-256(내용이 1비트만 달라도 완전히 달라지는 지문값)을 계산하면, 이름이 무엇이든 같은 내용은 같은 지문이 나옵니다. 저는 이 지문을 파일마다 한 번씩 계산해 두고, 새 파일이 들어올 때마다 그 목록에 있는지만 확인합니다.
# dedupe.py - 내용 기준 중복 이미지 찾기
import hashlib
from pathlib import Path
EXTS = {".jpg", ".jpeg", ".png", ".webp", ".gif"}
def file_hash(path, chunk=1024 * 1024):
"""파일 내용의 SHA-256 지문. 큰 파일도 안전하게 1MB씩 나눠 읽는다."""
h = hashlib.sha256()
with open(path, "rb") as f:
while True:
block = f.read(chunk)
if not block:
break
h.update(block)
return h.hexdigest()
def find_duplicates(folder):
"""{지문: [파일들]} 형태로 묶어 2개 이상인 것만 돌려준다."""
index = {}
for p in Path(folder).rglob("*"):
if p.is_file() and p.suffix.lower() in EXTS:
index.setdefault(file_hash(p), []).append(p)
return {h: files for h, files in index.items() if len(files) > 1}
if __name__ == "__main__":
for h, files in find_duplicates("./images").items():
keep, *drop = sorted(files, key=lambda p: len(str(p))) # 경로가 짧은 것을 남긴다
print(f"[남김] {keep}")
for d in drop:
print(f" [중복] {d}") # 확인 후 지울 것 - 바로 삭제하지 말 것
마지막 줄에 주목해 주세요. 처음 만들 때 저는 찾자마자 바로 지우게 했다가, 남길 파일을 잘못 고르는 바람에 원본을 날린 적이 있습니다. 그 뒤로는 목록만 출력하고 삭제는 사람이 확인한 뒤에 하도록 바꿨습니다. 자동 삭제를 넣더라도 "휴지통 폴더로 옮기기"까지만 해두면 되돌릴 수 있습니다.
문제 2 - 매번 처음부터 받지 않게 하기
수집 대상이 늘어날수록 뼈아파지는 부분입니다. 스크립트를 돌릴 때마다 전부 다시 확인하면, 어제 받은 1만 장을 오늘 또 확인하느라 시간이 계속 늘어납니다. 해결은 간단합니다 - 이미 처리한 것을 기억해 두고 건너뛰는 것입니다. 저는 앞서 만든 지문을 작은 데이터베이스(SQLite, 파일 하나로 끝나는 가벼운 DB)에 쌓아 두고, 새 파일이 들어오면 지문만 조회합니다. 15만 장이 쌓인 지금도 새로 도는 시간은 몇 초에서 끝납니다. 텍스트 파일에 한 줄씩 적어도 되지만, 수만 줄이 넘어가면 조회가 느려져서 DB가 편합니다.
# seen.py - 이미 처리한 파일을 기억해 두고 건너뛰기
import sqlite3
from pathlib import Path
from dedupe import file_hash, EXTS
db = sqlite3.connect("seen.db")
db.execute("CREATE TABLE IF NOT EXISTS seen (hash TEXT PRIMARY KEY, path TEXT, at TEXT)")
db.commit()
def is_new(path):
"""처음 보는 내용이면 True를 돌려주고 기록한다."""
h = file_hash(path)
row = db.execute("SELECT 1 FROM seen WHERE hash = ?", (h,)).fetchone()
if row:
return False # 전에 처리한 내용 - 건너뜀
db.execute("INSERT INTO seen (hash, path, at) VALUES (?, ?, datetime('now'))",
(h, str(path)))
db.commit()
return True
new_count = skip_count = 0
for p in Path("./images").rglob("*"):
if p.is_file() and p.suffix.lower() in EXTS:
if is_new(p):
new_count += 1 # 여기서 실제 작업(복사·변환 등)
else:
skip_count += 1
print(f"새 파일 {new_count}장 / 건너뜀 {skip_count}장")
문제 3 - 한 번 실패해도 멈추지 않게
오래 도는 스크립트는 반드시 실패를 만납니다. 네트워크가 잠깐 끊기거나, 상대 서버가 잠시 응답하지 않거나, 파일 하나가 깨져 있거나. 처음 만든 스크립트는 이럴 때 그냥 죽어서, 아침에 확인하면 밤새 아무것도 안 하고 멈춰 있었습니다. 필요한 것은 두 가지입니다. 첫째 실패한 하나 때문에 전체가 멈추지 않게 하고, 둘째 다시 시도하되 간격을 점점 늘리는 것입니다. 실패하자마자 곧바로 재시도하면 상대 서버에 부담만 주고 똑같이 실패합니다. 2초, 4초, 8초로 늘려 가며 기다리면 일시적인 문제는 대개 그 사이에 풀립니다.
# fetch.py - 실패해도 멈추지 않고, 간격을 늘려 가며 재시도
import time
import requests
def fetch(url, tries=3):
"""실패하면 2초, 4초, 8초로 기다렸다 재시도. 끝내 실패하면 None."""
for i in range(tries):
try:
r = requests.get(url, timeout=15,
headers={"User-Agent": "my-collector/1.0"})
if r.status_code == 200:
return r.content
if r.status_code == 429: # 너무 자주 요청함 - 더 오래 기다린다
time.sleep(30)
continue
return None # 404 등은 재시도해도 의미 없음
except requests.RequestException:
pass
time.sleep(2 ** (i + 1)) # 2초 → 4초 → 8초
return None
for url in ["https://example.com/a.jpg", "https://example.com/b.jpg"]:
data = fetch(url)
if data is None:
print(f"실패(건너뜀): {url}") # 하나 실패해도 다음으로 계속 간다
continue
print(f"성공: {url} ({len(data):,} 바이트)")
time.sleep(1) # 요청 사이 간격 - 상대 서버에 대한 예의
15만 장이 되고 나서야 알게 된 것들
규모가 커지면서 예상 못 한 문제도 나왔습니다. 첫째, 한 폴더에 파일이 수만 개 쌓이면 탐색기도 명령어도 느려집니다. 날짜별로 하위 폴더를 나누는 것만으로 해결됐습니다. 둘째, 용량은 원본 형식이 좌우합니다. 저장만 해두는 이미지를 WebP로 바꾸니 화질 차이는 거의 못 느끼면서 용량이 크게 줄었습니다. 셋째, 지문 목록을 메모리에 전부 올리지 마세요. 15만 개를 파이썬 딕셔너리에 담으면 메모리를 꽤 먹습니다. 위 예제처럼 DB에 두고 필요할 때 한 건씩 조회하는 편이 안전합니다. 넷째, 이 모든 것보다 중요한 것은 백업입니다. 중복 제거 스크립트에 실수가 있으면 원본까지 지웁니다.
마무리
정리하면 오래 도는 수집기에 필요한 것은 세 가지입니다 - 이름이 아니라 내용으로 중복을 판단하고, 이미 한 일은 기억해서 건너뛰고, 실패해도 멈추지 않게 하는 것. 그리고 이 셋보다 먼저 확인할 것이 "이걸 모아도 되는가"입니다. 기술적으로 가능한 것과 해도 되는 것은 다르고, 그 선을 넘지 않는 선에서도 만들 수 있는 것은 충분히 많습니다.