1. Introduction
이 앱은 Python 초보자용 코드가 아닙니다. 제가 필요해서 노트 대신 임시 코드로 올려둡니다. SR 안드로이드 버전. 일단 코드만.
경고 : 이 프로그램은 연구 및 실험용입니다. 실무에 적용하시면 큰 문제가 될 수도 있습니다. 사용자의 특정 실험용 폴더만을 대상으로 사용해야 합니다. 그것도 복사본이 있는 폴더와 파일들만을 대상으로 해야 합니다.
이 앱은 파일 이름을 다듬어 주는 일종의 File Name Renamer입니다. 기본적으로 파일명에서 기호, 특수 문자들을 밑줄 문자로 대체합니다. 이 기능뿐입니다. 시작은 일단 그렇습니다. 범용으로 만들 계획은 아직 없으며 당장의 필요를 채우면 끝입니다만. ^.^;
이 앱과 관련된 주제가 문자 및 문자열, 정규표현식, os 모듈, tkinter GUI 등과 연관되어 있어서 이런 저런 다양한 코딩 연습에 도움이 될 듯합니다.
이 앱은 완성판이 아니며 미완성 부분 코드로 끝날 수 있습니다. 여러가지 '코딩 기법' 참고용 입니다. 간결한 문자열 처리를 위한 re 모듈 이외에 GUI 형태로 사용되어 학습자라면 tkinter 모듈도 다룰 수 있어야 합니다.
이후 어느 정도 안정적이 되면 GUI로 tkinter 대신에 PyQt, wxPython, Kivy 등을 고려할 수도 있을 듯. 아직은 tkinter만.
아래 본문 코드는 최종 업데이트 코드 및 코드 주석과 다소 다를 수도 있습니다. 최종 소스 코드 파일과 자체 주석을 참고하세요.
안드로이드용을 우선적으로 고려해서 스마트폰과 PC를 왔다 갔다, 스마트폰으로 직접 글쓰기는 제법 힘들군요. 티스토리 편집기의 반항적 기질(?)도 만만치 않고.
ㅡ.ㅡ;


기본 설정으로 파일명에서 다음 문자들을 밑줄 문자로 변환합니다. 업데이트 하려면 끝도 없어 보입니다.
현재 최신 버전은 v1.0.9.b4.rev193입니다. 본문 업데이트 수정은 추후.

