본문 바로가기
STUDY

자모음이 분리된 한글 파일 디렉토리 이름을 파이썬으로 일괄 변경하기

by PsychoFLOOD 2023. 2. 13.
728x90

업무상 필요한 파일을 다운로드 받았는데 한글파일의 이름이 초성/중성/종성이 모두 분리되어 있는 형태로 보이는 일이 종종 있다.

파이썬코드로 NFC 형식의 스트링을 NFD로 바꾸어본 모습.

 

보통 이런식으로 초/중/종 성이 분리되어서 윈도우에서 보여진다.

 

 

이는 시스템의 유니코드 정규화(Unicode Normalize Form) 방식이 달라서 발생하는 것으로 대표적으로 MacOS는 NFD(Normalization Form Canonical Decomposition)를 사용하고 Windows는 NFC(Normalization Form Canonical Composition)를 사용한다.

보여지는 문자는 같이 보여도 시스템 내부적으로는 표현하는 방법이 서로 다르다 하겠다. 이를 유니코드 등가성이라고 부르며 아래 위키 페이지를 참조해보면 잘 설명되어 있다.

https://ko.wikipedia.org/wiki/%EC%9C%A0%EB%8B%88%EC%BD%94%EB%93%9C_%EB%93%B1%EA%B0%80%EC%84%B1

 

유니코드 등가성 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 유니코드 등가성(Unicode equivalence)은 특정한 일련의 코드포인트들이 반드시 동일 문자를 대표해야 하는 유니코드 문자 인코딩 표준의 사양이다. 이 기능은 비슷

ko.wikipedia.org

 

7글자 한글을 utf-8 format으로 변환후 NFC/NFD 스트링 각각의 길이를 보면 2배 이상 차이가 나는 것을 볼 수 있다.

 

 

이런식으로 표현된 파일이름이 한두개이면 괜찮은데 몇십 몇백개일 경우 참 난감한 경우가 가끔 있다.

이를 해결하기 위해 파이썬에서 특정 폴더를 선택시 하위 폴더를 돌면서 NFD 형식으로 된 파일/디렉토리 이름을 모두 일괄적으로 변경하여 주는 간단한 코드를 작성해보았다.

 

UI는 파이썬 기본 UI인 tkinter를 이용하여 작업해보았다.

우선 filedialog의 askdirectory를 이용해서 특정 폴더의 경로를 받아온 다음 해당 폴더 하위의 모든 파일 개수를 받아온다.

이후 다시한번 하위폴더를 recurcive call을 이용해서 돌면서 파일/디렉토리 이름을 받아와서 NFC 형식으로 변경한후 원래 스트링과 다르다면 rename을 수행하도록 하였다.

또한 전체개수 및 현재 처리중인 개수를 확인하여 progress bar를 그려주도록 하였다.

import sys
import tkinter as tk
import tkinter.ttk as ttk
import tkinter.filedialog as fd
from tkinter import *
from tkinter import messagebox
from unicodedata import normalize
import os

recursive_level = 0
all_file_count = 0
current_count =0
offset=16

root = tk.Tk()
root.geometry('450x210')
root.title('All Files/Dirs name normailize from nfd to nfc')
root.resizable(False, False)

frame = tk.Frame(root)
frame.pack()

txt = Label(root, pady=offset, text="Path : Please set Path to click SetPath button")
txt.pack()

def getDir():
    root.dirName = fd.askdirectory()
    getAllFilesCount(root.dirName)
    txt.configure(text="Path : "+root.dirName)
    btn2.configure(state='normal')

def getAllFilesCount(dirname):
    global all_file_count
    filenames = os.listdir(dirname)
    for filename in filenames:
        tempname = os.path.join(dirname, filename)
        all_file_count+=1
        
        if os.path.isdir(tempname):
            getAllFilesCount(tempname)
    

btn = Button(root, width=100, pady=offset, text="SetPath", command=getDir)
btn2 = Button(root, width=100, pady=offset, text="Start Rename!", command=lambda: change_nfd_to_nfc_all_file(root.dirName))
btn.pack()
btn2.pack()
btn2.configure(state='disabled')
pb_var = DoubleVar()
pb = ttk.Progressbar(root, maximum=100, length=450, variable=pb_var, mode="determinate")
pb.pack(ipady=20)

