파일 형식별 데이터 처리

파일 형식별 데이터 처리 #

개요 #

데이터는 다양한 형식으로 저장되고 전송되며, 각 형식마다 고유한 특성과 처리 방법이 있다. 효율적인 데이터 수집을 위해서는 각 파일 형식의 특성을 이해하고 적절한 도구와 기법을 사용해야 한다.

구조화된 데이터 형식 #

CSV (Comma-Separated Values) #

import pandas as pd
import csv
from io import StringIO

class CSVProcessor:
    def __init__(self):
        self.encoding_options = ['utf-8', 'cp949', 'euc-kr', 'latin-1']
    
    def read_csv_safe(self, file_path, **kwargs):
        """안전한 CSV 읽기 (인코딩 자동 감지)"""
        for encoding in self.encoding_options:
            try:
                df = pd.read_csv(file_path, encoding=encoding, **kwargs)
                print(f"성공적으로 읽음: {encoding} 인코딩")
                return df
            except UnicodeDecodeError:
                continue
            except Exception as e:
                print(f"오류 발생 ({encoding}): {e}")
                continue
        
        raise Exception("모든 인코딩 옵션으로 읽기 실패")
    
    def detect_delimiter(self, file_path, sample_size=1024):
        """구분자 자동 감지"""
        with open(file_path, 'r', encoding='utf-8') as file:
            sample = file.read(sample_size)
            
        sniffer = csv.Sniffer()
        delimiter = sniffer.sniff(sample).delimiter
        return delimiter
    
    def process_large_csv(self, file_path, chunk_size=10000, process_func=None):
        """대용량 CSV 청크 단위 처리"""
        results = []
        
        for chunk in pd.read_csv(file_path, chunksize=chunk_size):
            if process_func:
                processed_chunk = process_func(chunk)
                results.append(processed_chunk)
            else:
                results.append(chunk)
        
        return pd.concat(results, ignore_index=True)
    
    def validate_csv_structure(self, df, expected_columns=None, required_columns=None):
        """CSV 구조 검증"""
        validation_result = {
            'is_valid': True,
            'issues': []
        }
        
        if expected_columns:
            missing_cols = set(expected_columns) - set(df.columns)
            if missing_cols:
                validation_result['is_valid'] = False
                validation_result['issues'].append(f"누락된 컬럼: {missing_cols}")
        
        if required_columns:
            for col in required_columns:
                if col in df.columns:
                    null_count = df[col].isnull().sum()
                    if null_count > 0:
                        validation_result['issues'].append(f"{col}: {null_count}개 null 값")
        
        return validation_result

# 사용 예시
csv_processor = CSVProcessor()
df = csv_processor.read_csv_safe('data.csv')
delimiter = csv_processor.detect_delimiter('data.csv')
validation = csv_processor.validate_csv_structure(df, expected_columns=['name', 'age', 'email'])

JSON (JavaScript Object Notation) #

import json
import pandas as pd
from pandas import json_normalize