안드로이드용 WinRAR로 뭔가를 처리하려는데 에러가 생겨서 임시로. 일을 크게 벌이지 말아야 하는데. ㅡ.ㅡ;
안드로이드용 WinRAR 앱은 특이하게도 '공유' 방식을 통해서 압축과 해제를 실행합니다( '내 파일' 앱에서 파일 우선 선택의 경우. '내 파일' 앱에서가 아니라 RAR 앱을 독립적으로 먼저 실행할 경우에는 해당사항 없음). 약간 불편하지만 실행 결과는 가장 신뢰할 만합니다. 처리 대상 파일명에 포함된 기호나 특수 문자가 오류의 원인일 수 있어서 아래 코드 앱을 통해서 임시 방편 해결책으로.
간단한 앱인데 tkinter GUI가 포함되니 제법 코드가 길어지네요.
2. Code
# SR for Android v1.0.9.b4.rev.193이 최신.
# 최신 버전은 하단 첨부 파일을 참고하세요.
# ...
# 이하 코드는v1.0.5 b1 코드입니다.
import os
import re
import tkinter as tk
from tkinter import filedialog
class FileRenamerApp:
def __init__(self, root):
self.root = root
self.root.title("SRA v1.0.5 b1")
self.root.geometry("1200x900+25+25")
self.default_folder = "/storage/emulated/0/Download/T_"
self.create_widgets()
def close_window(self):
# 이벤트 루프 종료 이후 윈도우 파괴
self.root.quit()
self.root.destroy()
def create_widgets(self):
# 대상 디렉토리 입력
tk.Label(self.root, text="대상 디렉토리 경로").pack(pady=(10, 0))
folder_frame = tk.Frame(self.root)
folder_frame.pack(pady=2)
self.folder_entry = tk.Entry(folder_frame, width=45)
self.folder_entry.insert(0, self.default_folder)
self.folder_entry.pack(side="left", padx=5)
tk.Button(folder_frame, text="☆ 경로 선택", command=self.choose_folder).pack(side="left")
# 치환할 문자 입력
tk.Label(self.root, text="치환할 문자들 (예: . ,- ,)").pack(pady=(10, 0))
self.find_entry = tk.Entry(self.root)
self.find_entry.insert(0, ".,-,',+,@,~,&,[,],{,},(,),`,;,ù, ")
self.find_entry.pack()
# 대체 문자 입력
tk.Label(self.root, text="대체 문자 (예: _)").pack(pady=(5, 0))
self.replace_entry = tk.Entry(self.root)
self.replace_entry.insert(0, "_")
self.replace_entry.pack()
# 접두사/접미사 옵션
self.add_prefix_var = tk.BooleanVar()
self.add_suffix_var = tk.BooleanVar()
tk.Checkbutton(self.root, text="파일명 앞에 추가", variable=self.add_prefix_var).pack(pady=(10, 0))
self.prefix_entry = tk.Entry(self.root)
self.prefix_entry.insert(0, "PreFix_")
self.prefix_entry.pack()
tk.Checkbutton(self.root, text="파일명 뒤에 추가", variable=self.add_suffix_var).pack(pady=(10, 0))
self.suffix_entry = tk.Entry(self.root)
self.suffix_entry.insert(0, "_SufFix_")
self.suffix_entry.pack()
# 실행 버튼
tk.Button(self.root, text=">> 파일 이름 변환 실행", command=self.rename_files).pack(pady=15)
# 결과 표시
self.result_label = tk.Label(self.root, text="", fg="green")
self.result_label.pack()
# 닫기 버튼
exit_frame = tk.Frame(self.root)
exit_frame.pack(side="bottom", anchor="se", pady=(10, 8), padx=(0, 8))
tk.Button(exit_frame, text="닫기", command=self.close_window).pack()
def choose_folder(self):
selected_folder = filedialog.askdirectory()
if selected_folder:
self.folder_entry.delete(0, tk.END)
self.folder_entry.insert(0, selected_folder)
def generate_clean_unique_filename(self, folder, filename):
"""
기존의 연속된 _NNNN 접미사를 모두 제거하고,
마지막 숫자만 기준으로 다시 부여하여 중복 반복을 막습니다.
"""
base, ext = os.path.splitext(filename)
# 1) 연속된 _NNNN 패턴 모두 제거
prefix = re.sub(r'(_\d{4})+$', '', base)
# 2) 마지막에 붙어 있던 네 자리 숫자 추출
match = re.search(r'_(\d{4})$', base)
suffix_num = int(match.group(1)) if match else 0
# 3) 숫자를 증가시키며 유일한 이름 찾기
while True:
candidate = f"{prefix}_{suffix_num:04}_{ext}"
candidate = re.sub(r'_+', '_', candidate).rstrip('_') # 중복이나..
# 밑줄 오류 처리 1. 지금은 불필요. 끝 문자 잘라내기 참고용.
if candidate.endswith('_'):
candidate = candidate[:-1]
if not os.path.exists(os.path.join(folder, candidate)):
return candidate
suffix_num += 1
def rename_files(self):
folder = self.folder_entry.get().strip()
find_chars = self.find_entry.get()
replace_char = self.replace_entry.get()
if not os.path.exists(folder):
self.result_label.config(text=" X 경로가 존재하지 않습니다.", fg="red")
return
count = 0
for filename in os.listdir(folder):
full_path = os.path.join(folder, filename)
if not os.path.isfile(full_path):
continue
# 1) 확장자와 이름 분리
parts = filename.rsplit('.', 1)
if len(parts) < 2:
continue
name, ext = parts
# 2) 치환 및 중복 밑줄 제거
pattern = '[' + re.escape(find_chars) + ']'
clean_name = re.sub(pattern, replace_char, name)
clean_name = re.sub(r'_+', '_', clean_name).rstrip('_')
# 3) 접두/접미사 추가
if self.add_prefix_var.get():
clean_name = self.prefix_entry.get() + clean_name
if self.add_suffix_var.get():
clean_name = clean_name + self.suffix_entry.get()
# 4) 끝에 밑줄이 없으면 한 번만 붙이기 (중복이나 이렇게 할 수도 있다는 예시..)
if not clean_name.endswith('_'):
clean_name += '_'
clean_name = re.sub(r'_+', '_', clean_name).rstrip('_') # 중복이나..
# 5) 확장자 다시 붙이고, 고유 숫자 부여
tmp_filename = f"{clean_name}.{ext}"
final_name = self.generate_clean_unique_filename(folder, tmp_filename)
# 6) 이름 변경
os.rename(full_path, os.path.join(folder, final_name))
count += 1
self.result_label.config(text=f"※ {count}개 파일 이름 변경 완료!", fg="green")
if __name__ == "__main__":
root = tk.Tk()
app = FileRenamerApp(root)
root.mainloop()
3. Notes
□ 안드로이드용. Pydroid 3에서 코딩 및 실행되고, 갤럭시 9에서만 테스트됨.
□ 유니코드 이모지는 참고로만 : Windows에서는 문제 없어도 안드로이드에서는 유니코드와 유니코드 이모지 부분에 일부 문제 있음. 안드로이드 탓인지 tkinter 탓인지.. ^.^; 아니면 폰트 탓인지. 폰트도 영향이 있는 것으로. tkinter 윈도우의 타이틀 바에 표시할 경우.

