CSV를 JSON으로 변환하기: 방법과 함정

· 12분 읽기

목차

CSV에서 JSON으로 변환의 기초 이해하기

CSV(쉼표로 구분된 값)를 JSON(JavaScript Object Notation)으로 변환하는 것은 개발자가 접하는 가장 일반적인 데이터 변환 작업 중 하나입니다. 간단한 데이터셋의 경우 프로세스가 간단해 보이지만, 기본 메커니즘을 이해하면 데이터를 손상시킬 수 있는 미묘한 버그를 피할 수 있습니다.

CSV 파일은 첫 번째 행에 일반적으로 열 헤더가 포함된 표 형식 구조를 따릅니다. 각 후속 행은 해당 헤더에 해당하는 값이 있는 레코드를 나타냅니다. 반면 JSON은 더 유연하고 표현력이 풍부한 계층적 키-값 구조를 사용합니다.

기본 변환은 CSV 헤더를 JSON 키에 매핑하며, 각 데이터 행은 JSON 배열의 객체가 됩니다:

CSV:
name,age,city
Alice,30,NYC
Bob,25,LA

JSON:
[
  {"name":"Alice","age":"30","city":"NYC"},
  {"name":"Bob","age":"25","city":"LA"}
]

이러한 일대일 대응은 평면 데이터 구조에 완벽하게 작동합니다. 그러나 실제 시나리오에서는 누락된 값, 일관되지 않은 데이터 타입, 특수 문자 및 중첩된 구조의 필요성 등 복잡한 문제가 발생하며 모두 신중한 처리가 필요합니다.

전문가 팁: 변환하기 전에 항상 CSV 파일의 처음 몇 행을 검사하세요. 구문 분석 오류를 일으킬 수 있는 일관되지 않은 구분 기호, 인용된 필드 및 예상치 못한 줄 바꿈을 찾으세요.

왜 CSV를 JSON으로 변환하나요?

JSON은 웹 API 및 최신 애플리케이션 개발의 사실상 표준이 되었습니다. 개발자가 CSV 데이터를 자주 변환해야 하는 이유는 다음과 같습니다:

데이터 타입 변환 과제

CSV를 JSON으로 변환할 때 가장 중요한 과제 중 하나는 데이터 타입을 보존하는 것입니다. CSV는 기본적으로 텍스트 형식이며 모든 값이 문자열로 저장됩니다. 이는 데이터에 JSON에서 올바르게 표현되어야 하는 숫자, 날짜, 부울 또는 null 값이 포함된 경우 문제를 일으킵니다.

숫자 데이터 구문 분석

제품 재고 데이터가 포함된 CSV 파일을 고려하세요. 적절한 타입 변환 없이는 가격 및 수량과 같은 숫자 값이 문자열로 남아 애플리케이션의 계산 및 비교가 중단됩니다.

import csv
import json

def parse_csv_with_types(filename):
    def try_numeric(val):
        # Handle empty values
        if not val or val.strip() == '':
            return None
        
        # Try integer conversion first
        try:
            return int(val)
        except ValueError:
            pass
        
        # Try float conversion
        try:
            return float(val)
        except ValueError:
            return val
    
    with open(filename, 'r') as f:
        reader = csv.DictReader(f)
        data = []
        for row in reader:
            typed_row = {k: try_numeric(v) for k, v in row.items()}
            data.append(typed_row)
    
    return json.dumps(data, indent=2)

이 접근 방식은 각 값을 먼저 정수로 변환하려고 시도한 다음 부동 소수점으로 변환하고, 두 변환이 모두 실패하면 마지막으로 문자열로 유지합니다. 결과는 숫자 정밀도를 보존하는 적절하게 타입이 지정된 JSON입니다.

날짜 및 시간 처리

CSV 파일에는 ISO 8601, 미국 형식(MM/DD/YYYY), 유럽 형식(DD/MM/YYYY) 또는 사용자 지정 형식 등 수많은 형식의 날짜가 포함될 수 있기 때문에 날짜 구문 분석은 고유한 과제를 제시합니다. 변환 로직은 이러한 변형을 처리해야 합니다:

from datetime import datetime

def parse_date(val):
    date_formats = [
        '%Y-%m-%d',           # ISO format
        '%m/%d/%Y',           # US format
        '%d/%m/%Y',           # European format
        '%Y-%m-%d %H:%M:%S',  # ISO with time
        '%m/%d/%Y %I:%M %p'   # US with 12-hour time
    ]
    
    for fmt in date_formats:
        try:
            return datetime.strptime(val, fmt).isoformat()
        except ValueError:
            continue
    
    return val  # Return original if no format matches

