https://min-c-max.tistory.com/entry/Chroma-LangChain-Tutorial-%ED%81%AC%EB%A1%9C%EB%A7%88-%EB%9E%AD%EC%B2%B4%EC%9D%B8-%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC-%EB%9D%BC%EA%B7%B8-RAG

Chroma LangChain Tutorial 크로마 랭체인 튜토리얼 라그 RAG

벡터 스토리지를 사용해야 되는 일이 생겼다. 그래서 이것저것 해보고 있다. Chroma Doc를 읽으며 여러 가지 코드들을 실행해 보며 이해해보고 있다. 나처럼 허우적 되는 사람을 위해 내가 이해한

min-c-max.tistory.com

 
코드 분석 이전글이다
전반적인 코드 흐름이 적혀 있어서
읽고 오는 게 무조건 이해하기 편하다.


 
코드 분석
 
코드를 좀 내 입맛대로 바꿨다.
꽤 옛날 문서라 모듈이 호환이 안 되는 경우도 있고 해서
(특히 ask_.py)
 
나 같은 초보자가 볼 거라 생각하고 진짜 자세히 설명할 거
코드를 한 줄 한 줄 다 뜯어서 설명할 거임.
코드 해체 분석기라고 생각 좀;

코해분

 
자 첫 빠따는 
이름만 봐도 대충 뭐 할 거라 감이 오는
wikipedia.py 다 (여기에는 Chroma나 Langchain 연관된 코드는 없음)
이 코드는 대부분 크롤링에 관한 내용임.
 

import requests
import pypandoc
import datetime
import re

 
이 라인들은 필요한 라이브러리들을 가져옴
 

  • requests: 웹 요청을 보내기 위해 사용됩니다. 위키피디아 API에 접근할 때 필요함
  • pypandoc: HTML을 일반 텍스트로 변환하는 데 사용됨.
  • datetime: 파일 이름 생성 시 현재 날짜와 시간을 사용하기 위해 필요함
  • re: 정규 표현식을 사용하여 HTML 내용에서 특정 패턴을 찾는 데 사용됨.

 
참고 : 정규 표현식(정규식 또는 regex라고도 함)
문자열의 일정한 패턴을 표현하는 일종의 형식 언어임,
문자열에서 특정 패턴을 검색, 매칭, 추출하는 데 사용함.


 

pypandoc.download_pandoc()

 
이건 내가 집어넣은 코드임.
pandoc을 사용하려고 다운하겠다는 뜻
(pypandoc을 import 했는데 안 돼서 그냥 다운 받음)
 
참고 : pandoc은 문서 변환 도구임.
현재 코드에서는 HTML을 일반 텍스트로 변환하는데 쓰였음.
 


 

_ALGORITHMS = ["Han Kang", "Random Forest", "K Nearest Neighbour", "One class SVM", "Linear Regression",
               "Logistic Regression", "Support Vector Machine", "K Means Clustering", "Hierarchical Clustering",
                "Simpsonsons", "Python", "Rainworld"]

 
걍 위키에서 긁어오고 싶은 것들 모아둔 리스트임.
본인이 원하는 토픽으로 변경해도 됨. "Han Kang" 주목
 


class Wikipedia:
    def __init__(self):
        self.wikipedia = {}

 
Wikipedia 클래스를 정의하고,
초기화 메서드에서 wikipedia라는 빈 딕셔너리를 생성.
이 딕셔너리는 각 주제의 내용을 저장하는 데 사용됨.
 
뭔 말임?
wikipedia가 객체를 생성하면
무조건
모든 객체가 wikipedia라는 딕셔너리를 가지고 시작한다.
라는 뜻임
 
붕어빵 틀을 만들어놨다고 생각하면 됨.
 
더 쉬운 예시

wiki1 = Wikipedia()
wiki2 = Wikipedia()

# wiki1과 wiki2는 각각 별도의 빈 wikipedia 딕셔너리를 가짐
print(wiki1.wikipedia)  # 출력: {}
print(wiki2.wikipedia)  # 출력: {}

