RAG(Retrieval-Augmented Generation)은 대규모 언어 모델(LLM)의 정확성과 신뢰성을 높이기 위해 외부 문서의 정보를 검색하고 결합해 응답을 생성하는 강력한 전략입니다. 하지만 RAG의 성능은 "좋은 텍스트 데이터"에 크게 의존합니다. 특히 한국어 환경에서는 .hwp 같은 한글 문서의 비중이 높아, 이를 효과적으로 파싱하는 작업이 중요합니다.
이번 포스트에서는 Python을 활용해 HWP 및 PDF 문서를 자동으로 텍스트로 변환하고 저장하는 실전 코드를 소개합니다. 이 코드는 이후 RAG 파이프라인에 쉽게 통합될 수 있도록 설계되었습니다.
기술 스택
- Python 3
- olefile: HWP의 OLE 구조를 분석
- zlib: HWP 내부 압축 해제
- fitz (PyMuPDF): PDF 텍스트 추출
- os, struct: 파일 및 바이너리 처리
폴더 구조
.
├── data/
│ ├── rule/ # 변환 대상 .hwp 및 .pdf 파일
│ └── rule_txt/ # 변환된 .txt 파일 저장 폴더
└── converter.py # 문서 파싱 및 변환 코드
전체 코드
(실행 부분의 폴더를 바꿔서 실행하세요.)
import os
import olefile
import zlib
import struct
import fitz # PyMuPDF
def get_hwp_text(filename):
f = olefile.OleFileIO(filename)
dirs = f.listdir()
# HWP 파일 검증
if ["FileHeader"] not in dirs or ["\x05HwpSummaryInformation"] not in dirs:
raise Exception("Not Valid HWP.")
# 문서 포맷 압축 여부 확인
header = f.openstream("FileHeader")
header_data = header.read()
is_compressed = (header_data[36] & 1) == 1
# Body Sections 불러오기
nums = []
for d in dirs:
if d[0] == "BodyText":
nums.append(int(d[1][len("Section"):]))
sections = ["BodyText/Section" + str(x) for x in sorted(nums)]
# 전체 text 추출
text = ""
for section in sections:
bodytext = f.openstream(section)
data = bodytext.read()
if is_compressed:
unpacked_data = zlib.decompress(data, -15)
else:
unpacked_data = data
section_text = ""
i = 0
size = len(unpacked_data)
while i < size:
header = struct.unpack_from("<I", unpacked_data, i)[0]
rec_type = header & 0x3ff
rec_len = (header >> 20) & 0xfff
if rec_type in [67]: # Paragraph text
rec_data = unpacked_data[i + 4:i + 4 + rec_len]
section_text += rec_data.decode('utf-16')
section_text += "\n"
i += 4 + rec_len
text += section_text
text += "\n"
return text
def get_pdf_text(filename):
doc = fitz.open(filename)
text = ""
for page in doc:
text += page.get_text()
text += "\n"
return text
def convert_documents(input_dir, output_dir):
os.makedirs(output_dir, exist_ok=True)
for filename in os.listdir(input_dir):
filepath = os.path.join(input_dir, filename)
name, ext = os.path.splitext(filename.lower())
try:
if ext == '.hwp':
text = get_hwp_text(filepath)
elif ext == '.pdf':
text = get_pdf_text(filepath)
else:
continue # Skip unsupported files
output_path = os.path.join(output_dir, name + ".txt")
with open(output_path, "w", encoding="utf-8") as f:
f.write(text)
print(f"Converted: {filename} -> {output_path}")
except Exception as e:
print(f"Failed to convert {filename}: {e}")
# 실행
convert_documents("./data/rule", "./data/rule_txt")
📄 핵심 코드 설명
1. HWP 파일 파싱
한글(HWP) 문서는 OLE2 포맷을 사용하며, 내부 섹션의 압축 여부에 따라 처리 방식이 다릅니다. 다음 함수는 이 구조를 분석하여 텍스트를 추출합니다.
def get_hwp_text(filename):
...
if ["FileHeader"] not in dirs or ["\x05HwpSummaryInformation"] not in dirs:
raise Exception("Not Valid HWP.")
...
if is_compressed:
unpacked_data = zlib.decompress(data, -15)
...
if rec_type in [67]: # Paragraph text
section_text += rec_data.decode('utf-16')
이 파서의 특징은:
- 압축 여부를 자동 인식
- 실제 문단 텍스트(rec_type == 67)만 추출
- UTF-16 디코딩을 통해 한글이 깨지지 않게 처리
2. PDF 파싱
PDF는 상대적으로 쉬운 구조이지만, 페이지 단위 추출이 필요합니다.
def get_pdf_text(filename):
doc = fitz.open(filename)
text = ""
for page in doc:
text += page.get_text()
fitz 라이브러리는 단순하면서도 레이아웃 유지에 강점이 있습니다.
3. 폴더 단위 변환
입력 폴더 내 .hwp 및 .pdf 파일을 일괄 처리하고 .txt로 저장합니다.
def convert_documents(input_dir, output_dir):
...
if ext == '.hwp':
text = get_hwp_text(filepath)
elif ext == '.pdf':
text = get_pdf_text(filepath)
변환 실패 시 예외를 로깅하며 흐름을 끊지 않는 방식도 실전 환경에서 유용합니다.
🤖 RAG 파이프라인에 어떻게 활용하나요?
- 텍스트 정제: 변환된 .txt 파일을 정제 및 청크 처리 (예: 문단 기준으로 나누기)
- 벡터화: 청크를 임베딩 모델(BERT, OpenAI Embedding 등)로 벡터화
- 벡터 DB 삽입: FAISS, Weaviate, Qdrant 등에 저장
- 질의 응답: 사용자 질문에 대해 관련 청크를 검색하고 LLM 입력으로 사용
이렇게 추출된 문서들은 RAG의 기반 지식이 되어, 모델이 "근거 있는" 응답을 생성할 수 있게 합니다.
🧪 마무리: 실전 운영 팁
- HWP의 경우 문서 포맷에 따라 일부 파싱 오류가 있을 수 있으므로 테스트 케이스를 확보하세요.
- 문서의 텍스트가 아닌 이미지(예: 스캔 PDF)는 OCR이 추가로 필요합니다.
- 파일명이 같을 경우 .txt로 저장할 때 덮어쓰기 주의 (중복 방지 로직 권장)
'AI' 카테고리의 다른 글
윈도우에서 gemma3 로컬 활용하기(ollama활용, sLLM) (0) | 2025.03.19 |
---|---|
LLM GPU 요구사항 계산기: LLM활용을 위한 요구사항 확인 도구 (1) | 2025.01.20 |
인기 LLM 기반 유료 AI 툴 리뷰: ChatGPT, Claude, Perplexity, Cursor (0) | 2024.09.26 |
LLM 모델 크기에 따른 GPU 세팅 가이드 ( RTX3090, H100 기준 ) (0) | 2024.09.19 |
[DallE3]Oriental Dragon (0) | 2024.09.12 |