class JSONProcessor:
    def __init__(self):
        pass
    
    def read_json_file(self, file_path, lines=False):
        """JSON 파일 읽기"""
        try:
            if lines:
                # JSON Lines 형식
                data = []
                with open(file_path, 'r', encoding='utf-8') as file:
                    for line in file:
                        data.append(json.loads(line.strip()))
                return data
            else:
                # 일반 JSON 형식
                with open(file_path, 'r', encoding='utf-8') as file:
                    return json.load(file)
        except Exception as e:
            print(f"JSON 읽기 오류: {e}")
            return None
    
    def flatten_json(self, data, separator='_'):
        """중첩된 JSON 구조 평면화"""
        if isinstance(data, list):
            return [self.flatten_json(item, separator) for item in data]
        elif isinstance(data, dict):
            flattened = {}
            for key, value in data.items():
                if isinstance(value, dict):
                    nested = self.flatten_json(value, separator)
                    for nested_key, nested_value in nested.items():
                        flattened[f"{key}{separator}{nested_key}"] = nested_value
                elif isinstance(value, list) and value and isinstance(value[0], dict):
                    # 리스트 내 딕셔너리 처리
                    for i, item in enumerate(value):
                        nested = self.flatten_json(item, separator)
                        for nested_key, nested_value in nested.items():
                            flattened[f"{key}{separator}{i}{separator}{nested_key}"] = nested_value
                else:
                    flattened[key] = value
            return flattened
        else:
            return data
    
    def json_to_dataframe(self, json_data, normalize_level=0):
        """JSON 데이터를 DataFrame으로 변환"""
        if isinstance(json_data, list):
            if normalize_level > 0:
                return json_normalize(json_data, sep='_', max_level=normalize_level)
            else:
                return pd.DataFrame(json_data)
        elif isinstance(json_data, dict):
            return pd.DataFrame([json_data])
        else:
            raise ValueError("지원되지 않는 JSON 구조")
    
    def extract_nested_fields(self, data, field_path):
        """중첩된 필드 추출 (예: 'user.profile.name')"""
        try:
            fields = field_path.split('.')
            result = data
            
            for field in fields:
                if isinstance(result, list):
                    result = [item.get(field) if isinstance(item, dict) else None for item in result]
                elif isinstance(result, dict):
                    result = result.get(field)
                else:
                    return None
            
            return result
        except Exception:
            return None

# 사용 예시
json_processor = JSONProcessor()

# 샘플 JSON 데이터
sample_json = {
    "users": [
        {
            "id": 1,
            "name": "김철수",
            "profile": {
                "age": 30,
                "address": {
                    "city": "서울",
                    "district": "강남구"
                }
            },
            "orders": [
                {"id": 101, "amount": 50000},
                {"id": 102, "amount": 30000}
            ]
        }
    ]
}

flattened = json_processor.flatten_json(sample_json)
df = json_processor.json_to_dataframe(sample_json['users'])

XML (eXtensible Markup Language) #

import xml.etree.ElementTree as ET
import pandas as pd
from bs4 import BeautifulSoup
import xmltodict

class XMLProcessor:
    def __init__(self):
        pass
    
    def parse_xml_to_dict(self, file_path):
        """XML을 딕셔너리로 변환"""
        try:
            with open(file_path, 'r', encoding='utf-8') as file:
                xml_content = file.read()
            
            return xmltodict.parse(xml_content)
        except Exception as e:
            print(f"XML 파싱 오류: {e}")
            return None
    
    def xml_to_dataframe(self, file_path, record_path=None):
        """XML을 DataFrame으로 변환"""
        try:
            tree = ET.parse(file_path)
            root = tree.getroot()
            
            records = []
            
            if record_path:
                # 특정 경로의 요소들을 레코드로 처리
                for element in root.findall(record_path):
                    record = {}
                    for child in element:
                        record[child.tag] = child.text
                    records.append(record)
            else:
                # 루트의 직접 자식들을 레코드로 처리
                for child in root:
                    record = {}
                    for subchild in child:
                        record[subchild.tag] = subchild.text
                    records.append(record)
            
            return pd.DataFrame(records)
        
        except Exception as e:
            print(f"XML to DataFrame 변환 오류: {e}")
            return None
    
    def extract_xml_attributes(self, file_path, element_path):
        """XML 요소의 속성 추출"""
        try:
            tree = ET.parse(file_path)
            root = tree.getroot()
            
            attributes_list = []
            
            for element in root.findall(element_path):
                attributes = element.attrib.copy()
                attributes['text'] = element.text
                attributes_list.append(attributes)
            
            return attributes_list
        
        except Exception as e:
            print(f"XML 속성 추출 오류: {e}")
            return []

# 사용 예시
xml_processor = XMLProcessor()

# 샘플 XML 파일 생성
sample_xml = """<?xml version="1.0" encoding="UTF-8"?>
<customers>
    <customer id="1">
        <name>김철수</name>
        <email>kim@example.com</email>
        <age>30</age>
    </customer>
    <customer id="2">
        <name>이영희</name>
        <email>lee@example.com</email>
        <age>25</age>
    </customer>
</customers>"""

