파이썬 CSV 두 파일 비교: ID 기준으로 추가, 삭제, 수정 행 구분하기

고유 ID로 각 CSV를 색인하고, ID 집합의 차이와 공통 ID의 비교 필드를 나눠 검사하세요.

적용 환경: Python 3.14 표준 라이브러리. 2026-10-23 확인. Windows Python 3.14.4에서 아래 합성 예제를 임시 작업 폴더로 실행해 assert와 예상 예외 검사를 통과했습니다. 실제 회사 파일·API·GUI는 실행하지 않았습니다.

파일 전체 비교와 업무 항목 비교는 다릅니다

매일 내려받는 자산 목록의 줄 순서만 바뀌었을 때, 파일 해시나 줄 단위 차이는 달라질 수 있습니다. 그러나 같은 자산의 소유 부서와 상태가 같다면 업무 변경은 아닐 수 있습니다. 이 글에서는 파일의 바이트 일치가 아니라 고유 자산 ID를 기준으로 의미 있는 필드가 변했는지 비교합니다. 원본 목록에 없는 ID가 보인다는 사실과 실제 자산을 폐기해야 한다는 판단도 분리합니다.

비교할 필드와 키 규칙을 먼저 고정합니다

합성 CSV의 헤더는 asset_id,owner_team,state이며 순서까지 고정합니다. asset_id는 문자열이라 001의 앞자리 0을 보존합니다. owner_team과 state 두 값의 튜플이 같으면 변경 없음입니다. ID 앞뒤 공백과 중복 ID, 빈 값·누락 열·추가 열은 실패시키며, 부서나 상태의 공백을 임의로 정제하지 않습니다. 시간 같은 변동 필드가 추가되면 비교 대상에 넣을지 먼저 결정하세요.

직접 실행하는 최소 예제

import csv
from io import StringIO

FIELDS = ["asset_id", "owner_team", "state"]

def load_snapshot(text):
    reader = csv.DictReader(StringIO(text, newline=""), strict=True)
    if reader.fieldnames != FIELDS:
        raise ValueError("헤더 불일치")
    indexed = {}
    for row in reader:
        if set(row) != set(FIELDS) or any(v is None or not v.strip() for v in row.values()):
            raise ValueError("누락·추가 열 또는 빈 값")
        key = row["asset_id"]
        if key != key.strip() or key in indexed:
            raise ValueError("ID 공백 또는 중복")
        indexed[key] = (row["owner_team"], row["state"])
    return indexed

def compare_snapshots(before_text, after_text, *, allow_empty=False):
    before, after = load_snapshot(before_text), load_snapshot(after_text)
    if before and not after and not allow_empty:
        raise ValueError("전체 삭제 후보: 빈 입력이 정상인지 확인하세요")
    old, new = set(before), set(after)
    return {
        "added": sorted(new - old),
        "removed": sorted(old - new),
        "changed": [key for key in sorted(old & new) if before[key] != after[key]],
    }

def expect_invalid(before, after):
    try:
        compare_snapshots(before, after)
    except ValueError:
        return
    raise AssertionError("잘못된 입력이 통과했습니다")

header = "asset_id,owner_team,state\n"
before = header + "001,지원,사용\n002,영업,보관\n"
after = header + "003,개발,사용\n001,지원,수리\n"
assert compare_snapshots(before, after) == {
    "added": ["003"], "removed": ["002"], "changed": ["001"]}
reordered = header + "002,영업,보관\n001,지원,사용\n"
assert compare_snapshots(before, reordered) == {"added": [], "removed": [], "changed": []}
expect_invalid(before, header + "001,지원,사용\n001,개발,수리\n")
expect_invalid(before, header + " ,지원,사용\n")
expect_invalid(before, header + "001,지원\n")
expect_invalid(before, header + "001,지원,사용,추가\n")
expect_invalid(before, header)
assert compare_snapshots(before, header, allow_empty=True)["removed"] == ["001", "002"]
assert compare_snapshots(header, header) == {"added": [], "removed": [], "changed": []}
print("추가 ['003'] / 삭제 ['002'] / 수정 ['001']")
print("행 순서 무시·중복 ID·열 오류·빈 입력 보호 검사 통과")

