mygems

여러 CSV 파일을 하나로 합치는 방법

단순 이어 붙이기는 열을 위치로 맞추기 때문에 헤더 순서가 다르면 결과가 조용히 망가집니다. 열 이름으로 병합하고 검증하는 방법입니다.

작성: uos ·

파일마다 열 구성과 순서가 같다면 가장 빠르고 정확한 병합 방법은 셸 한 줄입니다. 첫 번째 헤더만 남기고 나머지는 버립니다.

head -n 1 first.csv > merged.csv
tail -q -n +2 *.csv >> merged.csv

이 페이지의 나머지는 "열이 같은 순서로 놓여 있다"는 가정이 사람들이 생각하는 것보다 훨씬 자주 틀리고, 그 실패가 조용히 일어난다는 이야기입니다. 두 열이 어긋난 채 병합하면 문제없이 열리고 멀쩡해 보이지만 내용은 틀린 파일이 만들어집니다.

먼저 헤더가 일치하는지 확인하기

병합한 뒤가 아니라 병합하기 전에 하세요. 이후의 모든 문제는 이것의 변형입니다.

for f in *.csv; do echo "$f: $(head -n 1 "$f")"; done

출력에서 네 가지를 확인합니다.

  • 열 순서가 다름. 한 파일은 name,email,date, 다른 파일은 name,date,email인 경우입니다. 그대로 이어 붙이면 날짜가 이메일 열에 들어가지만 아무 경고도 없습니다.
  • 같은 항목인데 이름이 다름. Email, email, E-mail, Email Address. 서로 다른 내보내기에서 나온 파일이나, 연도가 다른 같은 내보내기에서 흔합니다.
  • 열이 더 있거나 없음. 나중 내보내기에서 항목이 늘어난 경우입니다. 행마다 너비가 달라집니다.
  • 첫 파일에 바이트 순서 표시(BOM)가 있음. name은 name이 아닙니다. 어떤 편집기에서도 보이지 않고, 열 이름으로 하는 조인을 깨뜨립니다.

모든 헤더가 바이트 단위로 같다면 위의 두 줄을 쓰고 여기서 읽기를 멈춰도 됩니다. 그렇지 않다면 이어 붙이기는 잘못된 도구이고, 열 이름으로 병합하는 도구가 필요합니다.

열 이름으로 병합하기

DuckDB: 파일이 크거나 헤더가 다른 경우

위치가 아니라 이름으로 맞추기 때문에, 명령 하나로 열 순서가 다른 문제와 열이 빠진 문제를 모두 처리합니다.

duckdb -c "COPY (SELECT * FROM read_csv('*.csv', union_by_name=true, filename=true)) TO 'merged.csv' (HEADER)"

실제로 일하는 것은 union_by_name=true입니다. 열은 헤더로 매칭되고, 열이 없는 파일은 행이 밀리는 대신 그 열이 NULL이 됩니다. filename=true는 각 행이 어느 파일에서 왔는지 기록하는 열을 추가하며, 생각보다 자주 필요하게 됩니다. "이 잘못된 행은 어느 내보내기에서 들어왔나"를 나중에 확인할 유일한 방법이기 때문입니다.

DuckDB는 스트리밍으로 처리하므로 파일 크기의 한계는 RAM이 아니라 디스크입니다. 합계 수 GB인 파일 100개도 특별한 일이 아닙니다.

Python: 먼저 이름을 맞춰야 하는 경우

같은 항목이 한 파일에서는 Email, 다른 파일에서는 E-mail Address라면 어떤 도구도 추측할 수 없습니다. 명시적으로 정규화하세요.

import glob, pandas as pd

rename = {"E-mail Address": "email", "Email": "email"}
frames = []
for path in glob.glob("*.csv"):
    df = pd.read_csv(path, dtype=str)
    df.columns = [c.strip().lstrip("") for c in df.columns]
    frames.append(df.rename(columns=rename).assign(source=path))

pd.concat(frames, ignore_index=True).to_csv("merged.csv", index=False)

병합 자체보다 중요한 세부 사항이 두 가지 있습니다. dtype=str은 pandas가 파일마다 타입을 추론하지 못하게 합니다. 이것이 없으면 한 파일에서는 숫자이고 다른 파일에서는 영숫자인 고객 ID 열이 1024와 "01024"가 섞인 채로 나오고, 앞의 0은 영영 사라집니다. 그리고 .assign(source=path)는 DuckDB가 기본으로 주는 출처 열과 같은 것입니다.

Power Query: 터미널이 없는 데스크톱