# 파일로 저장
with open('sample.xml', 'w', encoding='utf-8') as f:
    f.write(sample_xml)

df = xml_processor.xml_to_dataframe('sample.xml', './/customer')

반구조화된 데이터 형식 #

Excel 파일 #

import pandas as pd
import openpyxl
from openpyxl.styles import PatternFill

class ExcelProcessor:
    def __init__(self):
        pass
    
    def read_excel_advanced(self, file_path, **kwargs):
        """고급 Excel 읽기"""
        try:
            # 모든 시트 이름 가져오기
            excel_file = pd.ExcelFile(file_path)
            sheet_names = excel_file.sheet_names
            
            sheets_data = {}
            
            for sheet_name in sheet_names:
                try:
                    df = pd.read_excel(file_path, sheet_name=sheet_name, **kwargs)
                    sheets_data[sheet_name] = df
                except Exception as e:
                    print(f"시트 '{sheet_name}' 읽기 실패: {e}")
            
            return sheets_data
        
        except Exception as e:
            print(f"Excel 파일 읽기 오류: {e}")
            return None
    
    def detect_data_range(self, file_path, sheet_name=0):
        """데이터 범위 자동 감지"""
        workbook = openpyxl.load_workbook(file_path)
        
        if isinstance(sheet_name, int):
            worksheet = workbook.worksheets[sheet_name]
        else:
            worksheet = workbook[sheet_name]
        
        # 사용된 범위 감지
        min_row = worksheet.min_row
        max_row = worksheet.max_row
        min_col = worksheet.min_column
        max_col = worksheet.max_column
        
        return {
            'min_row': min_row,
            'max_row': max_row,
            'min_col': min_col,
            'max_col': max_col,
            'range': f"{worksheet.cell(min_row, min_col).coordinate}:{worksheet.cell(max_row, max_col).coordinate}"
        }
    
    def handle_merged_cells(self, file_path, sheet_name=0):
        """병합된 셀 처리"""
        workbook = openpyxl.load_workbook(file_path)
        
        if isinstance(sheet_name, int):
            worksheet = workbook.worksheets[sheet_name]
        else:
            worksheet = workbook[sheet_name]
        
        # 병합된 셀 정보 수집
        merged_ranges = list(worksheet.merged_cells.ranges)
        
        # 병합 해제 및 값 복제
        for merged_range in merged_ranges:
            # 병합된 영역의 첫 번째 셀 값 가져오기
            min_col, min_row, max_col, max_row = merged_range.bounds
            first_cell_value = worksheet.cell(min_row, min_col).value
            
            # 병합 해제
            worksheet.unmerge_cells(str(merged_range))
            
            # 모든 셀에 값 복제
            for row in range(min_row, max_row + 1):
                for col in range(min_col, max_col + 1):
                    worksheet.cell(row, col).value = first_cell_value
        
        return workbook
    
    def export_to_excel(self, dataframes_dict, output_path):
        """여러 DataFrame을 Excel 파일로 내보내기"""
        with pd.ExcelWriter(output_path, engine='openpyxl') as writer:
            for sheet_name, df in dataframes_dict.items():
                df.to_excel(writer, sheet_name=sheet_name, index=False)
                
                # 스타일링 적용
                workbook = writer.book
                worksheet = writer.sheets[sheet_name]
                
                # 헤더 스타일링
                header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")
                for cell in worksheet[1]:
                    cell.fill = header_fill

# 사용 예시
excel_processor = ExcelProcessor()
sheets_data = excel_processor.read_excel_advanced('data.xlsx')
data_range = excel_processor.detect_data_range('data.xlsx', 'Sheet1')

비구조화된 데이터 형식 #

텍스트 파일 #

import re
import chardet
from collections import Counter