검증한 출력

추가 ['003'] / 삭제 ['002'] / 수정 ['001']
행 순서 무시·중복 ID·열 오류·빈 입력 보호 검사 통과

예상 결과와 집합 연산을 연결합니다

어제에는 001·002, 오늘에는 003·001이 있습니다. 새 ID 집합에서 옛 집합을 빼면 추가 003이고, 반대 차이는 삭제 후보 002입니다. 양쪽에 있는 001은 상태가 사용에서 수리로 바뀌어 수정 대상입니다. 코드는 ID를 정렬해 실행마다 결과 순서가 안정되게 합니다. 정렬 기준은 문자열이며 ID의 숫자 크기 비교를 뜻하지 않습니다.

중복 ID를 마지막 행으로 덮어쓰면 안 되는 이유

같은 ID가 두 번 들어왔는데 단순 사전 대입으로 끝내면 어느 행을 비교했는지 숨겨집니다. 아래 로더는 두 번째 동일 ID에서 중단합니다. 이름이나 행 번호를 키로 쓰면 동명이름·정렬 변경 때문에 오판할 수 있으므로, 생성 시스템이 보장하는 고유 ID를 사용해야 합니다. 이 예제의 ID 변경은 삭제와 추가로 표현되며 동일 자산의 ID 교체를 자동 추론하지 않습니다.

빈 CSV를 전체 삭제로 해석하지 않도록 보호합니다

수집 실패로 헤더만 저장됐는데 어제의 모든 행을 삭제 후보로 내보낼 수 있습니다. 예제는 이전 자료가 있는데 새 자료가 비어 있으면 기본적으로 예외를 냅니다. 정말 전체 비움이 정상이라고 별도 확인한 경우에만 allow_empty=True를 명시합니다. 부분 누락이나 잘못된 날짜 자료까지 잡는 보호는 아니므로 건수 급변과 추출 성공 여부도 호출부에서 확인하세요.

실제 파일과 큰 자료에 적용할 때의 한계

예제는 인코딩 처리가 끝난 작은 CSV 문자열을 메모리에 올립니다. 실제 파일은 인코딩·newline 설정을 확인하고 읽어야 합니다. 두 목록을 모두 사전으로 보관하므로 큰 자료에는 DB 키 기반 비교나 디스크 처리 방식이 필요합니다. 출력은 비교 결과일 뿐 외부 시스템의 삭제·수정·계정 차단을 실행하지 않습니다. 개인정보 대신 합성 ID로 시험하고, 차이 목록도 내부 정보라면 접근 권한과 보관 기간을 정하세요.

자주 묻는 질문

행 순서가 바뀌면 수정으로 나오나요?

아닙니다. 고유 ID로 색인하므로 예제에서는 순서 변경만으로 추가·삭제·수정이 생기지 않습니다.

공백을 제거해서 비교하면 더 편하지 않나요?

업무 규칙이 그렇게 정한 경우에만 별도 정제하세요. 식별자와 필드 값을 임의로 합치면 실제 차이를 숨길 수 있습니다.

삭제 목록을 바로 운영 시스템에 반영해도 되나요?

이 글은 차이 탐지만 다룹니다. 입력 완전성, 업무 날짜와 승인 조건을 확인한 별도 절차 없이 실제 삭제로 연결하지 마세요.

공식 문서와 확인일

다음 공식 문서를 2026-10-23에 직접 열어 예제 동작과 대조했습니다. 입력 자료와 출력은 직접 작성한 합성 예제이며, 위 코드를 실행해 재현한 결과입니다.

관련 가이드와 다음 검사

작성 안내: AI 보조로 작성했으며 합성 자료의 로컬 실행 결과를 검증했습니다. 운영자의 실제 업무 경험이나 사람 전문가 검수를 주장하지 않습니다. 자신의 입력 계약과 환경에 맞는 테스트를 먼저 추가하세요.

댓글

이 블로그의 인기 게시물

API 키 저장 방법: 해싱과 암호화의 차이, Fernet 예제

파이썬 Google Sheets API 연동: 서비스 계정 권한과 읽기·쓰기

Pandas 결측치 처리: isna, fillna, dropna 선택 기준