[무료 도구] 대량문자 엑셀 분할기 — 5천건 자동 분류 Python 프로그램

대량 문자 발송 서비스는 보통 1회 발송 한도가 5,000건으로 제한되어 있습니다. 수만 건의 수신자 명단이 담긴 엑셀 파일이 있을 때, 이를 수작업으로 나누는 건 매우 번거롭고 실수도 생깁니다. 저도 실무에서 이 문제를 반복해서 겪다 보니, 아예 자동으로 처리해주는 도구를 만들게 되었습니다.

✅ 이 도구가 하는 일

  • 🔓 암호화된 엑셀 파일 자동 복호화 — 비밀번호가 걸린 파일도 입력 한 번으로 해제
  • 🧹 전화번호 중복 제거 — 번호 기준으로만 판단, 이름 중복은 무시
  • 빈 번호 / 잘못된 항목 제거00_제거목록.xlsx로 별도 저장
  • ✂️ N건씩 자동 분할 저장 — 기본값 5,000건, 자유롭게 변경 가능
  • 📁 결과 폴더 자동 생성 및 오픈 — 실행 시각이 붙은 고유 폴더에 저장

🛠 필요 환경 (Requirements)

Python 3.8 이상이 필요합니다. 아직 설치되지 않았다면 python.org에서 받으세요.

Python 설치 후, 아래 명령어를 명령 프롬프트(cmd)에서 실행해 필요한 패키지를 설치합니다:

Python
pip install pandas openpyxl xlrd msoffcrypto-tool
패키지역할
pandas엑셀 데이터 읽기 및 처리
openpyxl.xlsx 파일 읽기/쓰기
xlrd구형 .xls 파일 읽기
msoffcrypto-tool암호화된 엑셀 파일 복호화

▶ 실행 방법 — .pyw 파일로 저장

아래 전체 코드를 복사한 뒤, excel_splitter.pyw 라는 이름으로 저장합니다.

📌 .pyw 확장자란?
일반 .py 파일을 실행하면 검은 콘솔 창이 함께 뜹니다. .pyw로 저장하면 콘솔 창 없이 GUI 창만 깔끔하게 실행됩니다. 저장 후 파일을 더블클릭하면 바로 실행됩니다.

📋 사용 방법 (단계별)

  1. excel_splitter.pyw 더블클릭으로 실행
  2. 엑셀 파일 선택 클릭 → 명단 파일 선택
    ※ 파일에 반드시 이름, 전화번호 컬럼이 있어야 합니다
  3. 저장 위치 확인 (자동으로 원본 파일 위치로 설정됨)
  4. 기존 비밀번호 입력 (암호 없는 파일이면 빈칸으로 두세요)
  5. 분할 수량 설정 (기본값 5000)
  6. Run 버튼 클릭
  7. 처리가 완료되면 결과 폴더가 자동으로 열립니다

💡 코드 구조 설명

1. 라이브러리 임포트

Python
import tkinter as tk
from tkinter import filedialog, scrolledtext, messagebox
import pandas as pd
import os
import re
import threading
import queue
import tempfile
import shutil
from datetime import datetime

try:
    import msoffcrypto
except ImportError:
    messagebox.showerror("Error", "msoffcrypto-tool이 설치되지 않았습니다.")
    exit()

tkinter는 Python에 기본 내장된 GUI 라이브러리입니다. 별도 설치 없이 바로 사용할 수 있습니다. threading을 사용해 파일 처리 중에도 UI가 멈추지 않도록 합니다. msoffcrypto가 설치되지 않으면 시작 시 바로 안내 메시지를 띄우고 종료합니다.

2. 파일 복호화 함수 — prepare_plain_file()

Python
def prepare_plain_file(src_path, old_pw, output_folder, log_queue):
    base = os.path.basename(src_path)
    dst = os.path.join(output_folder, "temp_" + base)
    try:
        with open(src_path, "rb") as fsrc:
            of = msoffcrypto.OfficeFile(fsrc)
            if of.is_encrypted():
                log_queue.put(f"암호 해제 중: {base}")
                of.load_key(password=old_pw)
                with open(dst, "wb") as fdst:
                    of.decrypt(fdst)
            else:
                shutil.copy2(src_path, dst)
        return dst
    except Exception as e:
        log_queue.put(f"파일 준비 실패 ({base}): {e}")
        return None

