Langchain을 이용해서 특정 웹사이트의 정보를 추출하고 그 정보를 기반으로 검색을 수행해 보자.
잘 익혀두면 다양한 분야에서 사용할 수 있을 것 같다.
출처
- http://www.teddynote.com/langchain/langchain-tutorial-06/
- https://python.langchain.com/docs/use_cases/web_scraping
LangChain에선 Scraping을 위한 3가지 단계의 컴포넌트들을 제공한다.
- 검색: URL로 쿼리(GoogleSearchAPIWrapper)
- 로드: URL을 HTML로 변환(AsyncHtmlLoader, AsyncChromiumLoader)
- 변환: HTML을 형식화된 텍스트로 변환(HTML2Text, Beautiful Soup)
여기서 GoogleSearchAPIWrapper의 경우 사용자 질문에 대해 Google 검색을 수행하고 그 결과를 바탕으로 답변을 해준다. 실제 검색한 결과를 통해 질문에 대한 답변을 생성하기에 답변에 대한 근거를 확인할 수 있다는 큰 장점이 있다.
하지만 이 포스팅의 목적은 웹사이트를 쉽게 크롤링하는 데 있다. 특정 사이트에서 원하는 정보를 추출하기 위해선 "로드", "변환"을 위한 컴포넌트를 사용할 필요가 있다.
일단 아래를 위해 설치해야 할 패키지는 다음과 같다.
pip install -q langchain-openai langchain playwright beautifulsoup4
playwright install
playwright는 처음 보는데, 아래에서 사용할 AsyncChromiumLoader가 Chromium 인스턴스를 통해 실행되는데 이 인스턴스를 설치하기 위한 용도 정도로 이해했다.
이 포스팅에선 네이버 뉴스 정보를 크롤링해 볼 것이다.
1. AsyncChromiumLoader를 사용해서 특정 웹사이트의 HTML 가져오기
사실 전통적으로 파이썬으로 크롤링하면 requests.get
을 통해 html 가져오고 beautifulsoup를 통해 html을 분석하는데, AsyncChromiumLoader는 langchain에서 제공하는 html을 비동기로 가져오는 컴포넌트이다.
사용법은 간단히 아래와 같다.
from langchain.document_loaders import AsyncChromiumLoader
urls = ["https://news.naver.com/main/main.naver?mode=LSD&mid=shm&sid1=105"]
loader = AsyncChromiumLoader(urls)
html = loader.load()
2. BeautifulSoupTransformer로 Html을 파싱 하기
이제 html을 llm이 분석하기 쉽도록 변형이 필요하다.
from langchain_community.document_transformers import BeautifulSoupTransformer
bs_transformer = BeautifulSoupTransformer()
docs_transformed = bs_transformer.transform_documents(html, tags_to_extract=["div"])
위 코드에서 중요한 점은 tags_to_extract
이다. tag를 포함한 html 기준으로 transform을 수행하는데 기준이 될 tag를 선택해줘야 한다.
분석하고자 하는 정보가 포함된 tag를 지정해주지 않으면 html 내에서 원하는 정보를 가져오지 못한다.
이를 확인하기 위한 방법은 html을 봐도 좋지만, 크롬에서 "검사"를 통해 원하는 정보들이 어떠한 Tag로 구성되어 있는지 확인하면 된다.
3. LLM에게 전달해서 쉽게 정보 추출하기
일반적인 크롤링에선 BeautifulSoup를 통해 Html을 파싱 했다면 이제 뉴스 제목을 추출하기 위해 제목에 해당되는 Selector 혹은 XPath를 통해 제목을 가져와야 한다.
하지만 이 방법의 문제점은, 완벽히 해당 웹페이지의 소스코드에 의존한다는 것이다. 웹페이지의 구성이 변경된다면 기존에 작성했던 크롤링을 위한 로직은 동작하지 않는다.
LLM은 적절한 정보를 전달하면 정보로부터 제목을 추출할 수 있다.
여기서 적절한 정보는 앞서 BeautifulSoupTransformer을 통해 변환한 docs 정보이다.
LLM에게 전달하기 앞서 허용토큰 길이를 고려해서 입력해줘야 한다. 문서를 쪼개야 하는데 이때 RecursiveCharacterTextSplitter
라는 컴포넌트를 사용할 수 있다.
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
chunk_size=1000, chunk_overlap=0
)
splits = splitter.split_documents(docs_transformed)
이제 정보를 추출하기 위해선 원하는 "정보"가 있어야 한다. langchain은 content로부터 원하는 schema의 정보를 추출할 수 있는 create_extraction_chain
메소드를 지원한다.
from langchain.chains import create_extraction_chain
schema = {
"properties": {
"뉴스 제목": {"type": "string"},
"언론사": {"type": "string"},
},
"required": ["뉴스 제목", "언론사"],
}
def extract(content: str, schema: dict):
return create_extraction_chain(schema=schema, llm=llm).run(content)
content로부터 "뉴스 제목", "언론사"를 추출하는 chain이다.
임의의 content를 넣어서 사용해 보자.
content = "뉴스제목은 '갤럭시 S24 시리즈' 사전 개통 시작…주요 매장서 예약자 대기 행렬이다. 그리고 언론사는 데일리안이다."
extract(content=content, schema=schema)
# [{'뉴스 제목': '갤럭시 S24 시리즈 사전 개통 시작…주요 매장서 예약자 대기 행렬', '언론사': '데일리안'}]
너무 쉽게 정보를 전달했지만 원하는 정보를 잘 추출하는 걸 확인할 수 있다.
이제 splits 된 content들을 모두 해당 chain에 넣어서 뉴스 제목과 언론사 정보를 추출하는 코드를 완성해 보자.
from tqdm import tqdm
extracted_contents = []
for split in tqdm(splits):
extracted_content = extract(content=split.page_content, schema=schema)
extracted_contents.extend(extracted_content)
# [{'뉴스 제목': "갤S24 사전개통 인파 '북적'…마감일 8일 연장", '언론사': '지디넷코리아'},
# {'뉴스 제목': '삼성, 갤럭시S24 중국 버전에 바이두 AI 챗봇 탑재', '언론사': '뉴시스'},
# {'뉴스 제목': '결국 탈선한 꿈의 ‘애플카’…10년 만에 시동 꺼진 완전자율주행', '언론사': '한국일보'},
# {'뉴스 제목': '인공지능(AI), 무차별 일자리 공습 ‘ON’… “안전지대가 없다”', '언론사': '한국일보'},
# ...
# {'뉴스 제목': '오픈AI, 연초부터 법정 공방 ‘가시밭길’…출판계-언론계 ‘정조준’', '언론사': '한국일보'},
# {'뉴스 제목': '‘챗GPT’ 개발사 오픈AI, 올해 매출 6조5,000억 전망…폭풍성장', '언론사': '한국일보'},
# {'뉴스 제목': '삼성‧SK‧인텔이 지갑 열었다, 반도체 유리기판 뭐길래? <2편>', '언론사': '서울경제'},
# {'뉴스 제목': '삼성‧SK‧인텔이 지갑 열었다, 반도체 유리기판 뭐길래? <1편>', '언론사': '서울경제'},
# {'뉴스 제목': "2년 만에 '반도체 왕좌'서 밀려난 삼성전자…1위 오른 기업은?", '언론사': '서울경제'},
# {'뉴스 제목': "'이천 쌀집' SK하이닉스 1년 농사 계획 엿보기", '언론사': '서울경제'},
# {'뉴스 제목': '서비스 안내', '언론사': '422'}]
대부분의 결과가 나쁘지 않게 나오긴 했는데, 후처리가 조금 더 필요할 거 같다.
*참고로 LLM은 OpenAPI의 Key를 사용해서 수행했다.
from langchain.chat_models import ChatOpenAI
llm = ChatOpenAI(model_name="gpt-4-0125-preview", temperature=0) # 2024.01.28 기준 NEW Tag가 붙은 모델이라 사용해봤는데, 너무 느리다. 간단히 크롤링을 위해 사용할 필욘 없을 듯,,