- 가상환경 설정 및 MVP 구현
- Dataset과 Vector DB 구현
- Dataset 확보
- Vector DB 구현
- Embedding & Searching 구현
- Generation 구현
데이터셋의 확보를 위해서 Selenium 크롤러를 구현했다. Beautifulsoup 대신 Seleinum을 사용한 이유는 간단히 자동화와 현재 같이 팀프로젝트를 진행하고 있는 팀원들도 쉽게 사용할 수 있도록 만들기 위해서 였다.
그래서 아래의 기능을 하는 크롤러를 만들었다.
- 서울기준 각 행정구와 행정동의 음식점, 카페, 그리고 가볼만한곳을 검색한다.
- 행정구 행정동의 검색된 장소들을 크롤링해온다.
- 마지막으로 크롤링된 내용을 json으로 전환하여 저장한다.
행정구와 행정동의 카테고리별 장소들을 찾을 수 있는 웹사이트는 Naver Map이 가장 적합하다고 판단하였고 아래의 사진에 나온 순서로 위의 기능을 구현했다. (기본적인 가상환경 설정은 따로 언급하지 않았다.)
0. map.naver.com 들어가고
1. 성북구 성북동 음식점을 입력하고 검색한다.
2. 위에서 부터 순서대로 내려오며 하나씩 클릭한다.
3. 클릭 후 뜨는 모달에서 장소에 대한 구체적인 정보들을 가지고 온다.
4. 2,3번을 반복한다.
이 기본로직을 바탕으로 크롤러를 먼저 구성한 다음에 상세한 부분들을 문제상황에 맞게 수정해 나갔다.
문제상황
Iframe 전환
NAVER Map의 경우 Iframe을 사용한다. Iframe은 모달과 같이 html페이지의 단독적인 공간이라고 생각하면된다. 그렇기에 Selenium의 Webdriver는 Iframe안의 내용을 조회하기 위해서 .switchframe()를 이용하여 지속적으로 frame을 변환해 줘야한다. 이 경우 Frame이 변환 되지 않으면 조회가 불가능 할 수 있다. 그렇기에 아래와 같이 함수로 따로 만들어 두어 확실하게 프레임 변환을 진행했다.
def switch_to_frame(driver, frame_name):
driver.switch_to.default_content()
driver.switch_to.frame(frame_name)
무한스크롤
Naver Map의 경우 무한스크롤이 적용이 됐기에 검색 직후 스크롤을 내려 줘야한다. 그렇게 해주지 않는 다면 로드가 되지 않기 때문에 크롤러가 오류가 발생한다. 그래서 아래와 같이 스크롤을 충분히 내려준 다음 그리고 로딩을 충분히 기다려준 다음 크롤링이 진행되도록 만들었다.
# Scroll down to bottom
switch_to_frame(driver, frame_name='searchIframe')
body = driver.find_element(By.CSS_SELECTOR, 'body')
body.click()
for i in range(200):
body.send_keys(Keys.PAGE_DOWN)
여러개의 페이지
Naver Map의 정보의 양은 방대하기 때문에 그에 맞춰 여러 페이지가 존재했다. 그래서 먼저 페이지를 찾아 변수에 담은뒤 해당 페이지에 리스트업 된 장소들을 하나씩 클릭하도록 만들었다. 다행이게도 서울지역의 경우 모든 검색 결과가 최소 5페이지로 구성이 돼있었다. 그리고 이번 크롤러를 구현하면서 Naver Map의 경우 '성북구 성북동 음식점'의 결과가 적더라도 근방까지 포함하여 5페이지를 구성한다는 사실을 확인했다. 그래서 행정구의 벗어나면 크롤링을 해오지 않는 로직을 추가하여 추가적인 조건을 넣지 않고 크롤링을 구성하였다.
# Page
page_tabs = driver.find_element(By.CSS_SELECTOR, "div.XUrfU > div.zRM9F")
searched_pages = page_tabs.find_elements(By.TAG_NAME, "a")
for page_index in range(len(searched_pages)):
switch_to_frame(driver, frame_name='searchIframe')
page = searched_pages[page_index]
if (page.text not in pages_to_search):
continue
print("Current Page: ", page.text)
page.click()
sleep(2)
크롤러 오작동
Selenium으로 만든 크롤러를 처음 사용해 봤는데 처음으로 '연약한' 라이브러리가 뭔지 알게 됐다. 아래의 내용들이 Selenium 크롤러가 오류가 발생하는 순간들이다.
- 인터넷 연결이 0.0001초 이상 끊겼을 때
- 찾으려고 하는 내용이 존재하지 않을 때
- 내용이 로드 되지 않았을 때
- 장소별로 관련 내용을 등록하지 않았을 때
- 찾으려고 하는 프렘임이 전환이 되지 않았을 때
- 너무 오랜 시간 ChromeWebDriver를 사용하였을 때
- 그냥 혹은 원인불명
실제로 1번과 5번이 가장 많이 발생하는 이유이고 해당 문제의 경우 크롤링이 실행되는 컴퓨터의 설정을 바꿔 주거나 아님 크롤러가 의도치 않게 꺼질경우 다시 실행하는 shell script파일을 만들면된다.
하지만 반대로 2,3,4번의 경우 예방 될 수 있고 2번의 경우 좋은 코딩 습관과도 연관이 된다는 점을 알게 됐다.
먼저 2번의 경우 아래와 같이 아래와 같이 해결 했다.
내용이 로드 되지 않았을 때
이 문제의 경우 2가지 해결방안이 존재하는데 더 안정적인 크롤링을 위해 둘다 사용했다. 하지만 역시 크롤러는 자주 꺼졌다.
첫번째로는 아래처럼 로드가 될 때 까지 기다리는 함수를 사용하였다. 하지만 이 부분이 제대로 작동하지 않았기에 결국에는 sleep()과 같이 강제로 크롤링을 잠시 멈추게 했다.
def info_get(driver):
switch_to_frame(driver, "entryIframe")
tab = tab_selector(driver, "정보")
try:
tab.click()
sleep(1)
info = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((
By.CSS_SELECTOR,
'div[data-nclicks-area-code="inf"]'))
).text
return info
except Exception as e:
print(e)
return "None"
장소별로 관련 내용이 존재 하지 않을 때
가장 간단한 문제이지만서도 습관하 되지 않았을 때 고치기 힘든 습관이었다. 누구나 입력값이 다를 경우를 대비하여서 try-catch를 해야한다고 생각은 하나 반대로 습관적으로 구현하지 않는다. 나도 똑같이 처음에는 구현하지 않았으나 결국에는 모든 조회에 있어서 아래와 같이 기다리게 전환하게 됐다. 물론 추후에는 try-catch를 좀 더 활용해야 된다.
def place_name_get(driver, place):
switch_to_frame(driver,"searchIframe")
try:
place_name = place.find_element(By.CSS_SELECTOR, "div.CHC5F > a.tzwk0 > div > div > span.place_bluelink.TYaxT").text
return place_name
except Exception as e:
print(e)
place_name = place.find_element(By.CSS_SELECTOR, "div > span.xBZDS").text
return place_name
3번의 경우 함수로 따로 만들었고 동시에 기존의 코드들 또한 중복되거나 도메인이 확실한경우 함수로 전환하였다. Github에 crawler_methods.py에서 확인 할 수 있다.
너무 오랜 시간 ChromeWebDriver를 사용하였을 때
기존의 크롤러는 행정구의 행정동의 음식점, 카페, 가볼만한 곳을 다 크롤링하고 행정구의 모든 행정동을 다 크롤링 하기전에는 웹드라이버를 끄지 않았다. 하지만 이럴 경우 웹드라이버는 자주 꺼지는 현상이 발생하게 됐는데 그 문제를 간단히 웹드라이버를 카테고리 별로 재시작하도록 수정 하였다. 추가적으로 웹드라이버 혹은 예상치 못한 오류 발생시 현재까지 작업한 내용들을 저장하도록 또한 만들었다.
for category_index in range(len(categories)):
# Driver
driver = webdriver.Chrome(service= Service(ChromeDriverManager().install()))
......
# Whencurrent current category's crawling is done
print(f"Updated Json List: {len(updated_json)}")
print(f"Duplication Check: {len(dup_check)}")
driver.quit()
.....
# Saving crawled data if there is any bad news
except WebDriverException as e:
print(e)
print("Updated: ", len(updated_json))
if (len(updated_json) != 0):
for new_row in updated_json:
data.append(new_row)
with open(file_path, 'w', encoding='utf-8') as json_file:
json.dump(data, json_file, indent=4, ensure_ascii=False)
sleep(10)
driver.quit()
이로써 크롤링을 통해 서울시의 대부분의 행정구와 행정도에 대한 대량의 장소 데이터를 확보 할 수 있었다.
아래의 링크를 통해 전체 코드를 확인 할 수 있다.
RAG-DATA/ha-crawler
이제 데이터가 준비 됐으니 다음단계인 Vector DB로 넘어가겠다.
RAG 구현 Step-by-Step Vector DB 구현 - 1: Implementation Outline
참조
'AI > Gen AI' 카테고리의 다른 글
RAG 구현 Step-by-Step Vector DB 구현 - 2: Faiss with LangChain (3) | 2024.09.17 |
---|---|
RAG 구현 Step-by-Step Vector DB 구현 - 1: Implementation Outline (1) | 2024.09.16 |
RAG 구현 Step-by-Step Dataset 확보 (1) | 2024.08.28 |
RAG Step-by-Step: 가상환경 설정 및 MVP 구현 (0) | 2024.08.20 |
RAG 구현 Step-by-Step: Intro (0) | 2024.08.19 |