빠른 팁: 여러 소스의 날짜를 처리할 때 JSON 출력에서 ISO 8601 형식(YYYY-MM-DD)으로 표준화하세요. 이 형식은 명확하며 문자열로 올바르게 정렬됩니다.

부울 및 Null 값 변환

CSV 파일은 종종 부울 값을 "true"/"false", "yes"/"no", "1"/"0" 또는 유사한 변형으로 나타냅니다. 빈 셀은 null 값을 나타낼 수 있지만 빈 문자열일 수도 있습니다. 변환 로직은 이러한 모호성을 처리해야 합니다:

CSV 값 의도된 타입 일반적인 실수 올바른 JSON
true 부울 "true" (문자열) true
1 부울 또는 정수 "1" (문자열) true 또는 1
(비어있음) Null "" (빈 문자열) null
N/A Null "N/A" (문자열) null

변환하기 전에 데이터가 어떻게 해석될지 미리 보려면 CSV 파서 및 뷰어를 사용하세요.

특수 문자 및 인코딩 처리

특수 문자 및 인코딩 문제는 다른 어떤 문제보다 더 많은 변환 실패를 일으킵니다. CSV 파일에는 필드 내의 쉼표, 텍스트의 줄 바꿈, 따옴표 또는 순진한 구문 분석 로직을 중단시키는 비ASCII 문자가 포함될 수 있습니다.

인용된 필드 및 이스케이프된 문자

CSV 표준(RFC 4180)은 쉼표, 따옴표 또는 줄 바꿈이 포함된 필드를 큰따옴표로 묶어야 한다고 지정합니다. 인용된 필드 내에서 따옴표 자체는 두 배로 늘려 이스케이프해야 합니다:

name,description,price
"Widget A","A simple, reliable widget",19.99
"Widget ""Pro""","The ""best"" widget available",49.99

강력한 CSV 파서는 이러한 경우를 자동으로 처리합니다. 자체 파서를 작성하는 경우 인용된 필드 내부에 있는지 추적하고 이스케이프 시퀀스를 올바르게 처리해야 합니다.

문자 인코딩 문제

CSV 파일은 UTF-8, Latin-1, Windows-1252 또는 기타 문자 집합으로 인코딩될 수 있습니다. 일치하지 않는 인코딩은 특히 영어가 아닌 문자의 경우 깨진 텍스트를 유발합니다:

CSV 파일을 읽을 때 항상 인코딩을 명시적으로 지정하세요:

import csv
import json

def convert_with_encoding(filename, encoding='utf-8'):
    try:
        with open(filename, 'r', encoding=encoding) as f:
            reader = csv.DictReader(f)
            data = list(reader)
            return json.dumps(data, ensure_ascii=False, indent=2)
    except UnicodeDecodeError:
        # Try alternative encodings
        for alt_encoding in ['latin-1', 'windows-1252', 'utf-16']:
            try:
                with open(filename, 'r', encoding=alt_encoding) as f:
                    reader = csv.DictReader(f)
                    data = list(reader)
                    return json.dumps(data, ensure_ascii=False, indent=2)
            except UnicodeDecodeError:
                continue
        raise ValueError(f"Could not decode {filename} with any known encoding")

전문가 팁: json.dumps()ensure_ascii=False 매개변수는 유니코드 문자를 \uXXXX 시퀀스로 이스케이프하는 대신 출력에 보존하여 JSON을 더 읽기 쉽게 만듭니다.

바이트 순서 표시(BOM)

일부 애플리케이션, 특히 Microsoft Excel은 UTF-8 파일의 시작 부분에 바이트 순서 표시(BOM)를 추가합니다. 이 보이지 않는 문자는 첫 번째 필드 이름을 잘못 읽게 할 수 있습니다. Python의 인코딩 매개변수는 'utf-8-sig'로 이를 자동으로 처리합니다:

with open(filename, 'r', encoding='utf-8-sig') as f:
    reader = csv.DictReader(f)

평면 CSV에서 중첩된 JSON 구조 만들기