■ cf. 파이썬 표기법으로 U+코드값 대신,
유니코드 Rocket 이모지의 경우,
Python 유니코드 약식 표기 방식, 4자리 방식,
\unnnn 방식 표기법은 여기서는 부적합.
Python 유니코드 확장 표기 방식, 8자리 방식. Full Space 방식. 여기서는 이 방식이 필요.
\U000......
\U0001F680
\U 다음에 8자리. 빈 앞자리는 0으로 채움.
반드시 대문자 U 사용? 소문자도 되는지는 테스트 필요. ^.^;
실습 및 확인은 패스. 추후.
이 부분은 이전 유니코드 관련 포스팅 참고.
https://grammar.tistory.com/m/35
■ f-string
f-string이란?
- f는 formatted string literal을 뜻하는 것으로, Python 3.6부터 도입된 문법입니다.
- 문자열 앞에 f를 붙이면, 중괄호 {} 안에 변수나 표현식을 직접 넣을 수 있습니다.
- 문자열 포매팅이 더 간결하고 직관적임.
self.result_label.config(text=f"※ {count}개 파일 이름 변경 완료!", fg="green")
■ Update 1 : 중복 밑 줄 문자 제거, 단일 밑줄 문자로 통합하기.
rename_files() 함수에 다음 코드 추가하기,
new_name = re.sub(r'_+', '_', new_name)
■ Update 2 :
1) 기본 처리 대상 문자 확대
self.find_entry = tk.Entry(self.root)
self.find_entry.insert(0, " .,-,',+,@,~,&,[,],{,},(,),`,;,ù, ")
self.find_entry.pack()
2) 후 처리 과정으로 특정 문자열 치환 도입
# 후 처리 영역 시작
# Bad String Process.
new_name = re.sub(r'bad_string_1', '_', new_name)
# 이하 복사/붙여넣기/문자열 수정.
new_name = re.sub(r'bad_string_2' , '_', new_name)
new_name = re.sub(r'bad_string_3' , '_', new_name)
new_name = re.sub(r'_+', '_', new_name)
# 마지막 정리, 중복일 수 있으나.
# 후 처리 영역 끝
■ Update 3 :
1) 파일명 앞 뒤 특정 문자열 추가.
2) 일부 중복 파일명 회피.
■ Update 4, 5 :
1) 알고리즘 새로 다시.
■ Update 6, 7 :
1) pack() to grid()
2) 기타 코드 정리.
■ Update 8 :
1) 외관만 동일하고
2) 내부는 거의 새로 다시
.. ^.^;
4. Version 1.0.8. b1
"""
Program Name : SR
Version : 1.0.8 beta 1
Date : 2025. 08. 10
Licence : Copyright(C) James 2025. All rights are reserved.
Description : This software is for non-commercial use only. Have a good time! ^.^;
"""
import os
import re
import tkinter as tk
from tkinter import filedialog
from typing import List
def strip_trailing_digits(filenames: List[str]) -> List[str]:
"""
끝에 4자리 숫자 패턴이 붙은 파일명만 대상으로,
제거했을 때
1) 원본 리스트에 같은 이름이 없고
2) 제거 결과끼리도 중복되지 않으면
숫자 부분을 제거한 새 리스트를 리턴.
아직 완전한 코드는 아님.
"""
pattern = re.compile(r'^(.*?)(\d{4})(\.[^.]+)?$')
original_set = set(filenames)
# 후보 매핑 (원본명 → 제거 후 이름)
candidate_map = {}
for name in filenames:
m = pattern.match(name)
if m:
base, _, ext = m.group(1), m.group(2), m.group(3) or ''
candidate_map[name] = base + ext
# 제거 후 이름별 등장 횟수 집계
result_counts = {}
for name in filenames:
tgt = candidate_map.get(name, name)
result_counts[tgt] = result_counts.get(tgt, 0) + 1
# 최종 리스트 생성
final = []
for name in filenames:
if name in candidate_map:
tgt = candidate_map[name]
if tgt not in original_set and result_counts[tgt] == 1:
final.append(tgt)
else:
final.append(name)
else:
final.append(name)
return final
def plan_and_resolve(rename_pairs: List[tuple]) -> List[str]:
"""
1) strip_trailing_digits 적용
2) 중복 발생 시 initial_candidate로 롤백
아직 완전한 코드는 아님.
"""
initial_targets = [new for _, new in rename_pairs]
stripped = strip_trailing_digits(initial_targets)
final_targets = []
seen = set()
for init, strip in zip(initial_targets, stripped):
if strip not in seen:
seen.add(strip)
final_targets.append(strip)
else:
seen.add(init)
final_targets.append(init)
return final_targets
def apply_renames(folder: str, rename_pairs: List[tuple], final_targets: List[str]) -> None:
"""
두 단계 안전 리네이밍:
1) <old> → <old>.renametmp
2) .renametmp → <new>
아직 완전한 코드는 아님.
"""
tmp_suffix = ".renametmp"
# 1단계: 모두 임시 이름으로 변경
for (old_name, _), _ in zip(rename_pairs, final_targets):
old_path = os.path.join(folder, old_name)
tmp_path = old_path + tmp_suffix
os.rename(old_path, tmp_path)
# 2단계: 임시 이름 --> 최종 이름
for (old_name, _), new_name in zip(rename_pairs, final_targets):
tmp_path = os.path.join(folder, old_name + tmp_suffix)
final_path = os.path.join(folder, new_name)
os.rename(tmp_path, final_path)
class FileRenamerApp:
def __init__(self, root):
self.root = root
self.root.title("SRA v1.0.8 b1")
self.root.geometry("1200x900+15+15")
self.default_folder = r"D:\Downloads_"
self.create_widgets()
def close_window(self):
# 이벤트 루프 종료 이후 윈도우 파괴
self.root.quit()
self.root.destroy()
def create_widgets(self):
# 대상 디렉토리 입력
tk.Label(self.root, text="대상 디렉토리 경로 :").grid(row=0, column=0, columnspan=2, sticky=tk.W, padx=25, pady=(25,5))
folder_frame = tk.Frame(self.root)
folder_frame.grid(row=1, column=0, columnspan=2, padx=(25,0), pady=(5, 125))
self.folder_entry = tk.Entry(folder_frame, width=45)
self.folder_entry.insert(0, self.default_folder)
self.folder_entry.grid(row=0, column=0)
tk.Button(folder_frame, text="☆ 경로 선택", command=self.choose_folder).grid(row=1, column=0, sticky=tk.W, pady=5)
options_frame = tk.Frame(self.root)
options_frame.grid(row=2, column=0, columnspan=2,sticky=tk.W, padx=(25,0), pady=(5, 25))
# 치환 문자
tk.Label(options_frame, text="치환할 문자들 (예: . ,- ,) : ").grid(row=0, column=0)
self.find_entry = tk.Entry(options_frame)
self.find_entry.insert(0, ".,-,',+,@,~,&,[,],{,},(,),`,;,ù, ")
self.find_entry.grid(row=0, column=1)
# 대체 문자
tk.Label(options_frame, text="대체 문자 (예: _) : ").grid(row=1, column=0, sticky=tk.W)
self.replace_entry = tk.Entry(options_frame)
self.replace_entry.insert(0, "_")
self.replace_entry.grid(row=1, column=1)
# 접두사/접미사 옵션
self.add_prefix_var = tk.BooleanVar(value=True) # 선택된 상태로 시작하기
self.add_suffix_var = tk.BooleanVar(value=True) # 선택된 상태로 시작하기
tk.Checkbutton(options_frame, text="파일명 앞에 추가 : ", variable=self.add_prefix_var).grid(row=2, column=0, sticky=tk.W)
self.prefix_entry = tk.Entry(options_frame)
self.prefix_entry.insert(0, "S5_") # PreFix_
self.prefix_entry.grid(row=2, column=1)
tk.Checkbutton(options_frame, text="파일명 뒤에 추가 : ", variable=self.add_suffix_var).grid(row=3, column=0, sticky=tk.W)
self.suffix_entry = tk.Entry(options_frame)
self.suffix_entry.insert(0, "_") # _SufFix_
self.suffix_entry.grid(row=3, column=1)
# 실행 버튼 프레임
run_frame = tk.Frame(self.root)
run_frame.grid(row=3, column=0, columnspan=2, sticky=tk.W, padx=(25, 5), pady=(125, 5))
# 실행 버튼
tk.Button(run_frame, text=">> 파일 이름 변환 실행", command=self.rename_files).grid(row=0, column=0, columnspan=2)
# 결과 표시 프레임
result_frame = tk.Frame(self.root)
result_frame.grid(row=4, column=0, columnspan=2, sticky=tk.W, padx=(25,5), pady=(5,55))
# 결과 표시
self.result_label = tk.Label(result_frame, text="Result : ", fg="green")
self.result_label.grid(row=0, column=0, columnspan=2)
# 닫기 버튼 프레임
exit_frame = tk.Frame(self.root)
exit_frame.grid(row=5, column=0, columnspan=2, sticky=tk.SE, padx=25, pady=25)
# 닫기 버튼
tk.Button(exit_frame, text="닫기", command=self.close_window).grid(row=0, column=0, columnspan=2, sticky=tk.SE)
def choose_folder(self):
sel = filedialog.askdirectory()
if sel:
self.folder_entry.delete(0, tk.END)
self.folder_entry.insert(0, sel)
def generate_clean_unique_filename(self, folder: str, filename: str) -> str:
base, ext = os.path.splitext(filename)
# 연속된 _NNNN 제거
prefix = re.sub(r'(_\d{4})+$', '', base)
match = re.search(r'_(\d{4})$', base)
num = int(match.group(1)) if match else 0
while True:
candidate = f"{prefix}_{num:04}{ext}"
candidate = re.sub(r'_+', '_', candidate).rstrip('_')
if not os.path.exists(os.path.join(folder, candidate)):
return candidate
num += 1
def rename_files(self):
folder = self.folder_entry.get().strip()
if not os.path.isdir(folder):
self.result_label.config(text="경로가 존재하지 않습니다.", fg="red")
return
# 1) 초기 후보 생성
rename_pairs = []
for fname in os.listdir(folder):
src = os.path.join(folder, fname)
if not os.path.isfile(src):
continue
parts = fname.rsplit('.', 1)
if len(parts) != 2:
continue
name, ext = parts
# 문자 치환 & 기본 정리
clean = re.sub(
'[' + re.escape(self.find_entry.get()) + ']',
self.replace_entry.get(), name)
clean = re.sub(r'_+', '_', clean).rstrip('_')
if self.add_prefix_var.get():
clean = self.prefix_entry.get() + clean
if self.add_suffix_var.get():
clean = clean + self.suffix_entry.get()
if not clean.endswith('_'):
clean += '_'
# 예제 하드코딩 블록을 제거하거나 사전 방식으로 개선하는 것이 좋을 듯.
tmp_filename = f"{clean}{'.' + ext}"
candidate = self.generate_clean_unique_filename(folder, tmp_filename)
rename_pairs.append((fname, candidate))
# 2) 충돌 해소
final_targets = plan_and_resolve(rename_pairs)
# 3) 안전 일괄 리네이밍
apply_renames(folder, rename_pairs, final_targets)
self.result_label.config(text=f"{len(rename_pairs)}개 파일 이름 변경 완료!", fg="green")
if __name__ == "__main__":
root = tk.Tk()
app = FileRenamerApp(root)
root.mainloop()
# 생각보다 복잡하네요. 일이 점점 겉잡을 수 없게 커져가는 듯. ^.^;
# 재귀 구조가 유용한가.. 생각 중.. 일단 여기까지. 한동안 업데이트 없을 것.
# 잠시 전체 프로그램 설계 방향을 완전히 새로 다시 잡아야 할 듯.
# 기본 뼈대 설계가 너무 부실, 아무 생각 없이 덤벼든 탓.. ㅠ.ㅠ
# 전부를 한꺼번에 고치는 것은 어려우니
# 2 트랙으로. 이건 이것대로 업데이트.
# 재설계 새 버전은 새 버전대로 업데이트.
# 당분간은.
# 새 버전은 신중하게 당분간은 설계만 제대로.
.
.
5. Files
■ Update 1. (v1.0.1)
■ Update 2. (v1.0.2 beta 3)
■ Update 3. (v1.0.4 beta 1)
■ Update 4, 5. (v1.0.5 beta 1)
■ Update 6. (v1.0.6 b3)
■ Update 7. (v1.0.7 b3)
■ Update 8. (v1.0.8.b1) : 이름 변경, sra --> sr
■ Update 9. (v1.0.9.b3) : 또 다시 수정... ㅠ.ㅠ;;
v1.0.9.b4
v.1.0.9.b4.rev.193
and.. 이전 버전 최소 수정판.
6. References
.
.
Happy Programming!
^.^;
'Programming > Python' 카테고리의 다른 글
| [ L2P ] Turtle 동심원 그리기 - 001 (0) | 2025.07.31 |
|---|---|
| [ L2P ] Turtle Screen, Coloring and Speed : Windmill - 001 (0) | 2025.07.28 |
| Portable Thonny IDE with py5 Download and Py5 연습 - 001 - 3D (0) | 2025.06.22 |
| Tkinter GUI와 함께 하는 파일 처리 연습 - 001 (0) | 2025.05.27 |
| 문자열 함수 - 001 (0) | 2025.05.25 |