def change_nfd_to_nfc_all_file(dirname):
    global recursive_level
    global all_file_count
    global pb
    global current_count
    
    recursive_level += 1
    filenames = os.listdir(dirname)
    for filename in filenames:
        current_count+=1
        pb_var.set(current_count/all_file_count * 100)
        pb.update()
        before_filename = os.path.join(dirname, filename)
        after_filename = normalize('NFC', before_filename)
        if before_filename != after_filename:
            print("Changing file/dir name.. Before : "+before_filename+" After : "+after_filename)
            os.rename(before_filename, after_filename)
        
        if os.path.isdir(before_filename):    
            change_nfd_to_nfc_all_file(before_filename)
    recursive_level -= 1
    if recursive_level==0:
        messagebox.showinfo("Info", "Processing Complete!")

root.mainloop()

 

위의 소스를 이용해서 실제로 변환을 시도하다가 사소한 문제가 하나 발생하였다. 빈 디렉토리의 이름이 NFD형식인경우 rename이 제대로 되지 않고 새로운? 폴더가 생성되는 문제이다.

이를 해결하기 위해서 빈 폴더인경우는 NFC 형식으로 새로운 폴더가 생기고 난뒤 원래의 빈 폴더는 삭제하도록 코드를 수정해주었다. 또한 rename시 발생하는 exception으로 인해 수행이 멈추지 않도록 try exception 구문을 이용해서 발생한 예외를 출력하고 계속 수행하도록 수정하였다.

import sys
import tkinter as tk
import tkinter.ttk as ttk
import tkinter.filedialog as fd
from tkinter import *
from tkinter import messagebox
from unicodedata import normalize
import os
import traceback

recursive_level = 0
all_file_count = 0
current_count =0
offset=16

root = tk.Tk()
root.geometry('450x210')
root.title('All Files/Dirs name normailize from nfd to nfc')
root.resizable(False, False)

frame = tk.Frame(root)
frame.pack()

txt = Label(root, pady=offset, text="Path : Please set Path to click SetPath button")
txt.pack()

def getDir():
    root.dirName = fd.askdirectory()
    root.dirName.replace("/", "\\")
    print("GetDirPath : "  + root.dirName)
    getAllFilesCount(root.dirName)
    txt.configure(text="Path : "+root.dirName)
    btn2.configure(state='normal')

def getAllFilesCount(dirname):
    global all_file_count
    filenames = os.listdir(dirname)
    for filename in filenames:
        tempname = os.path.join(dirname, filename)
        all_file_count+=1
        
        if os.path.isdir(tempname):
            getAllFilesCount(tempname)
    

btn = Button(root, width=100, pady=offset, text="SetPath", command=getDir)
btn2 = Button(root, width=100, pady=offset, text="Start Rename!", command=lambda: change_nfd_to_nfc_all_file(root.dirName))
btn.pack()
btn2.pack()
btn2.configure(state='disabled')
pb_var = DoubleVar()
pb = ttk.Progressbar(root, maximum=100, length=450, variable=pb_var, mode="determinate")
pb.pack(ipady=20)

def change_nfd_to_nfc_all_file(dirname):
    global recursive_level
    global all_file_count
    global pb
    global current_count
    
    recursive_level += 1
    filenames = os.listdir(dirname)
    for filename in filenames:
        current_count+=1
        pb_var.set(current_count/all_file_count * 100)
        pb.update()
        before_filename = os.path.join(dirname, filename)
        after_filename = normalize('NFC', before_filename)
        if before_filename != after_filename:
            print(str(current_count) +"/"+str(all_file_count)+" Changing file/dir name.. Before : "+before_filename+"\nAfter : "+after_filename + "\n")
            try:
                if os.path.isdir(before_filename) and os.path.isdir(after_filename):
                    os.rmdir(before_filename)
                else:
                    os.rename(before_filename, after_filename)
            except Exception:
                traceback.print_exc()
        
        if os.path.isdir(before_filename):    
            change_nfd_to_nfc_all_file(before_filename)
    recursive_level -= 1
    if recursive_level==0:
        messagebox.showinfo("Info", "Processing Complete!")

root.mainloop()

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

728x90

댓글