CSV는 본질적으로 평면적이며 2차원 테이블을 나타냅니다. JSON은 중첩된 객체 및 배열이 있는 계층적 구조를 지원합니다. 평면 CSV 데이터를 중첩된 JSON으로 변환하려면 신중한 설계와 추가 로직이 필요합니다.

관련 데이터 그룹화

각 행에 모든 주문에 대해 고객 정보가 반복되는 고객 주문이 포함된 CSV 파일을 고려하세요:

customer_id,customer_name,order_id,product,quantity
101,Alice,1001,Widget,5
101,Alice,1002,Gadget,3
102,Bob,1003,Widget,2

더 나은 JSON 구조는 각 고객 아래에 주문을 그룹화합니다:

[
  {
    "customer_id": 101,
    "customer_name": "Alice",
    "orders": [
      {"order_id": 1001, "product": "Widget", "quantity": 5},
      {"order_id": 1002, "product": "Gadget", "quantity": 3}
    ]
  },
  {
    "customer_id": 102,
    "customer_name": "Bob",
    "orders": [
      {"order_id": 1003, "product": "Widget", "quantity": 2}
    ]
  }
]

이 변환을 구현하는 방법은 다음과 같습니다:

import csv
import json
from collections import defaultdict

def csv_to_nested_json(filename):
    customers = defaultdict(lambda: {"orders": []})
    
    with open(filename, 'r') as f:
        reader = csv.DictReader(f)
        for row in reader:
            customer_id = int(row['customer_id'])
            
            # Set customer info if not already set
            if 'customer_id' not in customers[customer_id]:
                customers[customer_id]['customer_id'] = customer_id
                customers[customer_id]['customer_name'] = row['customer_name']
            
            # Add order
            customers[customer_id]['orders'].append({
                'order_id': int(row['order_id']),
                'product': row['product'],
                'quantity': int(row['quantity'])
            })
    
    return json.dumps(list(customers.values()), indent=2)

중첩 키를 위한 점 표기법

또 다른 접근 방식은 CSV 헤더에서 점 표기법을 사용하여 중첩을 나타냅니다:

name,address.street,address.city,address.zip
Alice,123 Main St,NYC,10001
Bob,456 Oak Ave,LA,90001

이것은 다음과 같이 변환됩니다:

[
  {
    "name": "Alice",
    "address": {
      "street": "123 Main St",
      "city": "NYC",
      "zip": "10001"
    }
  }
]

구현에는 헤더 키를 구문 분석하고 중첩된 딕셔너리를 구축해야 합니다:

def set_nested_value(obj, path, value):
    keys = path.split('.')
    for key in keys[:-1]:
        obj = obj.setdefault(key, {})
    obj[keys[-1]] = value

def csv_to_nested_with_dots(filename):
    with open(filename, 'r') as f:
        reader = csv.DictReader(f)
        data = []
        for row in reader:
            obj = {}
            for key, value in row.items():
                set_nested_value(obj, key, value)
            data.append(obj)
    return json.dumps(data, indent=2)

대용량 파일 확장 및 성능 최적화

작은 CSV 파일을 변환하는 것은 간단하지만 프로덕션 시스템은 종종 수백만 행이 포함된 파일을 처리합니다. 전체 멀티 기가바이트 CSV를 메모리에 로드하면 충돌 및 성능 문제가 발생합니다.

스트리밍 처리

전체 파일을 메모리에 로드하는 대신 행별로 처리하고 JSON을 점진적으로 작성하세요:

import csv
import json

def stream_csv_to_json(input_file, output_file):
    with open(input_file, 'r') as infile, open(output_file, 'w') as outfile:
        reader = csv.DictReader(infile)
        
        outfile.write('[\n')
        first = True
        
        for row in reader:
            if not first:
                outfile.write(',\n')
            first = False
            
            json.dump(row, outfile)
        
        outfile.write('\n]')

이 접근 방식은 파일 크기에 관계없이 일정한 메모리 사용량을 유지합니다. 단점은 중첩된 구조를 쉽게 만들거나 모든 데이터를 한 번에 봐야 하는 집계를 수행할 수 없다는 것입니다.

청크 처리

일부 집계가 필요하지만 전체 데이터셋이 필요하지 않은 작업의 경우 파일을 청크로 처리하세요:

def process_in_chunks(filename, chunk_size=10000):
    with open(filename, 'r') as f:
        reader = csv.DictReader(f)
        chunk = []
        
    

We use cookies for analytics. By continuing, you agree to our Privacy Policy.