from langchain_community.document_loaders import TextLoader
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import OpenAIEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.llms import OpenAI
from langchain.chains import RetrievalQA
from typing import List
from langchain.schema import Document
import os
여기는 임포트부터 뭐 처음 보는 것들이 많다.
일단 랭체인이 뭔지부터 알아보자.
LangChain(랭체인)
랭체인은 사실 그냥 LLM을 가지고
어플리케이션을 개발하는 프레임 워크임 (안드로이드 , ios 앱 말고 더 큰 의미의어플리케이션)
대표적인 기능은 이런 게 있음
프롬프트 관리: 효과적인 프롬프트를 작성, 최적화, 재사용하는 도구를 제공.
langchain.prompts
체인: 여러 LLM 또는 다른 구성 요소를 연결하여 복잡한 작업을 수행.
langchain.chains
에이전트: LLM을 의사 결정 엔진으로 사용하여 작업을 자동으로 수행.
langchain.agents
메모리: 대화나 작업 간의 정보를 유지하고 관리.
langchain.memory
인덱서와 벡터 저장소: 대규모 데이터셋에서 관련 정보를 효율적으로 검색.
langchain.vectorstores, langchain.embeddings
이렇게 5개가 대표적인 기능임
그리고노란색으로 칠해둔 애들은 이번 코드에 사용되는 애들임
근데 import문을 자세히 본 사람은 눈치챘겠지만
점마들 말고도 훨씬 많은 모듈을 씀.
+α
문서 로더: 다양한 형식의 문서를 로드하고 처리함.
langchain.document_loaders
텍스트 분할기: 긴 문서를 더 작은 청크로 분할함.
langchain.text_splitter
언어 모델: 다양한 왕짱큰 언어 모델(LLMs)을 사용할 수 있게 함.
langchain.llms
스키마: LangChain의 기본 구조를 정의함.
langchain.schema
이건 좀 다른 녀석들에 비해 설명을 들었을 때 이해가 직관적이지 못함.(내가 그랬음)
쉽게 말하면 LangChain의 "설계도"라고 생각하면 됨.
'문서'가 뭔지, '언어 모델'이 어떤 기능을 해야 할지 정의함.
새로운 기능을 추가할 때 기존 기능들과 잘 호환되도록 규칙을 제공하는 역할도 함.
지금은 이 정도로만 가볍게 알고 자세한 건 추후 설명
근데 렝체인 뒤에 _community가 붙어 있는데 이건 머임?
LangChain Community
요거는 걍 랭체인 커뮤니티에서 관리하는 통합 패키지임
주로 서드파티 도구, API, DB 등과의 연동을 담당함.
우리가 쓰는 Chroma같은게 서드파티 도구임.
요런거 쓸라믄 커뮤를 써야한다는것.
실험적이고 새로운 기능들이 먼저 이곳에서 많이 시도됨.
(LangChain의 카카오톡실험실 ㄷㄷ)
커뮤니티에서 검증된 기능들이 나중에 핵심 Langchain 패키지로 이동되기도 함.
암튼 이런 특징 때문에 더 많은 외부 서비스, 도구, 데이터베이스 등과의 통합을 제공하게 됨.
그래서 이 코드에서는 더 최신 기능 더 다양한 기능을 위해 커뮤니티 모듈을 사용함.
클래스 알아보기
from langchain_community.document_loaders import TextLoader
위에 언급한 문서 로더 기능을 사용하기 위한 준비 단계다.
langchain_community.document_loaders
랭체인 뒤에 커뮤니티가 붙었다는 점만 다름.
TextLoader
document_loaders 모듈에서
TextLoader라는 특정클래스를 '좀 쓸게요' 하고 가져옴.
인마는 텍스트 파일을 읽어 들이는 기능을 함.
예시 코드
from langchain_community.document_loaders import TextLoader
# 텍스트 파일 경로
file_path = "example.txt"
# TextLoader를 사용하여 파일 로드
loader = TextLoader(file_path)
# 문서 내용 읽기
documents = loader.load()
# 문서 내용 출력
print(documents)
이 작업을 하면 documents에 다음과 같은 documents 객체가 들어있을 거임.(출력 시)
documents = [
Document(
page_content="파일의 전체 내용...(매우 긴 문자열)",
metadata={
"source": "2024-10-16_17-20-28.txt"
}
)
]
documents(변수)는 리스트인데 그 안에
단 하나의 Document 객체가 포함되어 있을 거임. (적어도 우리가 분석하는 코드에서는)
Document 객체가 먼디용?
Document는 LangChain 라이브러리에 정의된 클래스임.
from langchain.schema import Document
우리가 여기서 임포트 해옴.
근데 안해도 작동 잘할거임 (내 코드 아님)
from langchain.schema import Document
class Document:
def __init__(self, page_content: str, metadata: dict = None):
self.page_content = page_content
self.metadata = metadata or {}
이렇게 생김.
document.page_content 이런 식으로 쓸 수 있는 거임.
정리
TextLoader
얘는 그냥 파일 읽을 준비만 하는클래스임
경로랑 인코딩 정보만 저장하고 실제로 파일 안 읽음
load()인마가 진짜로 파일을 읽는 메소드임
(TextLoader의 인스턴스 메소드.)
촤라라락 읽고 읽은 내용을 Document 객체로 변환
from langchain_community.vectorstores import Chroma
위에 설명해서 이제 이해가 쉬울 텐데
langchain_community.vectorstores 라는 모듈에서
Chroma를 쓰겠다는 거임
그렇다는 건 당연히 LangChain에서 지원하는
다른 벡터 DB도 쓸 수 있다는 뜻! (벡터 DB 개념은 바로 밑에 설명)
짧게 살펴보고 가자면 이런 게 있다
Chroma: 간단하고 빠르게 설정할 수 있어 프로토타이핑에 좋음.
FAISS: 대규모 데이터셋에서 빠른 검색이 필요할 때 유용함. (페북 AI에서 만듦)
Pinecone: 클라우드 기반 설루션이 필요하고 확장성이 중요할 씀.
Qdrant: 고성능이 필요하고 복잡한 필터링 기능이 필요할 때 씀.
Weaviate: 그래프 데이터 구조와 벡터 검색을 결합하고 싶을 때 유용함.
Milvus: 대규모 분산 시스템에서 벡터 검색이 필요할 때 씀.
우리도 간단한 설정과 빠른 프로토타이핑을 위해
Chroma를 공부하는 거임.
이 코드 만든 아저씨도 그 목적으로 쓴 거일 테고
FAISS랑 Chorma는 인터페이스가 유사해서
모델만 바꿔 끼우면 쉽게 교체 가능 할지도?
(하고 싶은 사람 라이브러리 설치하고 실행 후 후기점.)
결론 :Chroma는 빠르다
잠만 근데 벡터 DB가 머임?
벡터 DB
벡터는 숫자들의 리스트임.
[1,2,3]뭐 막 일케 생겼음.
인공지능에서는 텍스트나 이미지 같은 복잡한 데이터를
저 위에 예시처럼숫자 리스트로 표현함.
그래서 저런 애들이 모여있어서벡터.데이터베이스. 임
그냥 텍스트 파일에서 정보 찾으면 안 됨? 왜 저렇게 변환함?
1. 일단 속도부터 차이가남.
예를 들어 :
1GB 크기의텍스트 파일에서 정보를 찾는 데몇 분이 걸릴 수 있지만,
벡터 DB를 사용하면 같은 양의 데이터에서몇 초만에 찾을 수 있는겨.
근데 그냥 속도만 빠르다고 쓰는 게 아님.
2. 의미 기반 검색
벡터 DB는 검색을 할 때 crtl + f 로 키워드 찾듯이 정보를 찾는 게 아님.
훨씬 지능적이게 검색을 함.
예를 들어 :
"지구 온난화의 영향" 을 검색한다면
"기후 변화로 인한 해수면 상승"같은 관련 정보도 찾아옴.
키워드가 정확히 매칭되지 않아도 문맥을 고려해서 찾아옴.
3. 유사성 측정
벡터 DB는 정보 간의 유사성을 수치화함.
뭔 말이고?
"사과의 영양성분" 이라는 질문에 대해
가장 관련 높은 정보부터 순서대로
사과의 비타민 함량
사과의 식이섬유
과일의 일반적인 영양가
요로코로미 찾아온다는 거임.
4. 다차원 데이터 처리
텍스트가 아닌 이미지나 소리를 검색해와야 하면 어칼 거임.
벡터 DB는 이미지, 소리도 컴터가 이해하는 방식으로 저장을 할 수가 있음.
결론:
벡터 DB를 쓰면 더 지능적이고 효율적인 정보 검색 처리가 가능함.
특히 데이터가 킹왕짱 커지거나 복잡한 질문을 답할 때 매우 유용해짐.
from langchain_community.embeddings import OpenAIEmbeddings
from은 이제 다 알 테니 생략하고
먼저 embeddings가 뭔지 알아보자
임베딩(embedding)
위에 언급했듯이
데이터들은 벡터 DB에 들어갈 때
숫자로 변환되어 벡터가 되어 들어감.
데이터 -> 벡터(숫자) -> 벡터 DB
근데 여기서 생각해봐야 할게
누가 데이터를 벡터로 바꿔주냐 이거임.
임베딩 모델 : 저요
ㅇㅇ 임베딩 모델이 그걸 하는 놈임.
얘가 대량의 데이터로 학습되어서 데이터의 의미와 관계를 포착함.
그리고 그걸 숫자로 표현하는 거임.
여기서 하나 더 알고 가면 좋은 점 있는데
이 벡터들은 의미나 관계를 다차원 공간상의 위치로 나타냄.
ㄷㄷ 뭐소리고
대충 후려쳐서 말하면
단어끼리 의미가 가까우면 서로 가까운데 위치함.
쉬운 예시로 ㄱ
3차원 공간이 있다고 생각해 보자
[2,3,1]이런 좌표가 있음.
이게벡터임
계란말이라는 단어가 임베딩되어벡터 [2,3,1]이 된 거임
그럼 이벡터 [2,3,1](계란 말이)는
3차원 공간에서 좌표
x=2
y=3
z=1
위치에 있는 거임
근데 갑자기 계란찜이 임베딩 되어서 들어옴.
그럼 임마는벡터 [2,3,1](계란 말이)와 가까운
x=3
y=3
z=1
계란찜 [3,3,1]쯤에 위치하게 되는 거임
둘 다 계란으로 만들었고 음식이니깐 유사성이 높아서 가까운데 위치하게 됨.
결국벡터에 들어가 있는숫자들은
데이터의 특성을 고차원 공간에서 표현하는 수치들이라고 보면 됨
이 차원은 천차만별임 GPT-3는 무슨 12,000 이상 차원까지 사용하고
몇백 따리차원도 있고 그럼.
근데 재밌는 건 인간이 각 차원에 대해서 정확한 의미는 해석할 수 없음
일단 12,000차원 이상의 공간을 직관적으로 이해하기 어렵고
임베딩 모델이 인간이 명시적으로 정의한 규칙이 아니라
블랙박스 안에서 데이터 지지고 볶으면서 지 꼬운 대로
학습한 패턴을 기반으로 작동해서 그럼
이게 인터레스팅 하다면
AI의 '설명 가능성(explainability)' 문제를 찾아보도록
다시 코드로 돌아와서
from langchain_community.embeddings import OpenAIEmbeddings
임베딩을 아니깐 이제 이 코드는 그냥 귀요미 중에 귀요미 코드.
embeddings패키지에서 OpenAIEmbeddings모델 꺼내 쓰겠다는 거임.
그게 끝임.
참고로 이런 애들도 쓸 수 있
HuggingFaceEmbeddings (킹깅갓이스)
VertexAIEmbeddings (구글이 만듦)
CohereEmbeddings (다국어 처리 잘함)
TensorflowHubEmbeddings
참고로OpenAIEmbeddings얘는 유료임.
API 키 필요함
참고로 OpenAI는 비영리 단체임.
(비영리 단체도 수익산업 가능함. 임마들이 증거임 (실제로 가능함 ㅋ)
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter 에서 RecursiveCharacterTextSplitter 를 꺼내온다.
text splitting
걍 말 그대로 텍스트 분할임.
RecursiveCharacterTextSplitter
ㄹㅇ 길다 이름
번역하자면 재귀적 문자 텍스트 분할기인데.
임마가 어떤 식으로 동작하냐면 (split_text 메소드 기준)
1. 먼저 전체 텍스트를 구분자(줄바꿈, 마침표 같은 거)를 기준으로 크게 나눔.
2. 그리고 이게 설정한 최대 길이(chunk_size)를 초과하면 또다시 더 작은 구분자로 나눔.
이 과정이재귀적이라Recursive가 붙은 거임
3. 모든 부분이 지정된 길이 이하가 될 때까지 계속함.
쉬운 예시를 인공지능에게 짜달라고 해봤음
청크 사이즈가 100일 때의 출력
청크 1 (56자): 1. 인공지능은 컴퓨터가 인간의 지능을 모방하는 기술입니다.
--------------------------------------------------
청크 2 (75자): 2. 머신러닝은 데이터로부터 학습하여 성능을 향상시키는 AI의 한 분야입니다.
--------------------------------------------------
청크 3 (62자): 3. 딥러닝은 인간 뇌의 신경망을 모방한 알고리즘을 사용합니다.
--------------------------------------------------
청크 4 (67자): 4. 자연어 처리는 컴퓨터가 인간의 언어를 이해하고 생성하는 기술입니다.
--------------------------------------------------
청크 사이즈 50일 때의 출력
청크 1 (48자): 1. 인공지능은 컴퓨터가 인간의 지능을 모방하는
--------------------------------------------------
청크 2 (8자): 기술입니다.
--------------------------------------------------
청크 3 (49자): 2. 머신러닝은 데이터로부터 학습하여 성능을 향상시키는
--------------------------------------------------
청크 3 (26자): AI의 한 분야입니다.
--------------------------------------------------
청크 4 (50자): 3. 딥러닝은 인간 뇌의 신경망을 모방한 알고리즘을
--------------------------------------------------
청크 5 (12자): 사용합니다.
--------------------------------------------------
청크 6 (49자): 4. 자연어 처리는 컴퓨터가 인간의 언어를 이해하고
--------------------------------------------------
청크 7 (18자): 생성하는 기술입니다.
--------------------------------------------------
직관적이다 그죠잉.
암튼 이런 식으로 텍스트 나누는 재귀뭐시기 클래스 가져다 쓴다는 코드임.
근데 데이터를 왜 나누는 거임? 청크는 또 머임;;;
청크(chunk)
말 그대로덩어리임.
근데데이터 분야에서청크는'관리 가능한 부분', '논리적 단위'를 나타냄.
"예림이 그 패 봐봐 혹시 장이야?"
이거를
예림이 / 그 패 봐봐 / 혹시 / 장이야?
이렇게 나누면청크로 나눈 건데
예/ 림이그 / 패봐 /봐혹 /시 / 장이 /야?
이렇게 나누면 청크로 나눴다고 보기 어렵다는 거임.
데이터를 나누는 이유
1. 모델이 입력 제한이 있는 경우가 있음.
토큰제한이 있어서 긴 문서를 못 넣는 경우임.
그걸 그냥 나눠서 박아버리는 거임.
2. 메모리 효율성
청크로나눠서 넣으면 메모리 사용이 최적화됨.
3. 병렬 처리
텍스트를 청크로 나눠서 동시에 처리시키는 거임.
그럼 시간 단축 ㄱㅇㄷ
30분 카레를 3분 카레 10개로 나눠서 전자레인지 10개에 돌리는 거임.
4. 정보 검색 개선
문서를 작은 청크로 나누면 특정 정보를 더 정확하고 빠르게 검색함
계란말이라는 단어를 찾아야 할 때
백과사전 다 뒤져서 단어 찾는 거보다.
백과사전을 ㄱ / ㄴ / ㄷ 이런 식으로 찢어놓는다면
그냥 ㄴ 청크 들고 가서 거서 찾으면 더 빠름.
5. 컨텍스트 유지
RecursiveCharacterTextSplitter와 같은 고급 분할기를 사용하면
의미 있는 단위(예: 문단, 문장)로 텍스트를 나눌 수 있어
각 청크 내에서 컨텍스트를 유지할 수 있음.
이러면 예림이가 패를 볼 수가 있음
아귀가 말리겠지만.
from langchain_community.llms import OpenAI
걍 OpenAI 모델 쓴다는 거
이것도 뭐 당연히 여러 가지 모델 쓸 수 있음.
Claude도 쓸 수 있고 뭐 라마도 쓸 수 있음.
llm = OpenAI(model_name="킹왕짱좋은모델")
참고로 이런 식으로 모델 선택 가능함.
gpt-4o 나 mini 같은 거 아마
아무것도 안 적으면 gpt-3.5-turbo-instruct 이 모델을 쓰는 걸로 알고 있음.
알아서 찾아보도록
from langchain.chains import RetrievalQA
chains라는 모듈에서RetrievalQA를 가져옴.
여기서는chains 말고 RetrievalQA를 먼저 설명함
RetrievalQA(Retrieval Question Answering)
임마는 문서에서 정보를 검색하고 질문에 답변하는 AI 모델을 만드는 도구임.
Retrieval 말 그대로 검색한다.라는 뜻
RetrievalQA는 여러 단계를 연결함.
문서 검색 (Retrieval)
관련 정보 추출
질문 이해
답변 생성
이게 단계 하나하나를 체인처럼 엮어서 동작시켜서chain임.
더 자세히
문서 인덱싱 (Document Indexing):
먼저, 사용 가능한 모든 문서를 작은 조각(chunk)으로 나눔.
각 조각을 벡터(숫자 배열)로 변환. ㅇㅇ맞음 '임베딩(embedding)'
이 벡터들을 효율적으로 검색할 수 있는 데이터베이스에 저장. ㅇㅇ 맞음 벡터 DB
사용된 친구들
문서 조각화: RecursiveCharacterTextSplitter
임베딩: OpenAIEmbeddings
벡터 데이터베이스: Chroma
질문 처리 (Query Processing):
사용자의 질문도 같은 방식으로 벡터로 변환.
임베딩: OpenAIEmbeddings (문서와 동일한 임베딩 모델 사용)
유사도 검색 (Similarity Search):
질문 벡터와 가장 유사한 문서 조각들을 찾음.
이 과정은 벡터 간의 '거리'를 계산함.계란찜 / 계란말이
Chroma 벡터 데이터베이스의 내장 검색 기능
관련 정보 추출 (Relevant Information Extraction):
가장 유사한 몇 개의 문서 조각을 선택.
Chroma의 검색 결과를 바탕으로 RetrievalQA 내부에서 처리
답변 생성 (Answer Generation):
선택된 문서 조각들과 원래 질문을 LLM에 입력으로 제공.
LLM은 이 정보를 바탕으로 답변을 생성.
LLM: OpenAI (코드에서 OpenAI() 사용)
각 단계에서 사용된 친구들을 주목해야 됨.
RetrievalQA 혼자 하드캐리하는 게 아님.
전체 과정은 RetrievalQA 클래스가 조율하지만
각 컴포넌트들을 연결하여 각 단계의 입력이 다음 단계의 출력으로
사용되는 전체 파이프라인을 구성함.
그 연결된 모습이 마치 체인 같다는 거임.
그래서Chain임
사실 이게 끝임.
RetrievalQA결과
이게 이 코드의 목적임.
정리 :
내가 갑자기 어디 멀리 가야겠다싶어서
오토바이를 만들어야겠다 생각함.
그래서 막바퀴랑안장이랑엔진이랑 다 들고 옴
근데 생각해 보니깐 나는오토바이를 만들 줄 모르는 거임 (바보)
근데 갑자기 뒤에서 어떤오토바이 장인이 툭툭 치면서 한 마디 함.
장인 :필수 부품은 다 있구마바퀴는 여기 두고 엔진은 저기두고 어쩌구 저쩌구
나: 네? 네.
장인: (뚝딱뚝딱)오토바이 완성
나: 야호
그냥 장인이 물건만 두라는 데로 뒀더니 오토바이가 만들어짐.
멀리 가야겠다=사용자에게 질문을 받고 알맞은 답을 하자
필수 부품은 다 있구마바퀴, 엔진, 안장 등=LLM, retriever, chain_type 등의 컴포넌트
바퀴는 여기 두고 엔진은 저기 두고=RetrievalQA구문
이것들을 가져온 사람=코드를 작성하는 개발자(나)
조립하는 방법을 모르는 상황=RetrievalQA의 복잡한 설정을 직접 하기 어려운 상황
장인=RetrievalQA.from_chain_type 메서드
오토바이=완성된 RetrievalQA 객체
이제 나는 바퀴랑 엔진 같은 필수 부품만 구해서
장인이 넣으란데 넣으면 오토바이가 만들어지는 것을 알게 됨.
이제 나는 LLM, retiever 같은 컴포넌트를
RetrievalQA.from_chain_type구문에 맞게 넣으면 사용자 질문에 알맞은 답변이 나오는 것을 알게 됨.
`RetrievalQA` 시스템 설정하는 거임. 이거 질문에 답변하는 AI 시스템임. `OpenAI()`는 OpenAI의 언어 모델 사용한다는 의미임. `chain_type="stuff"`는 사용할 검색 방법 지정하는 거임. `retriever=self.vectordb.as_retriever()`는 앞서 만든 벡터 데이터베이스를 검색기로 사용한다는 의미임.
암튼 토픽이 {topic}에 전달되고 위키피디아 API는 이를 json 형식으로 데이터를 반환함. 우리가 url 뒤에 format=json 라고 적어놔서 그럼
response = requests.get(url)
requests.get(url)로 해당 URL에 GET 요청을 보냄. 이게 response에 들어감.
현재 response는 문자열임.
마지막
data = response.json()
data = response.json()의 뜻은 받은 응답을 JSON 형식으로 파싱 하라는 거임.
그게머임?
python한테 지금 response(현재 문자열)이 사실은 json 형식이라고 속삭이는 거임.
그러면 python이 "아하!"하고 좋다고 이건 json이구나 하고 json형식에 맞춰서 지가 다루기 쉬운 파이썬 객체로 파싱해옴.(아마 딕셔너리로)
예시: 위는 파싱 전 아래는 파싱 후
텍스트 -> 딕셔너리가 됐음.
# API 응답 (response.text로 볼 수 있는 내용) 지금 텍스트임 그냥
'{"parse":{"title":"Python","pageid":23862,"text":{"*":"<div class=\"mw-parser-output\">..."}}}'
# response.json()을 통해 변환된 Python 객체 json형식에 맞춰서 딕셔너리로 맹듬
{
"parse": {
"title": "Python",
"pageid": 23862,
"text": {
"*": "<div class=\"mw-parser-output\">..."
}
}
}
def pull_content(self, topic: str):
참고: 이건 정말 너무 당연한 이야기인데 왕왕왕 초보를 위해 적자면 클래스 내부에 이렇게 첫 번째 파라미터에 self가 있으면 메소드라고 생각하면 됨. 역할도 순수 메소드 같이함.
오잉 메소드 안같은 메소드는 머임? 후반부 정적 메소드 참조
slef는 객체 자신을 참조한다는 뜻인데 pull_content 자신을 참조한다는 게 아니라. damm.pull_contnet에서 damm이 자신임 damm을 참조한다는 거
문제의 혼파망 코드.
if data.get("error", None) is None:
html_content = data["parse"]["text"]["*"]
redirect_link_pattern = r'<div class=\"redirectMsg\">.*?<a href=\"([^"]*)\" title=\"([^"]*)\">'
match = re.search(redirect_link_pattern, html_content, re.DOTALL)
if match:
redirected_topic = match.group(2)
url = f"https://en.wikipedia.org/w/api.php?action=parse&page={redirected_topic}&format=json"
response = requests.get(url)
data = response.json()
self.wikipedia[topic] = data["parse"]["text"]["*"]
else:
if self.wikipedia.get("error", None) is None:
self.wikipedia["error"] = []
self.wikipedia["error"].append(topic)
return self
이 코드 해석 이전에 알아야 할게 많음. 일단 리디렉션에 대해 알아함.
리디렉션(혹은 리다이렉션 알아 부르쇼)은 웹 페이지나 URL의 자동전환을 의미함.
걍 쉽게 말하면
"USA" 를 치든 "United States of America"를 치든 결국 같은 페이지로 연결된다는 것을 의미함.
"NYC"나 "New York City" 같은 페이지로 연결되고
"color"와 "colour" 같은 철자 차이를 처리하기도 함
결론 : 리디렉션 = 한 페이지에서 다른 페이지로 자동으로 사용자를 이동시키는 기능임.
근데 이 리디렉션 페이지는 쓸모 있는 내용이 없음 걍 실제 내용이 있는 페이지로 이동하는 기능을 가진 페이지임.
그래서 이 코드 만든 사람은 실제 페이지가 아닌 리디렉션 페이지가 크롤링되는 것을 방지하려고 로직을 짰는데 이게 위 코드임. (결국 리디렉션 페이지도 크롤링을 해오긴한다.)
코드의 주요 목적 :
리디렉션 페이지가 아닌 실제 내용이 있는 페이지를 찾아 삔다.
찾은 실제 페이지의 내용을 추출해 삔다.
추출한 내용을 'self.wikipedia' 딕셔너리에 저장해삔다.
현재 data에는 json이 파싱 된 파이썬 객체(아마도 딕셔너리)가 들어있음.
data = response.json()
근데 어떤 값이 들어가 있는지 모름.
예측 가능한 응답.
크게 2가지로 나눌 수 있음. 1. 성공적인 응답. 2. 에러
에러부터 알아보자
우리가 요청을 보냈을 때 API가 요청을 처리할 수 없거나 기분이 나쁘면 에러를 보냄.
data = {
"error": {
"code": "missingtitle",
"info": "The page you specified doesn't exist.",
"*": "See https://en.wikipedia.org/w/api.php for API usage. Subscribe to the mediawiki-api-announce mailing list at <https://lists.wikimedia.org/postorius/lists/mediawiki-api-announce.lists.wikimedia.org/> for notice of API deprecations and breaking changes."
}
}
4) 첫 번째 ([^"]*)로 "/wiki/Artificial_intelligence"를 캡처.
주소를 적어둠
5) title=" 부분을 찾아 제목의 시작을 인식함.
"이 주소의 실제 이름은요" 표시를 찾는다.
6) 두 번째 ([^"]*)로 "Artificial intelligence"를 캡처함.
실제 이름을 적어둠.
7) 메모 끝.
리디렉션 data가 어떻게 생겼는지 본다면 더 이해하기 쉽다.
<div class="redirectMsg">
리다이렉트 주의: 이 문서는 <a href="/wiki/Happy_Cat" title="Happy_Cat">Happy Cat</a> 문서로 넘겨주고 있습니다.
</div>
요래 생김
여기에 적혀 있는 url(href)과 title이 실제 문서 주소와 문서 제목인 거임.
리디렉션 페이지에는 위와 같이 "어? 여기 말고 절로 가셔야되영. 주소는 이거(url)고 금마 이름은 이거(title)임." 라는게 적혀 있음.
우리가 지금 난리를 치면서 패턴을 집어넣는 게 저 몇 마디 듣자고 하는 거임.
1) ~ 7) 번을 data와 함께 비교하며 읽으면 이해 가능.
중요한 건 지금 해당 코드가 1) ~7)의 과정은 아님 다음 코드를 위한 설명이고 엄밀히 말하면 해당 코드는 그냥 문자열을 변수에 저장한 것뿐임.
참고 : r 접두사의 목적은 해당 문자열을 "raw string"으로 취급하라고 Python에게 지시하는 거
그럼 어케되냐 1) 백슬래시(\)를 특별히 처리하지 않고 그대로 사용할 수 있음. 2) - 일반 문자열: \n은 새 줄, \t는 탭 등으로 해석되는데 - Raw 문자열: 그냥 있는 그대로 두 개의 문자로 취급함. 편견 없는 사나이임.
match = re.search(redirect_link_pattern, html_content, re.DOTALL)
1) re는 맨 위에 import란에서 설명했듯이 문자열 패턴 매칭을 위한 도구툴임. 2) search는 re의 모듈 함수임 문자열 전체에서 패턴과 일치하는 첫 번째 위치를 찾아버림. 구문 : re.search(pattern,string,flags)
이걸 위 코드에 적용하면 html_content에서 redirect_link_pattern과 일치하는 부분을 찾는 거임. 이게 내가 이전 코드에 언급한 1) ~ 7) 과정을 실행하게 되는 부분임 re.serach가 그걸함
그럼 re.DOTALL은 먼데용?
re.DOTALL은 "." 이 줄 바꿈 문자를 포함한 모든 문자와 매치되도록 함.
뭔 소리고 싶겠지만
일단 이걸 알아야 됨.
정규표현식에서 .점은 ㄹㅇ 문자가 아니라 의미를 가진 메타 문자임
. (점):
의미: 줄 바꿈을 제외한 모든 단일 문자와 매치
예: a.c는 "abc", "a1c", "a@c" 등과 매치.
실제 문자 .점을 찾고 싶으면 백슬래시 \. 이렇게 써야됨
암튼 의미에 줄바꿈을 제외한 모든 단일 문자와 매치가 중요함.
근데 re.DOTALL를 쓰면 이게 줄 바꿈도 포함한이 돼버림
확실한 예시를 보고 가자.
<div class="redirectMsg">
리다이렉트 주의: 이 문서는 다음으로 넘어갑니다 # 여기서 줄바꿈 발생
<a href="/wiki/스팀덱" title="스팀덱">
아 스팀덱 사고 싶다. # 여기서도 줄바꿈 발생
</a>
</div>
# 리다이렉션 링크를 찾기 위한 정규 표현식 패턴
redirect_link_pattern = r'<div class=\"redirectMsg\">.*?<a href=\"([^"]*)\" title=\"([^"]*)\">'
줄 바꿈 발생 주석에 주목.
re.DOTALL 없이 검색
re.DOTALL을 사용하지 않으면, '.*?'가 첫 번째 줄 바꿈에서 멈춤 따라서 <a href=...> 부분을 찾지 못함
re.DOTALL을 사용하여 검색
re.DOTALL을 사용하면, '.*?'가 줄 바꿈을 포함한 모든 문자와 매치됨 따라서 <a href=...> 부분을 정상적으로 찾을 수 있음
이러면 지금 match 안에 패턴을 못 찾은 경우 none 패턴을 찾은 경우 패턴에 맞는 값(re.Match 객체)이 들어가 있게 된다.
결국 다시 천천히 생각해 보면 none이 나왔다는 것은 이게 실제 페이지라는 것을 의미한다.
첫 번째 if 문에서 error가 아님을 확인했기 때문에 이제 가능성은 실제 페이지 / 리디렉션 페이지 뿐인데
리디렉션 페이지에는 패턴이 있어서 패턴에 맞는 값이 들어가기에 none은 나올 수 없기 때문이다.
API에서 받아온 HTML 구조와 패턴이 일치하지 않으면 리디렉션 페이지여도 none이 나올 수 있지 않나요?
그럴 수 있다. 근데 그건 알아서 해결하자 해석: 나도 모름
if match:
redirected_topic = match.group(2)
url = f"https://en.wikipedia.org/w/api.php?action=parse&page={redirected_topic}&format=json"
response = requests.get(url)
data = response.json()
if match: 부터
다들 알고 있겠지만 match에 패턴에 해당하는 객체가 들어있으면 True라 if문에 진입하고 none이 들어있으면 False라서 걍 if문을 무시해 버린다.
참고 : 이런 분류를 truthy / falsy 라고 한다. 이게 무슨 뜻일까?
truthy는 대충 봐도 True로 취급되는 값일 것 같고 falsy는 대충 봐도 False로 취급되는 값처럼 쓰일 것 같다.
그리고 그게 맞다.
truthy 값:
0이 아닌 모든 숫자 (예: 1, -1, 3.14)
비어있지 않은 문자열 ("hello", "0")
비어있지 않은 리스트, 튜플, 딕셔너리 등 ([1, 2], (3,), {"key": "value"})
True 자체
대부분의 객체 인스턴스
falsy 값:
0, 0.0
빈 문자열 ("")
None
False
빈 컨테이너 ([], {}, set())
그냥 falsy 값이 아니면 모두 truthy다.
즉 리디렉션 페이지를 긁어왔다면 match에 패턴에 맞는 객체가 들어왔을 거고 해당 if문에 진입하게 된다
실제 페이지를 긁어왔다면 match에 none이 들어갔을 거고.. 이제 알져?
redirected_topic = match.group(2)
group 메소드
group 메소드는 re.Match 객체의 전용 메소드다. re.Match 객체는re.search(), re.match(), re.fullmatch() 함수의 결과로 반환됨. (더 있을 수도 있음 re.finditer() 이런 거)
우리는 re.search()를 사용했기 때문에 re.Match 객체가 match 변수에 들어가 있음.
고로코로미 match.group(2) 메소드를 쓸 수 있다 이 말이야.
참고 : python 객체에는 group 메소드 못씀. 해당 메소드가 없어서 AttributeError 발생함 AttributeError 뜻 : 난 그런 거 모릅니다.
일단 더 나은 이해를 위해 변수에 값들이 어케 저장되어 생겨먹었는지 확인하고 뽑아내보자.
import re
# 스팀덱에 대한 HTML 문자열 (위키피디아 리다이렉트 메시지를 모방)
html_content = '<div class="redirectMsg">리다이렉트 페이지</div><a href="/wiki/Steam_Deck" title="Steam_Deck">Steam Deck 사고 싶다.</a>'
# 리다이렉트 링크를 찾는 패턴
redirect_pattern = r'<div class=\"redirectMsg\">.*?<a href=\"([^"]*)\" title=\"([^"]*)\">'
# re.search()를 사용하여 패턴 검색 (re.DOTALL 플래그 썼음 알죠?)
match = re.search(redirect_pattern, html_content, re.DOTALL)
if match:
print("히히 Match 객체 발사")
print("Match 객체 전체:")
print(match)
print("\nMatch 객체의 주요 정보:")
# group() 메소드 사용
print("전체 매치 (group(0)):", match.group(0))
print("href 값 (group(1)):", match.group(1)) #주목
print("title 값 (group(2)):", match.group(2)) #주목
# 이런것도 있음
print("매치 시작 위치:", match.start())
print("매치 끝 위치:", match.end())
print("매치 범위:", match.span())
else:
print("매치가 뭐고?.")
출력값(이게 중요함) :
히히 Match 객체 발사
Match 객체 전체:
<re.Match object; span=(0, 84), match='<div class="redirectMsg">리다이렉트 페이지</div><a href="/w>
Match 객체의 주요 정보:
전체 매치 (group(0)): <div class="redirectMsg">리다이렉트 페이지</div><a href="/wiki/Steam_Deck" title="스팀덱">
#여기가 중요함 여기를 보소
href 값 (group(1)): /wiki/Steam_Deck #주목
title 값 (group(2)): Steam_Deck #주목
#여기를 보라고 여기
매치 시작 위치: 0
매치 끝 위치: 84
매치 범위: (0, 84)
우리가 캡처를 이용해서 href값과 title값을 뽑아냈음.
우리가 적어놓은 패턴에 ([^"]*) 이게 있는데 이게 캡처임. 정확히는 캡처 그룹. 캡처 그룹에는 순서대로 번호가 매겨짐.
너 1번 너 2번. 이런 식으로 (0번은 전체 캡처 모음임)
우리는 첫 번째로 herf 값을 두 번째로 title 값을 뽑아옴
그래서 match.group(2) 하면 두 번째 캡처 그룹 title title : Steam_Deck Steam_Deck이 나오는 거 (실제 페이지 제목)
def merge_content_to_plaintext(self):
content = ""
for i in self.wikipedia.values():
# don't include error list
if isinstance(i, list):
continue
content += Wikipedia.html_to_plaintext(i.replace('\n', '')).replace('\n', '')
return content
메소드 목적 :
Wikipedia 객체에 저장된 모든 내용(오류 목록 제외)을 하나의 일반 텍스트로 병합.
HTML 형식의 내용을 일반 텍스트로 변환하고, 모든 줄 바꿈을 제거하여 연속된 텍스트로 만드삐.
for i in self.wikipedia.values():
딕셔너리의 모든 값들에 대해 반복문 시작 후 딕셔너리의 모든 값을 반환함.( values() )
# don't include error list
if isinstance(i, list):
continue
content += Wikipedia.html_to_plaintext(i.replace('\n', '')).replace('\n', '')
return content
이건 주석만 봐도 알 수 있는데 에러 목록은 제외하고 정보들은 병합하는 과정임.
if isinstance(i, list): continue
만약 i 가 list면 그냥 건너뛰라는 거임. 아마 에러 목록이 리스트로 저장되어 있나 봄.
정적 메소드 데코레이터를 사용하면 메소드를 함수처럼 쓸 수 있음. 인스턴스 없이 직접 클래스를 통해 호출 가능. (여기서 제일 높은 사람 불러와.) self 매개변수도 안 받음. 그래서 클래스나 인스턴스 수정도 안 함.
@staticmethod
def to_txt(text: str, path: str = None):
if path is None:
# 콜론을 제거하고 공백을 언더스코어로 대체
path = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
with open(f"{path}.txt", "w", encoding='utf-8') as f:
f.write(text)
f.close()
return f"saved to {path}.txt"
이 코드는 뭐 설명할 게 없음 그냥
1. 사용자가 사용할 때 path를 지정하면 사용자가 지정한 이름으로 저장되고 2. 지정 안 하면 (path is None) 자동으로 현재 날짜 시간으로 파일 이름을 생성함.
이 부분이 이해가 안 되면 파일입출력을 한번 보고 오면 "와따 이레 쉬운 코드였어유?" 할 거임.
if __name__ == "__main__":
wiki = Wikipedia()
for algorithm in _ALGORITHMS:
wiki.pull_content(algorithm)
# print(wiki.topics)
# print(wiki.html_to_plaintext(wiki.get_content("Isolation Forest")))
# print(wiki.to_txt(wiki.html_to_plaintext(wiki.get_content("Isolation Forest"))))
print(wiki.merge_content_to_plaintext())
print(wiki.to_txt(wiki.merge_content_to_plaintext()))
대망의 마지막 코드 블럭.
그냥 우리가 지금까지 분석한 코드들을 사용하기만 하는 영역이라 큰 흐름만 알면 됨.
큰 흐름
1)
if __name__ == "__main__":
걍 스크립트가 직접 실행될 때만 동작한다는 뜻임.
좀 더 쉽게 말하면 "이 파일이 직접 실행이 되고 있는겨?"를 확인함 직접 실행될 때만 실행
근 게 모듈로 임포트 될 때는 이 코드 블럭이가 실행이 안된다는 거임
2)
wiki = Wikipedia()
이건 뭐.. 아시죠? wikipedia 클래스의 인스턴스 wiki 생성.
3)
for algorithm in _ALGORITHMS:
wiki.pull_content(algorithm)
_알고리즘 리스트에 정의된 각 토픽을 순서대로 넣는겨 뭐 Hang Kang , Random Forest , KimChi 이런 식으로
지금부터 내가 제시하는 방법은 순전히 내 경험으로 얻은 귀납적 추론이다. 뇌피셜이라는 뜻이다.
하지만 나는 아래 제시된 동일한 에러를 여러번 접했고 다음과 같은 방법으로 항상 해결했다.
에러코드 401의 해결법이다.
openai.AuthenticationError: Error code: 401 -
{'error': {'message': 'Incorrect API key provided:
sk-proj-********************************************DA6D.
You can find your API key at https://platform.openai.com/account/api-keys.',
'type': 'invalid_request_error', 'param': None, 'code': 'invalid_api_key'}}
다음과 같은 오류가 났을텐데.
원인 :
이건 당신의 API 코드가 외부에 노출되었을때
openai 측에서 당신의 API 코드를 그냥 비활성화 시켜서 생기는 에러다.
상세 :
깃허브에 그냥 벅 하고 API 키를 올리면 API 키가 자동으로 비활성화된다. 깃허브뿐만 아니라 외부인이 접근할 수 있는 사이트에 당신의 API 키가 노출되면 비활성화될 것이다.
해결법 :
401 에러가 뜬 API 키는 그냥 삭제하고 새로운 API 키를 발행한다.
중요 : 그 어디에도 당신의 API 키를 공유하지 말고 코드를 실행해본다. 높은 확률로 에러가 뜨지 않을 것이다.
깃허브에 퍼블리시 하고 싶으면 프라이빗으로 리포지토리를 만들자.
애초에 API 키를 모두가 볼 수 있는 장소에 노출한다는 것은 내 신용카드 정보를 노출 시키는 것과 같은 민감한 사안이다. 항상 보안에 유의하자.
에러코드 429의 해결법
openai.RateLimitError: Error code: 429 -
{'error': {'message': 'You exceeded your current quota,
please check your plan and billing details.
For more information on this error, read the docs:
https://platform.openai.com/docs/guides/error-codes/api-errors.',
'type': 'insufficient_quota', 'param': None, 'code': 'insufficient_quota'}}
에러코드에도 해답이 나와있는데 대충 (우리는 자선단체가 아니니깐) billing details. 를 확인해보라는 뜻이다.
톱니바퀴 아이콘을 누른다. API 발급까지 할 수 있는 사람이라면 OpenAi <Dashboard>를 쉽게 찾을 수 있을 것이다. 우측 상단의 Dashboard 옆에 톱니바퀴(설정)을 누른다.(OpenAI 사이트에서 Docs를 눌러서 들어오면 쉽게 찾을 수 있다.)