is_encrypted()로 암호화 여부를 자동 감지합니다. 암호화된 파일이면 비밀번호로 해제 후 임시 폴더에 복사본을 저장합니다. 암호가 없는 파일은 그냥 복사합니다. 원본 파일은 절대 건드리지 않습니다.

3. 핵심 처리 함수 — process_data()

이 함수가 프로그램의 핵심입니다. 내부를 단계별로 살펴봅니다.

① 결과 폴더 자동 생성

Python
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
final_output_dir = os.path.join(output_folder_path, f"분류결과_{timestamp}")
os.makedirs(final_output_dir, exist_ok=True)

실행할 때마다 분류결과_20260303_143022 형태의 고유 폴더가 만들어집니다. 이전 결과를 덮어쓰지 않아 안전합니다.

② 엑셀 읽기 (xls / xlsx 자동 분기)

Python
ext = os.path.splitext(prepared_path)[1].lower()
engine = "xlrd" if ext == ".xls" else "openpyxl"
df = pd.read_excel(prepared_path, engine=engine, dtype=str)
df.columns = [str(c).strip() for c in df.columns]

확장자를 보고 구형 .xls와 최신 .xlsx를 자동으로 구분합니다. dtype=str로 읽어 01012345678 앞자리 0이 사라지지 않습니다. 컬럼명 앞뒤 공백도 자동으로 제거합니다.

③ 전화번호 정제

Python
df['정제번호'] = df['전화번호'].apply(
    lambda x: re.sub(r'[^0-9]', '', str(x).split('.')[0]) if pd.notna(x) else ""
)

010-1234-5678, 010.1234.5678 등 다양한 형식을 숫자만 남겨 통일합니다. pandas가 숫자를 1012345678.0으로 읽는 경우 .split('.')[0]으로 소수점을 제거합니다.

④ 중복 제거 및 제거목록 저장

Python
invalid_df = df[df['정제번호'] == ""].copy()
valid_temp = df[df['정제번호'] != ""].copy()
duplicate_df = valid_temp[valid_temp.duplicated(subset=['정제번호'], keep='first')]

removed_df = pd.concat([invalid_df, duplicate_df])
if not removed_df.empty:
    removed_df.to_excel(os.path.join(final_output_dir, "00_제거목록.xlsx"), index=False)

번호가 없는 행과 중복 번호 행을 모아 00_제거목록.xlsx로 따로 저장합니다. 중복 판단은 전화번호 기준으로만 합니다 (이름이 달라도 번호가 같으면 제거). 첫 번째 등장 항목(keep='first')은 유지합니다.

⑤ N건씩 분할 저장

Python
chunk_size = int(split_n)  # 기본값 5000
df_list = [final_df.iloc[i:i + chunk_size] for i in range(0, len(final_df), chunk_size)]

for idx, chunk in enumerate(df_list):
    out_name = f"분류파일_{idx + 1}번_{len(chunk)}명.xlsx"
    chunk.to_excel(os.path.join(final_output_dir, out_name), index=False)

분류파일_1번_5000명.xlsx, 분류파일_2번_3241명.xlsx 형태로 저장됩니다. 파일명에 실제 건수가 표시되어 몇 건인지 바로 확인 가능합니다.

4. GUI 클래스 — App

Python
class App:
    def __init__(self, root):
        self.root = root
        self.root.title("Excel Splitter Tool")
        self.root.geometry("650x600")
        # 파일 선택, 저장 위치, 비밀번호, 분할 수량, Run 버튼, 로그창 구성
        self.log_queue = queue.Queue()

tkinter의 Frame, Label, Entry, Button, ScrolledText로 UI를 구성합니다. threading.Thread로 처리 로직을 별도 스레드에서 실행해, Run 중에도 UI가 반응합니다. queue.Queue로 처리 스레드와 UI 스레드 간 로그 메시지를 안전하게 주고받습니다.

5. 메인 실행부

Python
if __name__ == "__main__":
    root = tk.Tk()
    app = App(root)
    root.mainloop()

.pyw로 저장하면 콘솔 없이 바로 실행됩니다. root.mainloop()가 창이 닫힐 때까지 프로그램을 유지합니다.

