LLM 엔지니어링11장 · 폐쇄망 문서 파이프라인과 폐쇄형 RAG6 / 23← 이전목차다음 →MetaContents

11장. 폐쇄망 문서 파이프라인과 폐쇄형 RAG

문서 수집

이사할 때는 방마다 흩어진 짐을 모아 같은 크기의 상자에 담고, 상자마다 "부엌, 그릇, 깨짐 주의" 같은 라벨을 붙입니다. 사내 문서도 부서마다 html, 워드, 마크다운, 메모장, 표 파일로 흩어져 있습니다. 모두 읽어 한 꼴의 기록으로 담고 라벨을 붙이는 것이 수집입니다.

폴더를 훑어 문서 목록과 견주고, 형식마다 읽고, 경로와 제목과 글의 한 꼴로 맞추고, 부서와 등급과 판과 날짜 라벨을 붙이는 네 단계 흐름도

1번에서 폴더의 파일과 문서관리시스템에서 뽑은 문서 목록을 견줘 빠진 것이 없는지 봅니다. 2번에서 형식마다 맞는 방법으로 글을 뽑습니다. 3번에서 모두 경로, 제목, 글을 가진 기록으로 맞춥니다. 4번에서 문서 목록의 부서, 등급, 판, 작성일을 라벨로 붙입니다. 여기서 이삿짐이 파일, 같은 크기의 상자가 한 꼴의 기록, 라벨이 메타데이터입니다.

형식마다 읽는 법

형식생김새이 장에서 읽는 법흔히 쓰는 패키지
html태그 사이에 글. 메뉴와 꼬리말이 섞인다표준 라이브러리 html.parserBeautifulSoup
docx여러 XML 파일을 묶은 zip표준 라이브러리 zipfile 과 xmlpython-docx
md, txt글 그대로. 인코딩이 제각각utf-8 로 읽고 안 되면 cp949(없음)
csv표행마다 "열이름 값" 문장으로pandas
pdf쪽 단위로 그린 글이 장에서는 PDF 에서 뽑아 둔 글을 쓴다pypdf, PyMuPDF

폐쇄망에서는 패키지 하나를 들여오는 데도 절차가 필요하므로, 이 장은 되도록 표준 라이브러리로 읽습니다. 쓰는 패키지가 적을수록 들여올 것도 줄어듭니다.

워드 파일 들여다보기

docx 는 zip 파일입니다. 안의 word/document.xml 에 글이 <w:t> 태그로 들어 있습니다.

Python
import zipfile

# docx 를 zip 으로 열어 안에 든 파일 이름을 본다
with zipfile.ZipFile("intranet/hr/복리후생_안내.docx") as z:
    print(z.namelist())
    xml = z.read("word/document.xml").decode("utf-8")
print(xml[300:420])

코드에 나오는 함수

코드쉬운 뜻
zipfile.ZipFile(경로)zip 파일을 연다. 압축을 풀지 않고 안의 파일을 하나씩 읽을 수 있다
with … as z:블록이 끝나면 파일을 저절로 닫는다
z.namelist()zip 안에 든 파일 이름의 목록을 돌려준다
z.read("word/document.xml")zip 안의 그 파일을 바이트로 읽는다
.decode("utf-8")바이트를 UTF-8(Unicode Transformation Format 8-bit) 규칙으로 글자로 바꾼다
xml[300:420]긴 글에서 300번째부터 419번째 글자까지만 잘라 보인다

이 코드가 뜻하는 것

선물 세트 상자로 읽어 봅니다. 겉은 상자 하나지만 열면 칸마다 다른 물건이 들어 있고, 그 가운데 우리가 먹을 것은 한 칸에 있습니다. 여기서 상자가 docx 파일, 칸이 zip 안의 파일들, 먹을 것이 든 칸이 word/document.xml 입니다.

줄하는 일쉽게 말하면
with zipfile.ZipFile(...) as z:docx 를 zip 으로 연다선물 상자 뚜껑을 연다
print(z.namelist())안의 파일 이름을 찍는다칸마다 무엇이 들었나 본다
xml = z.read(...).decode("utf-8")본문 파일을 읽어 글자로 바꾼다먹을 것이 든 칸을 꺼낸다
print(xml[300:420])본문의 일부를 찍는다포장을 조금 벗겨 본다

교안을 만든 PC 에서 namelist() 는 ['[Content_Types].xml', '_rels/.rels', 'word/document.xml'] 세 개를 보였습니다. 이 장의 docx 는 make_data.py 가 필요한 파일만 넣어 만든 것이라, 워드에서 저장한 docx 보다 안의 파일이 적습니다. 잘라 본 글에는 <w:t xml:space="preserve">건강검진</w:t> 처럼 태그 사이에 글이 끼어 있습니다. 태그를 걷고 글만 모으는 일은 아래 작은 예와 실습 2에서 합니다.

작게 줄여서 눈으로 보기

메모리 안에 아주 작은 docx 본문을 만들고, <w:t> 안의 글만 문단별로 모아 봅니다. 문단 두 개의 글은 설명을 위해 지어낸 것입니다.

Python
import io
import zipfile
import xml.etree.ElementTree as ET

W = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}"
doc = ('<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>'
       '<w:p><w:r><w:t>건강검진</w:t></w:r></w:p>'
       '<w:p><w:r><w:t>해마다 </w:t></w:r><w:r><w:t>받습니다.</w:t></w:r></w:p>'
       '</w:body></w:document>')

buf = io.BytesIO()                                   # 파일 대신 메모리에 zip 을 만든다
with zipfile.ZipFile(buf, "w") as z:
    z.writestr("word/document.xml", doc)

with zipfile.ZipFile(buf) as z:
    root = ET.fromstring(z.read("word/document.xml"))
for p in root.iter(W + "p"):                         # 문단(p)마다
    print("".join(t.text for t in p.iter(W + "t")))  # 그 안의 글(t)을 잇는다
출력
건강검진
해마다 받습니다.
결과읽는 법
건강검진첫 문단(<w:p>)의 글 조각 하나다
해마다 받습니다.둘째 문단은 글 조각(<w:t>) 두 개로 나뉘어 있었다. 이어 붙여 한 줄이 되었다

io.BytesIO() 는 메모리 안의 가짜 파일이라 디스크에 아무것도 남기지 않습니다. ET(ElementTree, 요소 나무)의 fromstring 은 XML 글을 나무 꼴로 읽고, iter 는 그 이름의 태그를 모두 찾아 줍니다. W 는 w: 접두어가 가리키는 이름공간(namespace) 주소입니다. 워드는 한 문장도 글꼴이 바뀌는 곳마다 <w:t> 를 나누므로, 문단 안의 조각을 이어 붙여야 문장이 됩니다.

주의할 점

  • 폴더의 파일과 문서 목록이 늘 맞지는 않습니다. 목록에 없는 파일, 파일이 없는 목록 줄을 먼저 세어 봅니다.
  • 스캔한 PDF 처럼 글자가 아니라 그림인 문서는 글을 뽑을 수 없습니다. 글자 인식(OCR)이 따로 필요하므로, 수집할 때 글자 수가 0 에 가까운 문서를 골라 둡니다.
← 이전 절11장 · 바깥으로 나가는 연결 막기다음 절 →11장 · 문서 정제