엑셀·문서 업무

엑셀 중복 행을 찾아 검토용 파일로 분리하기

네 열이 완전히 같은 행을 찾아 원본 행 번호와 함께 새 엑셀 파일로 모읍니다. 첫 행도 포함해 중복 묶음을 확인합니다.

목차 보기

이런 분께엑셀 데이터를 취합한 뒤 중복 입력을 확인하는 사무 담당자

준비사항
  • Python 3.12와 터미널을 준비합니다. python --version으로 버전을 확인합니다.
  • 다운로드한 ZIP을 모두 풀고 example.py가 있는 폴더에서 터미널을 엽니다.
  • 처음에는 동봉된 연습 데이터로 실행한 뒤 결과와 원본을 비교합니다.
  • orders.xlsx의 Orders 시트와 openpyxl을 사용합니다. Microsoft Excel 앱은 코드 실행에 필요하지 않습니다.

01무엇을 중복으로 볼지 정하기

이 예제의 중복 기준은 order_id, customer, item, quantity 네 값이 모두 같은 행입니다. 주문 번호 하나만 같은 행은 중복으로 보지 않습니다. 문자열의 공백이나 대소문자도 자동으로 바꾸지 않습니다.

중복을 삭제하기 전에 같은 내용을 가진 모든 행을 Duplicates 시트에 모읍니다. 처음 등장한 행도 포함합니다. 한 번만 나온 행은 Unique 시트에 남겨 전체 8행을 빠짐없이 검토할 수 있게 합니다.

02입력 파일 구조 살펴보기

orders.xlsx의 시트 이름은 Orders이며 첫 행의 열 이름과 순서가 아래와 같아야 합니다. 열을 추가해도 헤더 오류가 나므로 실무 파일을 바로 덮어씌우기보다 필요한 네 열을 복사한 연습용 파일을 만드세요.

역할예시
order_id문자열 주문 번호O-1001
customer문자열 고객명Alpha
item문자열 품목Sensor
quantity양의 정수 수량2

완전히 빈 행은 건너뜁니다. 일부 값만 비어 있거나 수식이 들어 있으면 중지합니다. 주문 번호·고객명·품목은 비어 있지 않은 문자열이어야 합니다. 숫자형 주문 번호는 자동 변환하지 않으므로 입력 파일에서 텍스트로 준비하세요.

03파일을 준비하고 실행하기

  1. ZIP을 풀고 README.txt에 적힌 입력 파일과 example.py가 같은 위치인지 확인합니다. ZIP 안에서 바로 실행하지 않습니다.
  2. example.py가 있는 폴더에서 아래 명령을 한 줄씩 실행합니다. Windows에서 python 명령이 없고 py가 있으면 각 명령의 python을 py -3.12로 바꿉니다.
  3. 완료 메시지가 나온 뒤 새 outputs 폴더를 엽니다. 기존 outputs가 있으면 먼저 다른 이름으로 보관하고 다시 실행합니다.
bash
python --version
python -m pip install -r requirements.txt
python example.py

설치는 인터넷 연결이 필요합니다. 코드의 BASE는 현재 터미널 위치가 아니라 example.py의 위치를 기준으로 입력과 결과 경로를 정합니다.

04전체 코드와 처리 순서

load_workbook의 읽기 전용 모드로 원본을 열고, Counter로 각 행의 등장 횟수를 셉니다. 원본 엑셀의 행 번호는 enumerate의 start=2로 기억합니다. 결과는 새 Workbook에 저장하므로 원본을 다시 저장하지 않습니다.

example.py
"""Find repeated business rows. Keep the source workbook unchanged."""
from collections import Counter
from pathlib import Path

from openpyxl import Workbook, load_workbook
from openpyxl.styles import Font, PatternFill

BASE = Path(__file__).resolve().parent
HEADERS = ("order_id", "customer", "item", "quantity")