# wiki1의 딕셔너리에 내용을 추가해도 wiki2에는 영향을 주지 않음
wiki1.wikipedia["Python"] = "프로그래밍 언어"
print(wiki1.wikipedia)  # 출력: {"Python": "프로그래밍 언어"}
print(wiki2.wikipedia)  # 출력: {}  (여전히 비어 있음)

    def pull_content(self, topic: str):
        url = f"https://en.wikipedia.org/w/api.php?action=parse&page={topic}&format=json"
        response = requests.get(url)
        data = response.json()

 
pull_content 메서드는 주어진 토픽에 대한 위키피디아 내용을 가져옴.
위키피디아 API에 요청을 보내고 JSON 형식의 응답을 받음.
 
흐름 :
API 응답 → JSON 형식의 텍스트 → Python 객체
 
자세한 흐름 :
일단 {topic}에 우리가 적은 토픽들이 들어감.
왜용?

  for algorithm in _ALGORITHMS:
        wiki.pull_content(algorithm)

 
 
왜냐면 전체 코드 막줄에 이게 박혀 있음. 알고리즘에 적은 토픽이 순서대로 pull_content 메소드에 들어감.
 
코드 해석

url = f"https://en.wikipedia.org/w/api.php?action=parse&page={topic}&format=json"

 
암튼 토픽이 {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 &lt;https://lists.wikimedia.org/postorius/lists/mediawiki-api-announce.lists.wikimedia.org/&gt; for notice of API deprecations and breaking changes."
    }
}

 
대충 이렇게 생겼을 거임.
 
 


성공적인 응답
 
성공적인 응답은 이런 식으로 보냄

data = {
    "parse": {
        "title": "뽀잉뽀잉",
        "pageid": 23862,
        "text": {
            "*": "<div class=\"mw-parser-output\"><p><b>뽀잉뽀잉/b> 안녕 나는 뽀잉뽀잉이야 나도 내가 뭔지 몰라. ...</p>..."
        },
        "langlinks": [...],
        "categories": [...],
        "links": [...],
        "templates": [...],
        "images": [...],
        "externallinks": [...],
        "sections": [...],
        "parsewarnings": [],
        "displaytitle": "뽀잉뽀잉)",
        "iwlinks": [...],
        "properties": {...}
    }
}

 
대충 이렇게 생겼을 거임.
 
이러면 뽀잉뽀잉이라는 위키피디아 페이지를 잘 크롤링해온 거임
 
근데 우리는 하나 더 생각해야 됨.
 
갓뎀리디렉션

리디렉션 만세

 
리디렉션 페이지를 긁어오면 아마 이렇게 생겼을 거임.

data = {
    "parse": {
        "title": "뽀잉뽀잉",
        "pageid": 23862,
        "text": {
            "*": "<div class=\"redirectMsg\"><p>Redirect to:</p><ul class=\"redirectText\"><li><a href=\"/wiki/뽀잉뽀잉\" title=\"뽀잉뽀잉\">뽀잉뽀잉</a></li></ul></div>"
        },
        ...
    }
}

 
이것은 원본 페이지로 이동하기 위한 데이터를 얻기 위해 필요함.
그 이외에는 하등 쓸모가 없기 때문에 이것을 걸러내는 로직이 해당 코드에 중요한 포인트임.


 
일단.
 
성공적인 응답 vs 에러는
 
아래 코드로 분별함.

if data.get("error", None) is None:

else:

 
 
일단 이건 다들 알겠지만 매우 간단함.
 
Case 1.
1) data에 error키 있음?
2)ㄴㄴ -> ㅇㅋ 그럼 none 드림.
3)그럼 none is none 이 되어서 해당 if문으로 진입함 (True)
 
Case 2.
1) data에 error키 있음?
2) 어 ㅅㅂ 이거 뭐야 error잖아. 
3)그럼 none is none 이 안되어서 else문으로 진입함 (False)
 


Case 1. 의 상황이다
우리는 if문에 진입했다.

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)

 
이제 이 코드가 실행된다.
 