Excel의 데이터 → 데이터 가져오기 → 폴더에서는 디렉터리를 지정하면 그 안의 모든 파일을 헤더로 매칭해 합칩니다. 이 작업을 잘하고, 폴더에 새 파일이 들어오면 새로 고칠 수 있으며, 매달 같은 병합을 반복할 사람에게는 정답입니다.

한계는 Excel의 한계입니다. 시트에 올리려면 병합 결과가 1,048,576행 안에 들어와야 합니다. 대신 데이터 모델로 불러오면 시트를 아예 거치지 않을 수 있고, 한도가 상당히 올라갑니다. 대부분은 모르는 방법입니다.

추가(append)와 조인(join)의 차이

"병합"은 서로 다른 두 가지 작업을 뜻하며, 잘못 고르면 오후 시간을 통째로 날립니다.

가진 것원하는 것작업
같은 열의 월별 내보내기 12개행이 더 많은 파일 하나추가(쌓기)
한 파일에 고객, 다른 파일에 주문열이 더 많은 파일 하나조인(키로 매칭)

위에서 다룬 것은 모두 추가입니다. 조인에는 공통 키와, 매칭되지 않는 행을 어떻게 할지에 대한 결정이 필요하며, 중복이 생기는 곳도 여기입니다. 양쪽에서 키가 고유하지 않으면 행이 조용히 불어나기 때문입니다. 조인 후 행 수가 어느 입력보다 많다면 바로 그 일이 일어난 것입니다.

조인에는 SQL이 가장 정직한 도구입니다.

duckdb -c "COPY (SELECT c.*, o.order_total FROM read_csv('customers.csv') c LEFT JOIN read_csv('orders.csv') o USING (customer_id)) TO 'merged.csv' (HEADER)"

휴대폰에서

현실적인 휴대폰 상황은 메일이나 파일 앱에 있는 몇 개의 파일(지역별 내보내기 세 개, 몇 달 치 명세서)을 월요일까지 노트북 없이 처리해야 하는 경우입니다.

여기서는 두 가지가 도움이 됩니다. 헤더가 같은 파일을 추가하는 것은 복사해서 붙여 넣기면 됩니다. 단, 앱이 셀 하나씩이 아니라 행 묶음을 붙여 넣을 수 있어야 합니다. 그리고 중요한 확인 작업인 헤더 비교는 휴대폰에서 가장 빠릅니다. 파일을 하나씩 열어 맨 윗줄을 읽으면 됩니다.

CSV Editor - Smart CSViPhone 및 iPad

각 내보내기를 몇 초 만에 열고, 행 범위를 다른 시트에 붙여 넣으며, 무료 버전에서도 저장할 때 모든 행을 다시 씁니다. 열과 행을 삽입할 수 있어서 항목이 늘어난 파일을 맞추는 일이 우회가 아니라 제대로 된 편집이 됩니다.

기능

파일이 몇 개를 넘으면 데스크톱을 기다리세요. 내보내기 30개를 손으로 병합하는 일은 터치스크린에서 할 일이 아닙니다. 번거로워서가 아니라 오류가 늘어나기 때문입니다.

병합 후 네 가지 확인

매번 실행하세요. 1분이면 되고, 문제가 되는 병합 실패는 거의 다 잡아냅니다.

  1. 행 수. 병합된 총합은 모든 입력의 행 수 합계에서 파일마다 헤더 하나씩을 뺀 값이어야 합니다. 병합 전에 wc -l *.csv, 병합 후에 wc -l merged.csv를 실행합니다.
  2. 헤더 수. grep -c "^name," merged.csv (첫 번째 열에 맞게 바꾸세요). 1보다 많으면 헤더가 데이터 행으로 들어간 것입니다.
  3. 열 너비. 모든 행의 항목 수가 같아야 합니다. 너비가 고르지 않다면 어딘가에 따옴표 없는 쉼표가 있는 것이며, 병합 버그가 아닙니다.
  4. 경계 확인. 병합된 결과에서 첫 번째 파일의 마지막 행과 두 번째 파일의 첫 행을 보세요. 어긋남은 거기서 가장 먼저 드러납니다.

두 번째 확인에는 미묘한 점이 있습니다. 헤더 줄이 데이터 안에 들어가면 정렬할 때 파일 중간으로 흩어져서 나중에 찾기가 매우 어려워집니다. 지금 잡아내세요.

CSV File Viewer - Smart CSViPhone 및 iPad · Android

