파일마다 열 구성과 순서가 같다면 가장 빠르고 정확한 병합 방법은 셸 한 줄입니다. 첫 번째 헤더만 남기고 나머지는 버립니다.
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)"
휴대폰에서
현실적인 휴대폰 상황은 메일이나 파일 앱에 있는 몇 개의 파일(지역별 내보내기 세 개, 몇 달 치 명세서)을 월요일까지 노트북 없이 처리해야 하는 경우입니다.
여기서는 두 가지가 도움이 됩니다. 헤더가 같은 파일을 추가하는 것은 복사해서 붙여 넣기면 됩니다. 단, 앱이 셀 하나씩이 아니라 행 묶음을 붙여 넣을 수 있어야 합니다. 그리고 중요한 확인 작업인 헤더 비교는 휴대폰에서 가장 빠릅니다. 파일을 하나씩 열어 맨 윗줄을 읽으면 됩니다.
각 내보내기를 몇 초 만에 열고, 행 범위를 다른 시트에 붙여 넣으며, 무료 버전에서도 저장할 때 모든 행을 다시 씁니다. 열과 행을 삽입할 수 있어서 항목이 늘어난 파일을 맞추는 일이 우회가 아니라 제대로 된 편집이 됩니다.
기능파일이 몇 개를 넘으면 데스크톱을 기다리세요. 내보내기 30개를 손으로 병합하는 일은 터치스크린에서 할 일이 아닙니다. 번거로워서가 아니라 오류가 늘어나기 때문입니다.
병합 후 네 가지 확인
매번 실행하세요. 1분이면 되고, 문제가 되는 병합 실패는 거의 다 잡아냅니다.
- 행 수. 병합된 총합은 모든 입력의 행 수 합계에서 파일마다 헤더 하나씩을 뺀 값이어야 합니다. 병합 전에
wc -l *.csv, 병합 후에wc -l merged.csv를 실행합니다. - 헤더 수.
grep -c "^name," merged.csv(첫 번째 열에 맞게 바꾸세요). 1보다 많으면 헤더가 데이터 행으로 들어간 것입니다. - 열 너비. 모든 행의 항목 수가 같아야 합니다. 너비가 고르지 않다면 어딘가에 따옴표 없는 쉼표가 있는 것이며, 병합 버그가 아닙니다.
- 경계 확인. 병합된 결과에서 첫 번째 파일의 마지막 행과 두 번째 파일의 첫 행을 보세요. 어긋남은 거기서 가장 먼저 드러납니다.
두 번째 확인에는 미묘한 점이 있습니다. 헤더 줄이 데이터 안에 들어가면 정렬할 때 파일 중간으로 흩어져서 나중에 찾기가 매우 어려워집니다. 지금 잡아내세요.
병합된 파일에 바로 SQL을 실행합니다. source 열로 GROUP BY를 하면 각 파일이 제 몫의 행을 냈는지 알 수 있어서, 검증이 쿼리 하나로 끝납니다.
기능인코딩: 모든 확인을 통과하는 실패
서로 다른 시스템의 파일은 인코딩이 다른 경우가 많고, 이어 붙여도 변환되지 않습니다. UTF-8 파일을 Windows-1252 파일에 붙이면 둘 다 아닌 파일이 됩니다. 앞부분에서는 이름이 제대로 보이고, 뒷부분에서는 é처럼 보입니다.
병합한 뒤가 아니라 병합하기 전에 변환하세요.
iconv -f WINDOWS-1252 -t UTF-8 old.csv > old-utf8.csv
file -I *.csv로 어떤 인코딩인지 추측할 수는 있지만, 짧은 파일에서는 잘 틀립니다. 확실한 방법은 각 파일을 열어 ASCII가 아닌 문자가 들어 있는 행을 직접 보는 것입니다.