코드 흐름 :
1) API 응답에서 페이지의 HTML 내용을 추출함. (뽕)
2) 리디렉션 페이지를 식별하기 위한 정규표현식 패턴을 정의함.
3) 정의된 패턴을 사용하여 HTML 내용에서 리디렉션 링크를 찾음.
 



1) API 응답에서 페이지의 HTML 내용을 추출함. (뽕)
 

html_content = data["parse"]["text"]["*"]

 
아주 간단한 코드다.
 
data 값에 뭐가 들어있는지는 우리는 알고 있다.
(모르겠으면 왜 모르는지 모르겠지만 위에 글을 조금 더 자세히 읽고 와라.)
 
data는 딕셔너리다. 딕셔너리에서 요소에 접근할 때는
data.["이거 좀 보여주소"] 이런 식으로 사용한다.
 
저 코드는 딕셔너리의 키 값에 접근 접근 접근 하는 방식이다.
 
1) data 값의 "parse"에 접근한다. (parse에 파싱 결과가 들어있음)
2) parse 섹션 내의 "text"에 접근한다.
3) text 에서 키가 "*"인 값을 가져온다.
 

data = {
    "parse": {
        "title": "뽀잉뽀잉",
        "pageid": 23862,
        "text": {
            "*": "<div class=\"mw-parser-output\"><p><b>뽀잉뽀잉</b> 안녕 나는 뽀잉뽀잉이야 뽀잉빠잉뽀잉 ...</p>...</div>"
        },
        # 기타 메타데이터...
    }
}

 
data 의 예시다 코드와 비교하면서 보면 더 이해하기 쉬울 거.
 
근데 왜 "*"를 키로 사용하는 거?
 
엥 그러게?
이는 MediaWiki API의 관례인데
"*"는 "모든 것" 또는 "전체 내용"을 의미하는 와일드카드로 사용된다.
 
쉽게 말하면 모든 HTML 내용이 벨류로서 "*"라는 키에 들어가 있다.
 
아직도 뭐라는지 모르겠어요 ㅠㅠ
넌 그냥 외워라 모든 HTML 내용을 가지고 오고 싶으면 "*"가 키인 값을 가져와라.
 
그럼 현재 html_contentHTML 전체 내용이 들어있게 된다.
 
중요한 점은 위에서 설명했듯이
요청이 성공했을 때 2가지 케이스가 있다.
 
실제 페이지 / 리디렉션 페이지.
리디렉션 페이지의 HTML 내용이 html_content에 들어가 있을 수도 있다.
라는 것을 일단 잊지 말고 넘어가자.
 
 


redirect_link_pattern = r'<div class=\"redirectMsg\">.*?<a href=\"([^"]*)\" title=\"([^"]*)\">'

 
이게 뭔가 싶을 수 있는데 
간단한 예시와 실제 예시를 비교하며 이해해 보자.
 
1) 정규 표현식이 <div class="redirectMsg"> 태그를 찾음.

  • "리다이렉트" 표지판을 찾는다.

2) .*?로 중간의 텍스트를 건너뜀.

  • 표지판에서 중요하지 않은 정보를 건너뜀.

3) <a href=" 부분을 찾아 링크의 시작을 인식.

  • "여기 주소 있어요" 표시를 찾음.

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 문자열: 그냥 있는 그대로 두 개의 문자로 취급함. 편견 없는 사나이임.

raw string 할아버지

match = re.search(redirect_link_pattern, html_content, re.DOTALL)

 
 
1) re는 맨 위에 import란에서 설명했듯이 문자열 패턴 매칭을 위한 도구툴임.
2) searchre의 모듈 함수임 문자열 전체에서 패턴과 일치하는 첫 번째 위치를 찾아버림.
구문 : 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 이 나오는 거 (실제 페이지 제목)
 

  url = f"https://en.wikipedia.org/w/api.php?action=parse&page={redirected_topic}&format=json"

 
이걸 다시 { redirected_topic } 에 넣은 뒤
이제 ㄹㅇ ㄹㅇ ㄹㅇ 실제 페이지 위키로 접근해서
해당 위키 정보를 긁어오는 거임.
 