class TextProcessor:
    def __init__(self):
        self.encoding_cache = {}
    
    def detect_encoding(self, file_path):
        """파일 인코딩 감지"""
        if file_path in self.encoding_cache:
            return self.encoding_cache[file_path]
        
        with open(file_path, 'rb') as file:
            raw_data = file.read()
            result = chardet.detect(raw_data)
            
        encoding = result['encoding']
        confidence = result['confidence']
        
        self.encoding_cache[file_path] = {'encoding': encoding, 'confidence': confidence}
        
        return {'encoding': encoding, 'confidence': confidence}
    
    def read_text_file(self, file_path, encoding=None):
        """텍스트 파일 안전 읽기"""
        if not encoding:
            detection_result = self.detect_encoding(file_path)
            encoding = detection_result['encoding']
        
        try:
            with open(file_path, 'r', encoding=encoding) as file:
                return file.read()
        except Exception as e:
            print(f"파일 읽기 오류: {e}")
            return None
    
    def extract_structured_data(self, text, patterns):
        """정규식을 사용한 구조화된 데이터 추출"""
        extracted_data = {}
        
        for field_name, pattern in patterns.items():
            matches = re.findall(pattern, text, re.MULTILINE | re.IGNORECASE)
            extracted_data[field_name] = matches
        
        return extracted_data
    
    def parse_log_file(self, file_path, log_pattern=None):
        """로그 파일 파싱"""
        if log_pattern is None:
            # 기본 로그 패턴 (Apache Combined Log Format)
            log_pattern = r'(\S+) \S+ \S+ \[([\w:/]+\s[+\-]\d{4})\] "(\S+) (\S+) (\S+)" (\d{3}) (\d+) "([^"]*)" "([^"]*)"'
        
        text_content = self.read_text_file(file_path)
        if not text_content:
            return None
        
        log_entries = []
        
        for line in text_content.split('\n'):
            if line.strip():
                match = re.match(log_pattern, line)
                if match:
                    log_entries.append(match.groups())
        
        return log_entries
    
    def analyze_text_statistics(self, text):
        """텍스트 통계 분석"""
        words = re.findall(r'\b\w+\b', text.lower())
        
        statistics = {
            'total_characters': len(text),
            'total_words': len(words),
            'unique_words': len(set(words)),
            'average_word_length': sum(len(word) for word in words) / len(words) if words else 0,
            'most_common_words': Counter(words).most_common(10),
            'lines': len(text.split('\n')),
            'sentences': len(re.split(r'[.!?]+', text))
        }
        
        return statistics

# 사용 예시
text_processor = TextProcessor()

# 이메일 주소와 전화번호 추출 패턴
patterns = {
    'emails': r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b',
    'phones': r'\b\d{2,3}-\d{3,4}-\d{4}\b',
    'urls': r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+'
}

text_content = text_processor.read_text_file('sample.txt')
extracted = text_processor.extract_structured_data(text_content, patterns)
stats = text_processor.analyze_text_statistics(text_content)

PDF 파일 #

import PyPDF2
import pdfplumber
import pandas as pd
from io import StringIO

class PDFProcessor:
    def __init__(self):
        pass
    
    def extract_text_pypdf2(self, file_path):
        """PyPDF2를 사용한 텍스트 추출"""
        try:
            with open(file_path, 'rb') as file:
                pdf_reader = PyPDF2.PdfReader(file)
                text = ""
                
                for page in pdf_reader.pages:
                    text += page.extract_text() + "\n"
                
                return text
        except Exception as e:
            print(f"PDF 텍스트 추출 오류: {e}")
            return None
    
    def extract_text_pdfplumber(self, file_path):
        """pdfplumber를 사용한 고급 텍스트 추출"""
        try:
            text = ""
            
            with pdfplumber.open(file_path) as pdf:
                for page in pdf.pages:
                    page_text = page.extract_text()
                    if page_text:
                        text += page_text + "\n"
            
            return text
        except Exception as e:
            print(f"PDF 텍스트 추출 오류: {e}")
            return None
    
    def extract_tables_from_pdf(self, file_path, page_numbers=None):
        """PDF에서 테이블 추출"""
        try:
            tables = []
            
            with pdfplumber.open(file_path) as pdf:
                pages_to_process = page_numbers or range(len(pdf.pages))
                
                for page_num in pages_to_process:
                    if page_num < len(pdf.pages):
                        page = pdf.pages[page_num]
                        page_tables = page.extract_tables()
                        
                        for table in page_tables:
                            if table:  # 빈 테이블 제외
                                df = pd.DataFrame(table[1:], columns=table[0])
                                tables.append({
                                    'page': page_num + 1,
                                    'dataframe': df
                                })
            
            return tables
        except Exception as e:
            print(f"PDF 테이블 추출 오류: {e}")
            return []
    
    def get_pdf_metadata(self, file_path):
        """PDF 메타데이터 추출"""
        try:
            with open(file_path, 'rb') as file:
                pdf_reader = PyPDF2.PdfReader(file)
                metadata = pdf_reader.metadata
                
                info = {
                    'title': metadata.get('/Title', ''),
                    'author': metadata.get('/Author', ''),
                    'subject': metadata.get('/Subject', ''),
                    'creator': metadata.get('/Creator', ''),
                    'producer': metadata.get('/Producer', ''),
                    'creation_date': metadata.get('/CreationDate', ''),
                    'modification_date': metadata.get('/ModDate', ''),
                    'pages': len(pdf_reader.pages)
                }
                
                return info
        except Exception as e:
            print(f"PDF 메타데이터 추출 오류: {e}")
            return {}

