
첫번째였나 두번째였나 RAG를 기초부터 다시 다지기로 한 뒤 가졌던 스터디 발표 시간.
나연이가 발표하는 날이었는데 꽤 많은 장수의 pdf 파일의 텍스트가 잘추출되는 것을 보여주었는데
이 덕분에 우리 스터디의 고민이었던 대용량 PDF 처리의 실마리를 찾아볼 수 있는 계기가 되었다.
그렇다면 날것, 파이썬 라이브러리만으로 처리했을 때는 랭체인과 어떤 차이가 있었길래 추출이 잘 안되었던걸까?
클로드 말에 따르면 랭체인이 잘 되는 이유는 내부적으로 레이아웃 분석을 해주기 때문이다.
직접 구현할 때는 pdfplumber나 fitz 중 어떤 걸 쓰느냐 + 어떻게 정렬하느냐가 핵심이다.
이 PDF처럼 다단 레이아웃이나 표가 섞인 경우 pdfplumber가 좌표 기반으로 더 잘 처리한다.
이 부분은 그날도 그 이전에도 계속 나왔던 얘기었으나 나는 잘 이해하지 못했다.
pdf가 좌표가 있다고.. 머리가 아프군 이정도.
22-05. 랭체인의 PDF를 읽는 다양한 방법
랭체인에서는 PDF 파일을 읽고 처리할 수 있는 다양한 PDF Loader를 제공합니다. 다음 주소에서 랭체인에서 제공하는 모든 PDF Loader들을 확인할 수 있습니다. 이번…
wikidocs.net
아래에 보게 될 코드는 PDF 문서에서 텍스트를 추출하여 RAG(검색 증강 생성) 시스템이 읽기 좋은 형태로 가공하는
데이터 로더(Loader)클래스이다.
단순히 글자를 긁어오는 게 아니라, 머리말/꼬리말을 잘라내고, 글자 위치를 계산해 줄을 맞추는 정교한 작업을 수행한다.
1. 초기 설정 : __init__
def __init__(self, header_margin=0.08, footer_margin=0.08, x_tolerance=5, y_tolerance=5):
이 부분은 로더가 작동할 때의 '기준'을 정한다.
- header_margin, footer_margin(0.08) : 페이지 상단과 하단의 8% 영역을 버린다.
보통 이 영역에는 페이지 번호나 문서 제목 같은 '잡음'이 많기 때문이다.
- x_tolerance, y_tolerance(5) : 글자가 약간 어긋나 있어도 같은 단어나 같은 줄로 인식할지 결정하는 '너그러움?'의 정도이다.
2. 메인 실행 흐름 : load
PDF 파일을 열어서 전체 페이지를 하나씩 돌며 텍스트를 뽑아낸다.
결과는 ['1페이지 내용', '2페이지 내용', ...] 식의 리스트 형태로 반환된다.
import pdfplumber
import re
from service.rag.ingestion.loader.base_loader import BaseLoader
class PDFLoader(BaseLoader):
# 이 부분은 로더가 작동할 때의 '기준'을 정한다.
def __init__(self, header_margin=0.08, footer_margin=0.08, x_tolerance=5, y_tolerance=5):
self.header_margin = header_margin
self.footer_margin = footer_margin
self.x_tolerance = x_tolerance
self.y_tolerance = y_tolerance
def load(self, pdf_path: str) -> list[str]:
pages = []
with pdfplumber.open(pdf_path) as pdf:
for page in pdf.pages:
text = self._extract_page(page)
if text.strip():
pages.append(text)
return pages
def _extract_page(self, page) -> str:
height = page.height
top = height * self.header_margin
bottom = height * (1 - self.footer_margin)
cropped = page.crop((0, top, page.width, bottom))
words = cropped.extract_words(x_tolerance=self.x_tolerance, y_tolerance=self.y_tolerance)
if not words:
return ""
words_sorted = sorted(words, key=lambda w: (round(w["top"] / 5) * 5, w["x0"]))
lines = self._group_into_lines(words_sorted)
return self._clean_text("\n".join(lines))
def _group_into_lines(self, words, line_gap=6):
if not words:
return []
lines, cur_line, cur_top = [], [words[0]], words[0]["top"]
for w in words[1:]:
if abs(w["top"] - cur_top) <= line_gap:
cur_line.append(w)
else:
lines.append(" ".join(wd["text"] for wd in cur_line))
cur_line, cur_top = [w], w["top"]
lines.append(" ".join(wd["text"] for wd in cur_line))
return lines
def _clean_text(self, text):
text = text.replace("\u00a0", " ").replace("\r", "\n")
text = re.sub(r" {3,}", " ", text)
text = re.sub(r"\n{3,}", "\n\n", text)
return text.strip()
3. 핵심 기술1 : 영역 자르기와 단어 추출(_extract_page)
제미나이 말로는 이 코드가 똑똑한 이유가
PDF를 단순 텍스트가 아니라 좌표값으로 보기 때문이라고 한다.
1. page.crop : 위에서 설정한 마진을 이용해 페이지의 위아래를 잘라낸다(중요한 본문만 남김).
(이건 전에도 성희가 말했지만 장단점이 있다.)
2. extract_words : 페이지 내의 모든 단어를 좌표(x, y 위치) 정보와 함께 추출한다.
3. words_stored : 추출된 단어들을 위에서 아래로, 왼쪽에서 오른쪽으로 정렬한다.
- 여기서 round(w["top"] / 5) * 5 기술이 쓰이는데, 이는 미세하게 높낮이가 다른 글자들을 같은 줄(Y좌표)로 묶기 위한 보정 작업이다.
4. 핵심기술2 : 줄바꿈 재구성(_group_into_lines)
PDF 내부 데이터는 사람 눈에는 줄이 맞아 보여도, 실제로는 순서가 뒤죽박죽인 경우가 많다.
- 이 함수는 단어들의 Y좌표(top)를 비교한다.
- 현재 줄의 글자들과 다음 글자의 높이 차이가 line_gab(6) 이하라면 "아, 이건 같은 줄이구나" 하고 옆에 붙인다.
- 차이가 크면 "여기서부터는 다음 줄이네"하고 줄바꿈을 수행한다.
5. 마무리 청소 : _clean_text
추출된 텍스트에 포함된 불필요한 공백이나 특수 문자를 정리한다.
- \u00a0 : 웹에서 흔히 딸려나오는 공백 문자를 일반 스페이스로 바꾼다.
- re.sub(r" {3, }", " ", text) : 공백이 너무 길면(3칸 이상) 2칸으로 줄인다.
- re.sub(r"\n {3, }", "\n\n", text) : 엔터가 너무 많이 쳐져 있으면 최대 2번(한 줄 띄움)으로 제한한다.
왜 이렇게 하는 걸까?
RAG 시스템(AI가 문서를 읽고 답변하는 시스템)에서는 텍스트가 깨끗하고 줄바꿈이 정확해야 AI가 문맥을 오해하지 않고 정확하게 답변할 수 있기 때문이다.
요약하자면 위 코드들은
1. "위아래 8%는 필요 없으니까 잘라내라"(헤더/푸터 제거)
2. "글자들을 좌표 기준으로 다 가져와서 줄별로 세워라"(정밀한 텍스트 재구성)
3. "공백이나 엔터 너무 많은 건 보기 흉하니 정리해라"(텍스트 클리닝)
그러하다...
'스터디 > RAG' 카테고리의 다른 글
| [기술 전략] RAG의 한계, "왜 내 RAG는 답변이 정확하지 않을까?" 성능 향상을 위한 필수 체크리스트 (0) | 2026.05.12 |
|---|---|
| [코드리뷰] Refactoring 4 (0) | 2026.05.05 |
| [Python - Library] 화면 그리기 귀찮다... 나도 Gradio 써본다. (0) | 2026.05.03 |
| [Python - Library] BaseModel이 뭐다냐? (0) | 2026.05.03 |
| [개념짚고가기] FAISS 이 녀석은 왜 나타났나? (0) | 2026.04.23 |