Steam_Deck(찐키피디아 제목) -> 투척 ->  { redirected_topic
 

response = requests.get(url)
data = response.json()

 
이 코드는 위에 설명했으니깐 안 함.
 
암튼 이러면 ㄹㅇ 찐찐찐 진짜 위키 실제 사이트 결과가
data에 들어감.
 
근데 href는 왜 캡처한 거예요? 쓰지도 않는데?
정답 :  나도 모름 내가 만든 코드가 아니라
 


self.wikipedia[topic] = data["parse"]["text"]["*"]

 
이 코드는 실제 페이지의 정보를 딕셔너리에 집어넣는다는 것을 의미한다.
 
크게 이 코드를 마주하게 되는 흐름은 이렇게 두 가지임.
 
1) 실제 페이지 
2) 리디렉션 페이지 -> 실제 페이지
 
이러한 흐름으로 결국 실제 페이지가 저 코드 위로 떨어진다.
 

wiki.pull_content(algorithm)

전체 코드 막줄에 이게 적혀 있다.

self.wikipedia[topic] = data["parse"]["text"]["*"] #정의

wiki.wikipedia[Han Kang] = "한강 작가님은 진짜 유명한 노벨상 수상자임" #실제값 대입


이를 실행하게 되면 내부적으로는  아래 코드가 실행된다는 거임.
 


        else:
            if self.wikipedia.get("error", None) is None:
                self.wikipedia["error"] = []
            self.wikipedia["error"].append(topic)

그냥 에러처리임
 
if문은 위에 설명한 코드와 완벽히 동일하게 작동하니 설명 생략
 
대충 error 나오면 error 난 것을 리스트에 집어넣는다는 거임.
나중에 에러 코드를 보기 위함인 듯.
 

return self

 
이것도 내 짧은 지식으로는 왜 있는지 잘 모르겠음.
없어도 실행 잘됨.
 
메소드 체이닝을 위해서 넣은 거 같은데
 
메소드 체이닝
: 여러 메소드 호출을 연속적으로 이어서 하는 패턴임

  • 예: wiki.pull_content("A").pull_content("B").pull_content("C")
    • 이런 식으로 작동함
    • 근데 for을 통해 알고리즘 내부 요소를 차근차근 넣어주고 있어서 이게 왜 필요한지 모르겠음.

future-proofing(미래에 발생할 변경사항을 위해 확장에 쉽게 대응)
인가보다 하고 넘어가자.
 


    @property
    def errors(self):
        return self.wikipedia["errors"]

    @property
    def topics(self):
        return self.wikipedia.keys()

    @property
    def topics(self):
        return [self.wikipedia[topic] for topic in self.wikipedia.keys() if topic != "errors"]

 
이쪽 코드는 그냥 디버깅용으로 휘갈긴 코드 같음.

  return self.wikipedia["errors"]

 
일단 errors를 반환 불가능함. errors 키를 가진 값이 코드 어디에도 없음
 

if self.wikipedia.get("error", None) is None:
                self.wikipedia["error"] = []
            self.wikipedia["error"].append(topic)

 
이거 보면 erros가 아니라 error이기도 하고.
 

@property
def topics(self):
    return self.wikipedia.keys()

@property
def topics(self):
    return [self.wikipedia[topic] for topic in self.wikipedia.keys() if topic != "errors"]

 
똑같은 이름의 속성이 2개 정의되어 있음.
이러면 마지막에 정의된 topics 속성만 적용됨.
 
상당히 띠용인 코드
 
그냥 의도만 이해하고 가자.
 
의도
 

  • 오류 처리를 위한 별도의 메커니즘을 만들어서 오류 확인
  • 사용자가 저장된 주제 목록, 내용, 그리고
  • 발생한 오류를 쉽게 접근할 수 있도록 속성(property)을 제공.

def get_content(self, topic: str):
    return self.wikipedia[topic]

 
 
