About me
home
Portfolio
home

웹스크래핑

진행 상태
완료
팀원
마감일
2023/10/27
태그
코딩
작업 (하위 작업)에 관계됨
3 more properties
서울시립대학교의 각 건물에서 매주 업로드되는 학식 메뉴를 웹 스크래핑하기 위해 파이썬의 Selenium 라이브러리를 활용하였다. 이 작업의 시작 단계는 서울시립대학교 학식 웹페이지(https://www.uos.ac.kr/food/placeList.do)에 접근하는 것이다. Selenium의 웹드라이버를 사용하여 웹 페이지를 Chrome 브라우저로 해당 웹페이지를 자동으로 실행하고 버튼 클릭기능과 table text 읽기 기능을 사용하여 식단 정보를 수집하였다.
Local 환경에서 웹 스크래핑 코드 실행 영상
웹 페이지에 접근한 후에는 각 건물, 즉 학생회관, 자연과학관, 본관, 그리고 양식당(아느칸)에서 제공하는 메뉴를 찾기 위해 HTML 요소를 탐색하였다. 일반적으로 이러한 정보는 테이블 또는 리스트 형태로 웹 페이지에 표시되므로, table내의 각 요소들의 type을 정확하게 식별할 수 있어야 한다.
정확한 HTML 요소를 찾은 후에는 Selenium의 find_element_by_id, find_elements_by_xpath 등의 메소드를 사용하여 학식 메뉴에 대한 정보를 추출하였다. 모든 건물의 학식 메뉴 정보를 수집한 후에는 이를 효율적으로 저장하기 위해 데이터베이스나 CSV 파일에 저장하였다. 이렇게 저장된 데이터는 Langchain의 Vector DataBase 저장시에 활용될 수 있다.

1. 학식 메뉴 웹스크래핑

from selenium import webdriver from selenium.webdriver.common.by import By import pandas as pd import re from datetime import datetime import os import glob import re class UOSMenuScraper: def __init__(self, save_dir='UOS_DB'): self.driver = webdriver.Chrome() self.buildings = { 'tab11': '학생회관', 'tab12': '본관', 'tab13': '양식당(아느칸)', 'tab14': '자연과학관' } self.current_weekday = datetime.now().weekday() self.current_date = datetime.now().strftime("%Y%m%d") self.save_dir = save_dir if not os.path.exists(self.save_dir): os.makedirs(self.save_dir) def init_driver(self): self.driver.get("https://www.uos.ac.kr/food/placeList.do") self.driver.implicitly_wait(10) def search_building(self, building_id): button = self.driver.find_element(By.ID, building_id) button.click() self.driver.implicitly_wait(1) def enter_weekly_menu(self): button = self.driver.find_element(By.ID, 'tab2') button.click() self.driver.implicitly_wait(1) def extract_meal_data_for_csv(self): table_element = self.driver.find_element(By.XPATH, "//*[@id='week']/table/tbody") rows = table_element.find_elements(By.TAG_NAME, "tr") weekdays = ['월', '화', '수', '목', '금'] data = {'Building': [], 'Day': [], 'Weekday': [], 'Morning': [], 'Lunch': [], 'Dinner': []} for i, row in enumerate(rows): ths = row.find_elements(By.TAG_NAME, "th") tds = row.find_elements(By.TAG_NAME, "td") day = ths[0].text if ths else None weekday = weekdays[i] meals = [re.sub('<br>', ' ', td.get_attribute("innerHTML")).strip() for td in tds] if day: data['Building'].append(self.building_name) data['Day'].append(day.split(' ')[0]) data['Weekday'].append(weekday) meals = ['없음' if len(meal) == 0 else meal for meal in meals] data['Morning'].append(meals[0] if len(meals) > 0 else None) data['Lunch'].append(meals[1] if len(meals) > 1 else None) data['Dinner'].append(meals[2] if len(meals) > 2 else None) return data def clean_text(self, text): # '&amp;' 제거 text = text.replace('&amp;', ',') # 콜론(:)과 그 앞뒤 단어 제거 text = re.sub(r"\S*\s*:\s*\S*", "", text) # 시간, 가격, 칼로리, 그램 정보 제거 text = re.sub(r"\d{1,2}:\d{2}~\d{1,2}:\d{2} \(\d{1,3},?\d{0,3}원\)|\d{3,4}kcal/\d{1,3}g|\d{3,4}kcal|\d{1,3},?\d{0,3}원", "", text) # 영어로 번역된 음식 이름 제거 (단, '코너 A', '코너 B', '코너 C'는 유지) # text = re.sub(r"(?<!코너 )[A-Za-z\s]*\([A-Za-z\s]*\)", "", text) text = re.sub(r"\b(?!코너 )[A-Za-z]{2,}\b", "", text) # 소괄호와 그 내용 제거 text = re.sub(r"\([^)]*\)", "", text) return text.strip() # 앞뒤 공백 제거 def extract_meal_data_for_md(self): table_element = self.driver.find_element(By.XPATH, "//*[@id='week']/table/tbody") rows = table_element.find_elements(By.TAG_NAME, "tr") weekdays = ['월요일', '화요일', '수요일', '목요일', '금요일'] data_md = "" header = "| Building | Day | Weekday | Morning | Lunch | Dinner |\n" separator = "| -------- | --- | ------- | ------- | ----- | ------ |\n" data_md += header + separator for i, row in enumerate(rows): ths = row.find_elements(By.TAG_NAME, "th") tds = row.find_elements(By.TAG_NAME, "td") day = ths[0].text if ths else None weekday = weekdays[i] meals = [self.clean_text(re.sub('<br>', ' ', td.get_attribute("innerHTML")).strip()) for td in tds] # clean_text 호출 if day: morning = meals[0] if len(meals) > 0 else None lunch = meals[1] if len(meals) > 1 else None dinner = meals[2] if len(meals) > 2 else None row_data = f"| {self.building_name} | {day.split(' ')[0]} | {weekday} | {morning} | {lunch} | {dinner} |\n" data_md += row_data return data_md def remove_old_files(self): for f in glob.glob(f"{self.save_dir}/*weekly_menu.csv"): os.remove(f) def save_to_csv(self, data): filename = f"{self.building_name}_weekly_menu.csv" full_path = os.path.join(self.save_dir, filename) df = pd.DataFrame(data) df.to_csv(full_path, index=False, encoding='utf-8') # df.to_csv(full_path, index=False, encoding='utf-8-sig') # 윈도우 환경 한글 깨짐 방지 def save_to_md(self, data_md): filename = f"{self.building_name}_weekly_menu.md" full_path = os.path.join(self.save_dir, filename) with open(full_path, 'w', encoding='utf-8') as f: f.write(data_md) def run(self): self.init_driver() self.remove_old_files() for building_id, building_name in self.buildings.items(): self.building_name = building_name self.search_building(building_id) self.enter_weekly_menu() meal_data_csv = self.extract_meal_data_for_csv() self.save_to_csv(meal_data_csv) meal_data_md = self.extract_meal_data_for_md() self.save_to_md(meal_data_md) self.driver.implicitly_wait(20) self.driver.quit() if __name__ == '__main__': scraper = UOSMenuScraper(save_dir='UOS_DB') scraper.run()
Python
복사

초기 설정과 드라이버 초기화

__init__ 메소드에서는 초기 설정을 한다. 웹 드라이버로는 Chrome을 사용하며, 대상 건물 이름과 해당 건물의 탭 ID를 딕셔너리로 저장한다. 또한 현재의 요일과 날짜 정보, 그리고 데이터를 저장할 디렉토리를 설정한다.
init_driver 메소드에서는 Selenium 웹드라이버를 초기화하고, 대학의 학식 페이지에 접근한다. 페이지 로딩을 위해 명시적 대기를 사용한다.

건물과 주간 메뉴 탭 선택

search_building 메소드에서는 각 건물의 탭을 클릭하는 작업을 수행한다. enter_weekly_menu 메소드에서는 주간 메뉴 탭(tab2)을 클릭한다.

메뉴 데이터 추출

extract_meal_data 메소드에서는 주간 메뉴 데이터를 추출한다. 이를 위해 XPATH를 사용하여 테이블의 tbody를 찾은 후, 각 행(tr)과 그 안의 셀(td)을 순회한다. 각 셀에서 메뉴 정보를 추출하고, 정규표현식을 사용하여 불필요한 문자를 제거한다.
clean_text 메소드는 음식 메뉴 데이터에서 필요하지 않은 부가 정보를 제거하여 사용자가 원하는 핵심 정보만을 제공하기 위한 전처리 메소드이다. 구체적으로 다음과 같은 작업을 수행한다:
제공 시간대와 가격 정보를 제거한다.
영어로 된 메뉴명을 제거한다. 단, '코너 A', '코너 B', '코너 C' 등은 유지한다.
원산지 및 칼로리 정보를 제거한다.
이렇게 처리된 데이터는 음성으로 메뉴 목록을 제공할 때 사용자가 필요로 하는 핵심 정보만을 효율적으로 전달할 수 있다.

전처리 전 메뉴 text

“11:30~14:00 (6,000원) 너비아니볶음 neobiani(pork) 두부무국 우엉채조림 숙주나물 소면야채무침 돈,계육:국산/대두:외국산 998kcal/46g”

전처리 후 메뉴 text

너비아니볶음 두부무국 우엉채조림 숙주나물 소면야채무침”

파일 관리 및 데이터 저장

remove_old_files 메소드에서는 이전에 저장된 md 파일을 삭제한다. extract_meal_data_for_md 메소드에서는 수집한 데이터를 표 형태로 Markdown 파일에 저장한다.

실행

run 메소드에서는 위의 모든 메소드를 순차적으로 호출한다. 먼저 웹드라이버를 초기화하고, 이전 파일을 삭제한다. 그리고 각 건물에 대해 주간 메뉴 데이터를 추출하여 md 파일로 저장한다. 마지막으로 웹드라이버를 종료한다.
기존에는 학식 메뉴 데이터를 CSV 파일로 저장하고 있었으나, Langchain을 활용해 서울시립대의 문서를 Language Learning Model (LLM)에 학습시키기 위해 전략을 변경하였다. Langchain의 학습 메커니즘이 주로 텍스트를 중심으로 하고 있기 때문에, 다양한 파일 형식(CSV, PDF, XML 등) 대신에 Markdown 형식으로 파일을 통일시켰다. 이렇게 하여 모든 데이터를 효율적으로 LLM에 학습시킬 수 있게 되었다.

2. 학교 공지사항 웹스크래핑

서울시립대에서 제공하는 REST API를 사용하여 XML 형태의 공지사항 목록을 추출하려 했지만, 학식 메뉴를 스크레이핑한 경험을 바탕으로 더 쉽고 전처리가 필요 없는 웹스크래핑 기술을 활용하는 것으로 변경하였다.
1.
시립대 학생들이 주로 확인하는 '일반 공지'의 공지사항 제목들을 추출한다.
2.
공지사항의 세부 내용까지 제공하는 것은 음성으로 전달한다는 제약을 고려하여 부적절하다고 판단하여 공지사항의 제목만 간략히 소개하는 방향으로 채택하였다. 학생들이 관심을 가지면 직접 시립대 홈페이지를 찾아들어가보는 정도로 안내한다.
3.
공지는 자주 업로드되므로 웹스크래핑도 빈번히 해야 한다.

서울시립대학교 일반 공지 목록의 최신 공지사항들의 제목들을 스크래핑한다.

시립대 공지사항 사이트 https://www.uos.ac.kr/korNotice/list.do?list_id=FA1
추출해야 할 공지사항 제목들
추출하여 markdown에 기록한 10개의 공지사항 제목들

구현 코드

from selenium import webdriver from selenium.webdriver.common.by import By import pandas as pd import os import glob from datetime import datetime class UOSNoticeScraper: def __init__(self, save_dir='UOS_DB'): self.driver = webdriver.Chrome() self.current_date = datetime.now().strftime("%Y_%m_%d") self.save_dir = save_dir if not os.path.exists(self.save_dir): os.makedirs(self.save_dir) def init_driver(self): self.driver.get('https://www.uos.ac.kr/korNotice/list.do?list_id=FA1') self.driver.implicitly_wait(8) def extract_notice_data(self): data = {'Title': []} for i in range(1, 11): # 10개의 공지를 확인한다고 가정 (필요에 따라 조정 가능) try: # '공지' 텍스트가 있는지 확인 notice_span = self.driver.find_element(By.XPATH, f'//*[@id="contents"]/ul/li[{i}]/div/p/span') if notice_span.text == '공지': # 제목 추출 title = self.driver.find_element(By.XPATH, f'//*[@id="contents"]/ul/li[{i}]/div/div/div[1]/a').text cleaned_title = title.replace("★", "").strip() cleaned_title = cleaned_title.replace("☆", "").strip() data['Title'].append(cleaned_title) except: # 해당 XPath가 존재하지 않을 경우, 다음 항목으로 넘어간다. continue return data def remove_old_files(self): for f in glob.glob(f"{self.save_dir}/공지사항*.csv"): os.remove(f) def save_to_csv(self, data): filename = f"공지사항_{self.current_date}.csv" full_path = os.path.join(self.save_dir, filename) df = pd.DataFrame(data) df.to_csv(full_path, index=False, encoding='utf-8') # df.to_csv(full_path, index=False, encoding='utf-8-sig') # 윈도우 환경 한글 깨짐 방지 def run(self): self.init_driver() self.remove_old_files() notice_data = self.extract_notice_data() self.save_to_csv(notice_data) self.driver.implicitly_wait(10) self.driver.quit() if __name__ == '__main__': scraper = UOSNoticeScraper(save_dir='UOS_DB') scraper.run()
Python
복사
다음 소스코드를 실행하여 UOS_DB 폴더 안에 최신 공지사항의 제목들을 기록
이는 LLM이 학습할 Vector DB에 저장됨