📄 전체 코드 (복사용)

아래 코드를 전체 복사해 excel_splitter.pyw로 저장하세요.

Python
import tkinter as tk
from tkinter import filedialog, scrolledtext, messagebox
import pandas as pd
import os
import re
import threading
import queue
import tempfile
import shutil
from datetime import datetime

try:
    import msoffcrypto
except ImportError:
    messagebox.showerror("Error", "msoffcrypto-tool이 설치되지 않았습니다.
'pip install msoffcrypto-tool'을 실행해주세요.")
    exit()

def prepare_plain_file(src_path, old_pw, output_folder, log_queue):
    base = os.path.basename(src_path)
    dst = os.path.join(output_folder, "temp_" + base)
    try:
        with open(src_path, "rb") as fsrc:
            of = msoffcrypto.OfficeFile(fsrc)
            if of.is_encrypted():
                log_queue.put(f"암호 해제 중: {base}")
                of.load_key(password=old_pw)
                with open(dst, "wb") as fdst:
                    of.decrypt(fdst)
            else:
                shutil.copy2(src_path, dst)
        return dst
    except Exception as e:
        log_queue.put(f"파일 준비 실패 ({base}): {e}")
        return None

def process_data(file_path, output_folder_path, old_pw, split_n, log_queue):
    try:
        log_queue.put("작업 시작...")
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        final_output_dir = os.path.join(output_folder_path, f"분류결과_{timestamp}")
        os.makedirs(final_output_dir, exist_ok=True)
        log_queue.put(f"생성된 결과 폴더: {final_output_dir}")

        temp_dir = tempfile.mkdtemp()
        prepared_path = prepare_plain_file(file_path, old_pw, temp_dir, log_queue)
        if not prepared_path:
            return

        log_queue.put(f"데이터 읽는 중: {os.path.basename(file_path)}")
        ext = os.path.splitext(prepared_path)[1].lower()
        engine = "xlrd" if ext == ".xls" else "openpyxl"
        df = pd.read_excel(prepared_path, engine=engine, dtype=str)
        df.columns = [str(c).strip() for c in df.columns]

        if '이름' not in df.columns or '전화번호' not in df.columns:
            log_queue.put("오류: 엑셀에 '이름' 또는 '전화번호' 컬럼이 없습니다.")
            return

        log_queue.put("데이터 정제 및 중복 제거 중...")
        df['정제번호'] = df['전화번호'].apply(
            lambda x: re.sub(r'[^0-9]', '', str(x).split('.')[0]) if pd.notna(x) else ""
        )

        invalid_df = df[df['정제번호'] == ""].copy()
        valid_temp = df[df['정제번호'] != ""].copy()
        duplicate_df = valid_temp[valid_temp.duplicated(subset=['정제번호'], keep='first')]

        removed_df = pd.concat([invalid_df, duplicate_df])
        if not removed_df.empty:
            removed_df.to_excel(os.path.join(final_output_dir, "00_제거목록.xlsx"), index=False)
            log_queue.put(f"제외 명단 저장 완료: {len(removed_df)}건")

        final_df = valid_temp.drop_duplicates(subset=['정제번호'], keep='first')
        final_df = final_df[['이름', '정제번호']].rename(columns={'정제번호': '전화번호'})

        try:
            chunk_size = int(split_n)
        except:
            chunk_size = 5000

        df_list = [final_df.iloc[i:i + chunk_size] for i in range(0, len(final_df), chunk_size)]

        for idx, chunk in enumerate(df_list):
            out_name = f"분류파일_{idx + 1}번_{len(chunk)}명.xlsx"
            chunk.to_excel(os.path.join(final_output_dir, out_name), index=False)
            log_queue.put(f"저장 완료: {out_name}")

        log_queue.put(f"모든 작업 완료! (총 {len(final_df)}명 분할)")
        messagebox.showinfo("완료", f"정제 및 분할 작업이 완료되었습니다.
경로: {final_output_dir}")
        os.startfile(final_output_dir)
        shutil.rmtree(temp_dir)

    except Exception as e:
        log_queue.put(f"오류 발생: {e}")
    finally:
        log_queue.put("__PROCESS_FINISHED__")

class App:
    def __init__(self, root):
        self.root = root
        self.root.title("Excel Splitter Tool")
        self.root.geometry("650x600")

        file_frame = tk.Frame(root, padx=10, pady=10)
        file_frame.pack(fill=tk.X)
        tk.Label(file_frame, text="엑셀 파일 선택").grid(row=0, column=0, sticky="w")
        self.file_path = tk.StringVar()
        tk.Entry(file_frame, textvariable=self.file_path, width=60).grid(row=0, column=1, padx=5)
        tk.Button(file_frame, text="파일 찾기", command=self.browse_file).grid(row=0, column=2)

        out_frame = tk.Frame(root, padx=10, pady=5)
        out_frame.pack(fill=tk.X)
        tk.Label(out_frame, text="저장 위치").grid(row=0, column=0, sticky="w")
        self.output_path = tk.StringVar()
        tk.Entry(out_frame, textvariable=self.output_path, width=60).grid(row=0, column=1, padx=5)
        tk.Button(out_frame, text="폴더 선택", command=self.browse_output).grid(row=0, column=2)

        opt_frame = tk.Frame(root, padx=10, pady=10, relief=tk.GROOVE, borderwidth=1)
        opt_frame.pack(fill=tk.X, padx=10, pady=5)
        tk.Label(opt_frame, text="기존 비밀번호:").grid(row=0, column=0, sticky="w")
        self.old_pw = tk.StringVar()
        tk.Entry(opt_frame, textvariable=self.old_pw, show="*", width=20).grid(row=0, column=1, sticky="w", padx=5)
        tk.Label(opt_frame, text="분할 수량(N):").grid(row=0, column=2, padx=(20, 5))
        self.split_n = tk.StringVar(value="5000")
        tk.Entry(opt_frame, textvariable=self.split_n, width=10).grid(row=0, column=3, sticky="w")

        self.run_button = tk.Button(root, text="Run", command=self.start_thread,
                                    font=("Helvetica", 12, "bold"), bg="lightblue", height=2, width=20)
        self.run_button.pack(pady=20)

        log_frame = tk.Frame(root, padx=10, pady=10)
        log_frame.pack(fill=tk.BOTH, expand=True)
        self.log_area = scrolledtext.ScrolledText(log_frame, wrap=tk.WORD, height=15)
        self.log_area.pack(fill=tk.BOTH, expand=True)
        self.log_area.configure(state='disabled')
        self.log_queue = queue.Queue()

    def browse_file(self):
        f = filedialog.askopenfilename(filetypes=[("Excel files", "*.xlsx *.xls")])
        if f:
            self.file_path.set(f)
            self.output_path.set(os.path.dirname(f))

    def browse_output(self):
        d = filedialog.askdirectory()
        if d: self.output_path.set(d)

    def start_thread(self):
        if not self.file_path.get() or not self.output_path.get():
            messagebox.showwarning("경고", "파일과 저장 위치를 모두 선택해주세요.")
            return
        self.log_area.configure(state='normal')
        self.log_area.delete(1.0, tk.END)
        self.log_area.configure(state='disabled')
        self.run_button.config(state="disabled", text="처리 중...")
        threading.Thread(target=process_data, args=(
            self.file_path.get(), self.output_path.get(), self.old_pw.get(),
            self.split_n.get(), self.log_queue), daemon=True).start()
        self.root.after(100, self.process_log_queue)

    def process_log_queue(self):
        try:
            while True:
                msg = self.log_queue.get_nowait()
                if msg == "__PROCESS_FINISHED__":
                    self.run_button.config(state="normal", text="Run")
                    return
                self.log_message(msg)
        except queue.Empty:
            pass
        self.root.after(100, self.process_log_queue)

    def log_message(self, message):
        self.log_area.configure(state='normal')
        self.log_area.insert(tk.END, message + "\n")
        self.log_area.see(tk.END)
        self.log_area.configure(state='disabled')

if __name__ == "__main__":
    root = tk.Tk()
    app = App(root)
    root.mainloop()

📌 주의사항: 이 도구는 Windows 환경에서 테스트되었습니다. macOS/Linux에서는 os.startfile() 부분이 동작하지 않을 수 있습니다.