이거 그냥 저장된 해당 토픽 위키피디아 내용 가져오는 코드.
 


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면 그냥 건너뛰라는 거임.
아마 에러 목록이 리스트로 저장되어 있나 봄. 
 

content += Wikipedia.html_to_plaintext(i.replace('\n', '')).replace('\n', '')

 
코드 실행 순서 

  1. ( i.replace('\n', ''): 이(i) 내용에서 모든 줄 바꿈 문자('\n')를 빈 문자열('')로 대체함.
  2. Wikipedia.html_to_plaintext(...): HTML 형식의 텍스트를 일반 텍스트로 변환.
    • 1단계에서 줄 바꿈이 제거된 내용이 이 메서드에 입력됨.
  3. (.replace('\n', ''): HTML에서 일반 텍스트로 변환된 결과물에 대해 다시 한번 줄 바꿈 제거함.
    • 2단계 변환 과정에서 새로운 줄 바꿈 생성에 대비한 듯
  4. content +=: 처리된 텍스트를 content 문자열에 추가합니다.
  5. return content : 병합된 내용 반환 (뽕)

    @staticmethod
    def html_to_plaintext(html: str):
        return pypandoc.convert_text(html, "plain", format="html")

 
 
이건 이제 다들 알겠지만
HTML일반 텍스트변환하라는 의미임.
 
"plain"이 스타일 없는 순수 텍스트를 의미함.
(아스키랑 유니코드 문자만 포함)
 
아니 근데 임마는 메소드인데 왜 self가 없음? 함수임?
 
정적 메소드 (@staticmethod)
 
정적 메소드 데코레이터를 사용하면 메소드를 함수처럼 쓸 수 있음.
인스턴스 없이 직접 클래스를 통해 호출 가능. (여기서 제일 높은 사람 불러와.)
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 이런 식으로
 
각 토픽에 대해 wiki.pull_content(algorithm)을 호출:

  1. 위키피디아 API를 사용하여 해당 주제의 내용을 겟또.
  2. HTML 형식의 내용을 self.wikipedia 딕셔너리에 세이브.
  3. 리디렉션 페이지가 감지되면 찐키피디아에 접근해서 해당 실제 페이지 내용 겟또.

4)

    print(wiki.merge_content_to_plaintext())

 
wiki.merge_content_to_plaintext()를 호출하고 결과를 출력:

  • 모든 가져온 위키피디아 내용을 하나의 텍스트로 병합.
  • HTML을 일반 텍스트로 변환 (html_to_plaintext 메소드 사용).

5)

  print(wiki.to_txt(wiki.merge_content_to_plaintext()))

 
wiki.to_txt(wiki.merge_content_to_plaintext())를 호출하고 결과를 출력:

  • 병합된 일반 텍스트 내용을 파일로 세이부.
  • 파일 이름은 인간이 지정 안 하면 현재 날짜와 시간을 기반으로 생성됨.
  • 저장된 파일 경로를 반환.

수고했다 모두들(특히 나)
 
이제 제일 중요한 코드인
ask_wikipedia.py 분석이 남았다.
 
해당 코드는 다음 블로그에 올림 ㅅㄱ

--------------------


ㅎㅇ 다음 블로그임
(2024-10-19 추가)
https://min-c-max.tistory.com/m/entry/Chroma-LangChain-Tutorial-2-%ED%81%AC%EB%A1%9C%EB%A7%88-%EB%9E%AD%EC%B2%B4%EC%9D%B8-%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC-%EB%9D%BC%EA%B7%B8-RAG-%EC%BD%94%EB%93%9C-%EB%B6%84%EC%84%9D

Chroma LangChain Tutorial (2) 크로마 랭체인 튜토리얼 라그 RAG 코드 분석

ㅎㅇ 2번째 코드 분석 글이다. 이전 글을 읽고 오면 더 이해하기 쉽다. 전체적인 코드 흐름에 관한 글. 0번https://min-c-max.tistory.com/entry/Chroma-LangChain-Tutorial-크로마-랭체인-튜토리얼-라그-RAG Chroma L

min-c-max.tistory.com

+ Recent posts