def main():
    destination = BASE / "outputs"
    if destination.exists():
        raise ValueError("outputs already exists; rename it before running again.")
    source = load_workbook(BASE / "orders.xlsx", read_only=True, data_only=False)
    try:
        if "Orders" not in source.sheetnames:
            raise ValueError("Orders sheet is missing.")
        worksheet = source["Orders"]
        rows = list(worksheet.iter_rows(values_only=True))
        if not rows or tuple(rows[0]) != HEADERS:
            raise ValueError(f"Expected headers: {HEADERS}")
        records = []
        for row_number, row in enumerate(rows[1:], start=2):
            if all(value is None for value in row):
                continue
            if any(value is None for value in row):
                raise ValueError(f"Row {row_number}: missing value.")
            if any(isinstance(value, str) and value.startswith("=") for value in row):
                raise ValueError(f"Row {row_number}: formulas are not supported.")
            if not all(isinstance(value, str) and value.strip() for value in row[:3]):
                raise ValueError(f"Row {row_number}: first three fields must be text.")
            if type(row[3]) is not int or row[3] <= 0:
                raise ValueError(f"Row {row_number}: quantity must be a positive integer.")
            records.append((row_number, tuple(row)))
        if not records:
            raise ValueError("No data rows were found.")
    finally:
        source.close()

    counts = Counter(row for _, row in records)
    repeated = [(number, row) for number, row in records if counts[row] > 1]
    unique = [(number, row) for number, row in records if counts[row] == 1]
    book = Workbook()
    book.remove(book.active)
    for name, selected in (("Duplicates", repeated), ("Unique", unique)):
        sheet = book.create_sheet(name)
        sheet.append(("source_row", *HEADERS, "occurrences"))
        for number, row in selected:
            sheet.append((number, *row, counts[row]))
        sheet.freeze_panes = "A2"
        sheet.auto_filter.ref = sheet.dimensions
        sheet.sheet_view.showGridLines = False
        for cell in sheet[1]:
            cell.font = Font(name="Arial", bold=True, color="FFFFFF")
            cell.fill = PatternFill("solid", fgColor="243B53")
        for column, width in zip("ABCDEF", (14, 16, 18, 20, 14, 16)):
            sheet.column_dimensions[column].width = width
    destination.mkdir(exist_ok=False)
    book.save(destination / "duplicate_review.xlsx")
    book.close()
    groups = sum(count > 1 for count in counts.values())
    print(f"Input rows: {len(records)}; duplicate groups: {groups}")
    print(f"Duplicate rows: {len(repeated)}; unique rows: {len(unique)}")
    print("Created outputs/duplicate_review.xlsx (source unchanged).")


if __name__ == "__main__":
    try:
        main()
    except (OSError, ValueError) as error:
        raise SystemExit(f"Stopped: {error}") from error

05실행하면 만들어지는 결과

outputs/duplicate_review.xlsx에 Duplicates와 Unique 두 시트가 생깁니다. source_row는 원본의 엑셀 행 번호이며, occurrences는 같은 내용이 전체 입력에서 나타난 횟수입니다.

항목동봉 데이터 결과
입력 데이터 행8
중복 묶음2
Duplicates 행4 (원본 2, 3, 5, 8행)
Unique 행4 (원본 4, 6, 7, 9행)

Duplicates의 4행은 제거할 행 4개라는 뜻이 아닙니다. 각 묶음에서 하나씩만 유지한다면 추가로 입력된 행은 2개지만, 무엇을 유지할지는 원본을 확인한 뒤 정해야 합니다.

06결과를 직접 확인하기

  1. Duplicates에서 source_row 2와 5가 O-1001이고 occurrences가 모두 2인지 확인합니다.
  2. source_row 3과 8이 O-1002인지 확인합니다.
  3. Duplicates 4행과 Unique 4행을 더해 입력 8행과 맞는지 확인합니다.
  4. orders.xlsx를 다시 열어 8개 데이터 행이 그대로 있는지 확인합니다.

07흔한 오류와 해결 방법

메시지 또는 증상확인할 것
Orders sheet is missing원본 시트 이름을 Orders로 맞춥니다.
Expected headers열 이름·순서·추가 열을 확인합니다.
missing value / formulas are not supported표시된 원본 행을 확인하고 값으로 준비합니다.
quantity must be a positive integer수량은 1 이상 정수로 입력합니다.
outputs already exists이전 결과 폴더를 다른 이름으로 보관합니다.
ModuleNotFoundError실행에 쓰는 python으로 requirements.txt를 설치합니다.

08이 예제에서 다루지 않는 것

  • 서식, 차트, 매크로, 연결, 숨김 행 상태를 복제하지 않습니다. 결과는 검토에 필요한 값과 기본 서식만 담습니다.
  • 공백 제거·대소문자 통일·유사 문자열 판정은 하지 않습니다. 중복 정의가 바뀌면 비교 기준을 별도로 설계해야 합니다.
  • 파일 전체를 메모리에 모으므로 대용량 처리 성능을 보장하지 않습니다. 암호화되거나 손상된 파일은 지원하지 않습니다.
  • 결과 파일에서 값을 수정해도 원본 또는 결과가 자동 재계산되지 않습니다. 새 결과가 필요하면 원본 복사본을 고친 뒤 다시 실행합니다.

실행·검증 기록

Windows 11 (10.0.26200), CPython 3.12.14 (64비트). openpyxl 3.1.5, Matplotlib 3.10.8. 예제별 사용 라이브러리는 requirements.txt에 고정했습니다.

  • 정상 데이터의 행 수·원본 행 번호·중복 횟수 검증
  • 원본 파일 SHA-256 불변 및 재실행 덮어쓰기 거부
  • 헤더 불일치·누락값·수식·잘못된 수량 오류 검사
검증 범위의 한계
  • Excel 데스크톱 앱에서의 표시와 대용량 성능은 검증하지 않았습니다.

사이트 전체의 작성·검증 원칙

직접 실행할 예제 파일

코드·입력 데이터·실행 안내가 포함되어 있습니다. 압축을 풀고 README.txt부터 읽어 주세요.

예제 ZIP 다운로드

직접 작성한 연습 자료 · 원본을 따로 보관한 뒤 실행하세요.

참고 출처

설명과 예제는 직접 작성했습니다. 관련 동작과 개념은 아래 공식 자료에서 확인할 수 있습니다.