6장. 문서 기반 챗봇
실습 1 — 여행 약관 PDF 읽어 보기
구름다리여행의 여행 약관(travel_terms.pdf)을 PyPDFLoader 로 읽고, 쪽마다 글자 수와 조항 수, 맨 위의 줄을 확인합니다. 약관은 4쪽, 조항 16개짜리로, 연습용으로 지어낸 가상 문서입니다. 쪽마다 되풀이되는 머리글이 있는지 찾아 두면 실습 2의 전처리(preprocessing, 쓰기 전에 다듬기)에서 지울 것이 정해집니다. 모델은 부르지 않으므로 API 키가 없어도 됩니다.
1단계. PDF 를 Document 목록으로 읽기
import warnings
# langchain-community 를 불러올 때 뜨는 "더 이상 관리하지 않는다" 경고("PDF 문서 읽기" 절)를 읽었으면 숨긴다
warnings.filterwarnings("ignore", category=DeprecationWarning)
from langchain_community.document_loaders import PyPDFLoader
# PyPDFLoader: pypdf 로 PDF 를 쪽마다 읽는 문서 로더. load() 는 쪽마다 Document 하나씩 돌려준다
loader = PyPDFLoader("travel_terms.pdf")
pages = loader.load()
print("쪽 수:", len(pages))
print("자료형:", type(pages[0]).__name__)
# metadata 사전에는 열쇠가 여럿 있으므로 쓸 것만 골라 찍는다
print("메타데이터 열쇠:", sorted(pages[0].metadata))쪽 수: 4
자료형: Document
메타데이터 열쇠: ['author', 'creationdate', 'creator', 'page', 'page_label', 'producer', 'source', 'title', 'total_pages']코드가 의미하는 것 PyPDFLoader(...).load() 는 챗봇이 읽을 책을 쪽마다 Document 로 바꿔 손에 쥐는 일이고, 이 목록이 색인 단계 전체의 출발점입니다. warnings.filterwarnings 는 경고를 읽고 판단한 뒤 출력을 깨끗이 두려는 것이지 문제를 고치는 것이 아닙니다. sorted(pages[0].metadata) 로 열쇠를 먼저 훑어 두면, 나중에 근거를 보여 줄 때 어느 열쇠(page, source)를 쓸지 고를 수 있습니다.
2단계. 쪽마다 글자 수와 조항 수 세기
import re
import pandas as pd
rows = []
for p in pages:
text = p.page_content
rows.append({
"쪽": p.metadata["page"] + 1, # page 는 0 부터 세므로 1 을 더한다
"글자 수": len(text),
"줄 수": len(text.split("\n")),
# 조항 제목은 "제5조 손님이 …"처럼 "제 + 숫자 + 조 + 빈칸"으로 시작한다
"조항 수": len(re.findall(r"제\d+조 ", text)),
})
table = pd.DataFrame(rows)
print(table.to_string(index=False))
print("모두:", int(table["글자 수"].sum()), "자 /", int(table["조항 수"].sum()), "조항") 쪽 글자 수 줄 수 조항 수
1 476 14 4
2 507 15 4
3 449 14 4
4 386 13 4
모두: 1818 자 / 16 조항코드가 의미하는 것 쪽마다 글자 수와 re.findall(r"제\d+조 ", text) 로 센 조항 수는 "쪽 하나를 그대로 검색 조각으로 써도 되는가"를 판단하는 숫자입니다. 한 쪽에 조항이 넷씩 들어 있으면, 쪽 단위로 검색했을 때 질문과 관계없는 조항 셋이 함께 프롬프트에 들어갑니다. metadata["page"] + 1 은 사람에게 보일 쪽 번호를 미리 맞춰 두는 버릇입니다.
3단계. 쪽마다 맨 위 두 줄 보기
from collections import Counter
for p in pages:
first, second = p.page_content.split("\n")[:2]
print(f"{p.metadata['page'] + 1}쪽 | {first} | {second}")
# Counter: 같은 값이 몇 번 나왔는지 센다. 쪽마다 첫 줄을 모아 세면 되풀이되는 머리글이 보인다
heads = Counter(p.page_content.split("\n")[0] for p in pages)
print(heads.most_common(1))1쪽 | 구름다리여행 여행 약관 (연습용 가상 문서) | 제1장 총칙과 예약
2쪽 | 구름다리여행 여행 약관 (연습용 가상 문서) | 제2장 취소와 환불
3쪽 | 구름다리여행 여행 약관 (연습용 가상 문서) | 제3장 여행 중의 규정
4쪽 | 구름다리여행 여행 약관 (연습용 가상 문서) | 제4장 보험과 그 밖의 사항
[('구름다리여행 여행 약관 (연습용 가상 문서)', 4)]코드가 의미하는 것 Counter(...).most_common(1) 로 쪽마다의 첫 줄을 세는 것은 "모든 쪽에 되풀이되는 글"을 사람 눈 대신 코드로 찾는 방법입니다. 이런 머리글은 질문과 상관없는데도 모든 조각에 섞여 들어가 임베딩을 서로 비슷하게 만듭니다. 여기서 찾은 글이 실습 2의 HEADER 가 되어 지울 대상이 됩니다.
4단계. 조항 제목 목록 모아 저장하기
titles = []
for p in pages:
# re.M(MULTILINE): ^ 가 글 전체의 처음이 아니라 줄마다의 처음을 뜻하게 한다
for m in re.finditer(r"^(제\d+조) (.+)$", p.page_content, flags=re.M):
titles.append({"조항": m.group(1), "제목": m.group(2), "쪽": p.metadata["page"] + 1})
titles = pd.DataFrame(titles)
print(len(titles), "개 조항")
print(titles.head(6).to_string(index=False))
table.to_csv("out_ch06_pages.csv", index=False, encoding="utf-8-sig")
print("저장: out_ch06_pages.csv")16 개 조항
조항 제목 쪽
제1조 목적 1
제2조 예약의 성립 1
제3조 예약 변경 1
제4조 결제 1
제5조 손님이 취소할 때 2
제6조 회사가 취소할 때 2
저장: out_ch06_pages.csv코드가 의미하는 것 re.finditer(..., flags=re.M) 로 줄 처음의 "제N조 제목"만 뽑는 것은 문서가 스스로 가진 짜임(차례)을 꺼내는 일입니다. 이 짜임을 알면 글자 수가 아니라 조항 경계에서 자를 수 있고, 조각마다 "제5조"라는 이름표를 붙일 수 있습니다. to_csv 로 남긴 쪽 정보는 문서가 바뀌었을 때 분량이 어떻게 달라졌는지 견줄 기준이 됩니다.
이 실습이 뜻하는 것
이 실습은 오픈북 시험 전에 책을 처음 훑어보며 쪽수와 단원을 세고, 쪽마다 맨 위에 무엇이 찍혀 있는지 보는 일과 같습니다. 책을 펴고(1단계), 쪽마다 분량을 세고(2단계), 맨 위 줄을 보고(3단계), 차례를 뽑아 둡니다(4단계). 여기서 책이 travel_terms.pdf, 펼친 쪽이 Document, 쪽 번호 메모가 metadata 입니다. 비유와 다른 점은, 사람은 머리글을 보자마자 "쪽마다 같은 글"인 줄 알지만 로더는 머리글도 본문과 똑같은 글로 꺼낸다는 것입니다.
| 단계 | 출력 | 뜻 |
|---|---|---|
| 1. 읽기 | 쪽 수 4, Document | 쪽마다 Document 하나입니다. metadata 에는 page, page_label, source, title 등 아홉 열쇠가 있습니다 |
| 2. 글자 수 | 476·507·449·386자, 조항 4개씩 | 모두 1,818자, 조항 16개입니다. 쪽 하나가 검색 조각으로 쓰기에는 깁니다 |
| 3. 맨 위 줄 | 같은 첫 줄 4번 | "구름다리여행 여행 약관 (연습용 가상 문서)"가 4쪽 모두의 첫 줄입니다. 둘째 줄은 장 제목입니다 |
| 4. 조항 제목 | 16개 조항 | 줄 처음의 "제N조 제목"만 골라 차례를 만들었습니다 |
한 줄로 줄이면, 나누기 전에 문서의 생김새(쪽 수, 분량, 되풀이되는 글, 짜임)를 먼저 확인한 실습입니다. 여기서 찾은 두 가지가 실습 2의 재료입니다. 하나는 쪽마다 붙은 머리글이고, 다른 하나는 "제N조"로 시작하는 조항의 짜임입니다. 머리글은 질문과 상관없는 글이라 지우고, 조항의 짜임은 자를 자리로 씁니다.
코드 설명
PyPDFLoader("travel_terms.pdf").load()는 쪽마다Document하나를 담은 목록을 돌려줍니다. 쪽 번호는metadata["page"]에 0 부터 들어 있습니다.re.findall(r"제\d+조 ", text)는 "제 + 숫자 한 개 이상(\d+) + 조 + 빈칸"을 모두 찾습니다. 빈칸까지 넣은 까닭은 본문 속의 "제5조의 당일 취소"처럼 다른 조항을 가리키는 말을 세지 않기 위해서입니다.Counter(...).most_common(1)은 가장 많이 나온 값과 그 횟수를 돌려줍니다. 4쪽 모두 같은 첫 줄이면 머리글입니다.re.finditer(..., flags=re.M)의^…$는 줄 하나의 처음부터 끝까지를 뜻합니다. 괄호로 묶은 두 부분이m.group(1)(조항 번호),m.group(2)(제목)입니다.out_ch06_pages.csv에는 쪽마다의 글자 수, 줄 수, 조항 수가 남습니다.