병합된 파일에 바로 SQL을 실행합니다. source 열로 GROUP BY를 하면 각 파일이 제 몫의 행을 냈는지 알 수 있어서, 검증이 쿼리 하나로 끝납니다.

기능

인코딩: 모든 확인을 통과하는 실패

서로 다른 시스템의 파일은 인코딩이 다른 경우가 많고, 이어 붙여도 변환되지 않습니다. UTF-8 파일을 Windows-1252 파일에 붙이면 둘 다 아닌 파일이 됩니다. 앞부분에서는 이름이 제대로 보이고, 뒷부분에서는 é처럼 보입니다.

병합한 뒤가 아니라 병합하기 전에 변환하세요.

iconv -f WINDOWS-1252 -t UTF-8 old.csv > old-utf8.csv

file -I *.csv로 어떤 인코딩인지 추측할 수는 있지만, 짧은 파일에서는 잘 틀립니다. 확실한 방법은 각 파일을 열어 ASCII가 아닌 문자가 들어 있는 행을 직접 보는 것입니다.

자주 묻는 질문

여러 CSV 파일을 하나로 합치려면 어떻게 하나요?
모든 파일의 헤더가 바이트 단위로 같다면 첫 번째 헤더를 남기고 나머지를 붙입니다. `head -n 1 first.csv > merged.csv`를 실행한 뒤 `tail -q -n +2 *.csv >> merged.csv`를 실행합니다. 헤더의 순서나 내용이 다르면 이어 붙이기는 열을 조용히 어긋나게 하므로, DuckDB의 read_csv에 union_by_name=true를 쓰거나 Excel의 '폴더에서' 데이터 가져오기처럼 열 이름으로 매칭하는 도구를 쓰세요.
병합한 CSV에서 열이 엉뚱한 위치에 들어간 이유는 무엇인가요?
이어 붙이기는 이름이 아니라 위치로 매칭하기 때문입니다. 열은 같지만 순서가 다른 두 파일을 이으면 값이 밀린 파일이 만들어지고, 어떤 오류도 나오지 않습니다. 병합하기 전에 항상 헤더 줄을 비교하세요. `for f in *.csv; do echo "$f: $(head -n 1 "$f")"; done`
열이 서로 다른 CSV 파일은 어떻게 병합하나요?
열 이름으로 매칭하고 빈 곳을 채웁니다. DuckDB는 `union_by_name=true`가 붙은 명령 하나로 처리하며, 파일에 없는 열은 NULL이 됩니다. pandas에서는 `dtype=str`로 읽은 프레임에 `pd.concat`을 쓰면 같은 결과가 나옵니다. 이름만 다른 같은 항목(Email과 E-mail Address 등)은 먼저 명시적으로 이름을 바꿔야 합니다. 이것은 어떤 도구도 추론할 수 없습니다.
병합된 CSV의 각 행이 어느 파일에서 왔는지 어떻게 알 수 있나요?
병합할 때 출처 열을 추가합니다. DuckDB의 read_csv는 filename=true를 주면 자동으로 추가하고, pandas에서는 프레임마다 `.assign(source=path)`를 쓰면 됩니다. 잘못된 행을 원래 내보내기까지 추적할 유일한 방법이며, 검증도 GROUP BY 한 번으로 끝납니다.
CSV 병합이 제대로 되었는지 어떻게 확인하나요?
네 가지를 확인합니다. 병합된 행 수가 입력의 합계에서 파일마다 헤더 하나씩을 뺀 값과 같은지, 헤더 문자열이 정확히 한 번만 나오는지, 모든 행의 항목 수가 같은지, 첫 번째와 두 번째 파일의 경계가 올바르게 보이는지입니다. 일찍 잡아낼 가치가 있는 실패는 헤더가 데이터 행으로 들어간 경우입니다. 정렬하면 파일 중간으로 흩어지기 때문입니다.
병합한 파일의 절반에서 악센트 문자가 깨지는 이유는 무엇인가요?
입력 파일의 인코딩이 서로 달랐고, 이어 붙인다고 변환되지는 않기 때문입니다. Windows-1252 파일을 UTF-8 파일에 붙이면 한쪽 절반은 이름이 올바르고 다른 절반은 문자가 깨집니다. 먼저 모두 UTF-8로 변환하세요. `iconv -f WINDOWS-1252 -t UTF-8 old.csv > old-utf8.csv`를 실행한 뒤 병합합니다.

uos — CSV Editor와 Smart CSV Viewer를 만들고 있으며, 병합 중 열이 한 칸 밀린 채 20만 행이 오류 없이 처리되는 것을 직접 본 적이 있습니다.