# 사용 예시
pdf_processor = PDFProcessor()
text = pdf_processor.extract_text_pdfplumber('document.pdf')
tables = pdf_processor.extract_tables_from_pdf('document.pdf')
metadata = pdf_processor.get_pdf_metadata('document.pdf')

대용량 파일 처리 #

스트리밍 처리 #

import os
import mmap
from itertools import islice

class LargeFileProcessor:
    def __init__(self):
        pass
    
    def process_file_in_chunks(self, file_path, chunk_size=1024*1024, process_func=None):
        """파일을 청크 단위로 처리"""
        results = []
        
        with open(file_path, 'r', encoding='utf-8') as file:
            while True:
                chunk = file.read(chunk_size)
                if not chunk:
                    break
                
                if process_func:
                    result = process_func(chunk)
                    results.append(result)
                else:
                    results.append(chunk)
        
        return results
    
    def read_lines_batch(self, file_path, batch_size=1000):
        """라인을 배치 단위로 읽기"""
        with open(file_path, 'r', encoding='utf-8') as file:
            while True:
                batch = list(islice(file, batch_size))
                if not batch:
                    break
                yield batch
    
    def memory_mapped_search(self, file_path, search_term):
        """메모리 맵을 사용한 대용량 파일 검색"""
        matches = []
        
        with open(file_path, 'r', encoding='utf-8') as file:
            with mmap.mmap(file.fileno(), 0, access=mmap.ACCESS_READ) as mmapped_file:
                content = mmapped_file.read().decode('utf-8')
                
                lines = content.split('\n')
                for line_num, line in enumerate(lines, 1):
                    if search_term in line:
                        matches.append({
                            'line_number': line_num,
                            'content': line.strip()
                        })
        
        return matches
    
    def get_file_info(self, file_path):
        """파일 정보 조회"""
        stat = os.stat(file_path)
        
        return {
            'size_bytes': stat.st_size,
            'size_mb': stat.st_size / (1024 * 1024),
            'size_gb': stat.st_size / (1024 * 1024 * 1024),
            'created': stat.st_ctime,
            'modified': stat.st_mtime,
            'accessed': stat.st_atime
        }

# 사용 예시
large_file_processor = LargeFileProcessor()

# 대용량 파일 처리 함수
def count_words_in_chunk(chunk):
    return len(chunk.split())

word_counts = large_file_processor.process_file_in_chunks('large_file.txt', process_func=count_words_in_chunk)
total_words = sum(word_counts)

# 배치 단위 라인 처리
for batch in large_file_processor.read_lines_batch('large_file.txt', batch_size=5000):
    # 배치 처리 로직
    processed_batch = [line.strip().upper() for line in batch]

파일 형식별 데이터 처리는 각 형식의 특성을 이해하고 적절한 도구를 선택하는 것이 핵심이다. 대용량 데이터의 경우 메모리 효율성과 처리 속도를 고려한 스트리밍 방식의 접근이 필요하다.