이 글은 제가 작업했던 내용을 정리하기 위해 수기 형식으로 작성 된 글입니다.
2022.12.09 - [[신.만.추]] - 신입이 만드는 추천시스템-1(개요)
2022.12.09 - [[신.만.추]] - 신입이 만드는 추천시스템-2(데이터 수집, 스크래핑)
- 텍스트 데이터 수집(크리에이터 컨텐츠, 광고주 제안서)
- 텍스트 데이터 전처리 및 키워드화
- 키워드로 워드임베딩 모델 학습
- 아이템 벡터화
저번에 작성했던 글에서 셀레니움과 뷰티풀수프를 활용한 스크래퍼를 만들고 이를 도커 이미지로 만든 후 많은 개수의 컨테이너를 만들어 스크래핑 시간을 줄였다.
이번에는 셀레니움의 비율을 최소화하고 requests라이브러리를 추가적으로 사용하여 셀레니움의 메모리 점유율을 줄여보겠다.
1. 텍스트 데이터 수집(크리에이터 컨텐츠, 광고주 제안서)-2
저번 글에서 짰었던 스크래핑 로직을 다시 나열하면 아래와 같다.
셀레니움
크롬창 생성 → Youtube 채널접속 → 화면조작 수행(스크롤, 클릭 등)
뷰티풀스프
화면조작 수행 완료 된 페이지 소스 추출 → 추출 된 소스에서 데이터 검색 → 데이터 텍스트화
최종 결과
텍스트화 된 데이터를 전처리 하여 DB에 업로드
스크래핑 코드
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import Select
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
import time
from bs4 import BeautifulSoup
import pandas as pd
# 셀레니움 기본 세팅
user_agent = 'user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36'
options = webdriver.ChromeOptions()
options.add_argument('--disable-translate')
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
options.add_argument("--disable-infobars")
options.add_argument("--enable-logging")
options.add_argument("--log-level=0")
options.add_argument("--single-process")
options.add_argument("--ignore-certificate-errors")
options.add_argument('--start-fullscreen')
options.add_argument('user-agent={0}'.format(user_agent))
driver = webdriver.Chrome(options=options)
driver.implicitly_wait(3)
base_url = '<https://www.youtube.com>'
recent_order_para = '/videos'
about_suffix = '/about'
c_url = "<https://www.youtube.com/channel/UCSQ55iSysqYErLRDC6ARi9w>"
# 예시 채널로 크롬드라이버 접속
driver.get(c_url+recent_order_para)
for i in range(3):
driver.find_element(By.TAG_NAME,'body').send_keys(Keys.PAGE_DOWN)
time.sleep(1)
# 해당 채널의 30개 영상 제목, 영상 링크 수집
page = driver.page_source
soup = BeautifulSoup(page,'lxml')
channel_name = driver.find_element_by_xpath('//*[@id="text-container"]').text
follower = driver.find_element_by_css_selector('#subscriber-count').text
all_title = soup.find_all('a','yt-simple-endpoint focus-on-expand style-scope ytd-rich-grid-media')
vod_link = [base_url+n['href'] for n in all_title]
vod_link = vod_link[0:30]
creator_data = []
for i in range(len(vod_link)):
driver.get(vod_link[i])
driver.implicitly_wait(5)
pannel = driver.find_element(By.XPATH,'//*[@id="contents"]')
driver.execute_script("arguments[0].scrollBy(0,100)",pannel)
button = driver.find_element(By.ID,'expand')
button.click()
page = driver.page_source
soup = BeautifulSoup(page,'lxml')
more_text = soup.select('#description')[1].text
meta_box = soup.find_all('span',"style-scope yt-formatted-string bold")
vod_title = soup.find('h1',"style-scope ytd-watch-metadata").text.replace('\\n','')
view_cnt = meta_box[0].string
pub_dt = meta_box[2].string
creator_data.append([channel_name, follower, vod_link[i], vod_title, more_text, view_cnt, pub_dt])
pd.DataFrame(creator_data,columns=['채널명','구독자수','영상링크','영상제목','더보기내용', '조회수','업로드날짜'])
driver.quit()
스크래핑 결과
사실 영상 목록을 더 확보하기 위해서 스크롤을 내리는 것은 크게 의미가 없다.
초기에 스크래퍼를 기획할 때 각 크리에이터 별 영상 30개로 설정했었고, 이로 인해 스크롤로 영상의 개수를 추가적으로 확보했어야 했다.
하지만 최근에 들어서는 로딩되는 영상의 개수 자체가 늘기도 했고 영상의 개수를 30개에서 10개로 줄였기 때문에 스크롤 부분이 필요가 없다.
즉 영상의 리스트를 확보하기만 하면 되기때문에 셀레니움 자체를 켤 필요가 없다.
다만 도커로 돌리게 되면 언어설정 자체가 영어로 되어 이를 변경하기 위해 셀레니움이 필요해진다.
언어 설정을 하나하나 클릭으로 변경해주어야 하기때문이다.
해당부분만 제외하고 다른부분은 requests로 변경이 가능하다.
다만 requests로 변경하는 과정에서 데이터를 json화 한 후 link를 가져와야해서 실제 코드 길이는 조금 더 길어지게 된다.
코드 길이가 길어짐에도 불구하고, 셀레니움을 제외하려는 이유는 메모리 사용량을 줄여보고자 함에 있다.
앞서 도커를 활용하여 스크래퍼를 대량으로 운용함에 있어서 메모리는 매우 중요한 자원이다.
셀레니움의 driver.quit()을 사용해도 결과적으로는 셀레니움창이 켜져있고 창을 조작하는 과정에서는 지속적으로 메모리를 사용하게 된다.
이는 스크래핑 속도 저하에도 영향을 끼치고 메모리 사용량으로 인해 더 많은 개수의 스크래퍼를 돌리지 못하게 될 수 있다.
<기존코드>
영상 목록 확보 코드
s_time = datetime.now()
base_url = '<https://www.youtube.com>'
recent_order_para = '/videos'
c_url = "<https://www.youtube.com/channel/UCSQ55iSysqYErLRDC6ARi9w>"
# 예시 채널로 크롬드라이버 접속
driver.get(c_url+recent_order_para)
for i in range(3):
driver.find_element(By.TAG_NAME,'body').send_keys(Keys.PAGE_DOWN)
time.sleep(1)
# 해당 채널의 30개 영상 제목, 영상 링크 수집
page = driver.page_source
soup = BeautifulSoup(page,'lxml')
channel_name = driver.find_element_by_xpath('//*[@id="text-container"]').text
follower = driver.find_element_by_css_selector('#subscriber-count').text
all_title = soup.find_all('a','yt-simple-endpoint focus-on-expand style-scope ytd-rich-grid-media')
vod_link = [base_url+n['href'] for n in all_title]
vod_link = vod_link[0:30]
<변경코드>
영상 목록 확보 코드(변경)
s_time = datetime.now()
base_url = '<https://www.youtube.com>'
recent_order_para = '/videos'
c_url = "<https://www.youtube.com/channel/UCSQ55iSysqYErLRDC6ARi9w>"
# 예시 채널로 소스코드 가져오기
res = requests.get(c_url+recent_order_para)
temp = res.text # 소스코드 텍스트로 변환
start_index = temp.find('ytInitialData') # 변환 된 텍스트에서 json화 할 위치 찾기
tmp = temp[start_index+16:]
end_index = tmp.find(';')
tmp = tmp[:end_index]
json_dict = json.loads(tmp) # json 변환
soup = BeautifulSoup(res.content, 'html.parser') # 채널명 찾기위한 뷰티불수프 활용
channel_name = soup.find('meta',property="og:title").get('content')
follower = json_dict['header']['c4TabbedHeaderRenderer']['subscriberCountText']['simpleText']
vod_link = []
temp_contents = json_dict['contents']['twoColumnBrowseResultsRenderer']['tabs'][1]['tabRenderer']
if 'content' in temp_contents.keys():
temp_contents = temp_contents['content']['richGridRenderer']['contents']
for cont in temp_contents:
if 'richItemRenderer' not in cont.keys():
continue
else:
temp_link = base_url+cont['richItemRenderer']['content']['videoRenderer']['navigationEndpoint']['commandMetadata']['webCommandMetadata']['url']
vod_link.append(temp_link)
주석을 제외하면 약 6줄 정도가 늘어나게 된다.
이제 영상 링크를 들어간 후의 코드를 변경해보도록 하겠다.
아래는 영상 링크를 들어간 후 스크래퍼 동작과정이다.
셀레니움
크롬창 생성 → 크리에이터 영상링크 접속 → 화면조작 수행(더보기 버튼 클릭)
뷰티풀스프
화면조작 수행 완료 된 페이지 소스 추출 → 추출 된 소스에서 데이터 검색 → 데이터 텍스트화
최종 결과
텍스트화 된 데이터를 전처리 하여 DB에 업로드
결과적으로는 셀레니움은 영상 하단의 더보기 버튼을 클릭하기 위해 사용된다.
그 이유는 하단의 더보기 버튼을 클릭하지 않으면 박스 안의 텍스트가 노출되지 않기 때문이다.
하지만 사실 어렵고 더럽긴 하지만 셀레니움이 없어도 해당 텍스트를 가져올 수 있는 방법이 있다.
더보기의 내용 중 일부를 페이지 소스에서 검색해보면 class로 구분되어 있지는 않지만 찾을 수는 있다.
문제는 해당 데이터를 어떻게 가져오느냐이다.
<기존코드>
더보기 클릭 후 데이터 수집 코드
creator_data = []
for i in range(len(vod_link)):
driver.get(vod_link[i])
driver.implicitly_wait(5)
pannel = driver.find_element(By.XPATH,'//*[@id="contents"]')
driver.execute_script("arguments[0].scrollBy(0,100)",pannel)
button = driver.find_element(By.ID,'expand')
button.click()
page = driver.page_source
soup = BeautifulSoup(page,'lxml')
more_text = soup.select('#description')[1].text
meta_box = soup.find_all('span',"style-scope yt-formatted-string bold")
vod_title = soup.find('h1',"style-scope ytd-watch-metadata").text.replace('\\n','')
view_cnt = meta_box[0].string
pub_dt = meta_box[2].string
creator_data.append([channel_name, follower, vod_link[i], vod_title, more_text, view_cnt, pub_dt])
pd.DataFrame(creator_data,columns=['채널명','구독자수','영상링크','영상제목','더보기내용', '조회수','업로드날짜'])
driver.quit()
<변경코드>
requests로 전체 페이지 소스코드를 긁고, 더보기 텍스트를 가져오기위해 json화 하는 코드
range_target = len(title_link)
creator_data = []
for url_idx in range(range_target):
url = title_link[url_idx]
res = requests.get(url)
temp = res.text
soup = BeautifulSoup(res.content, 'html.parser')
start_index = temp.find('ytInitialPlayerResponse')
tmp = temp[start_index+25:]
try:
end_index = tmp.find(';</script>')
tmp = tmp[:end_index]
json_dict = json.loads(tmp)
status = json_dict['playabilityStatus']['status']
more_text = json_dict['microformat']['playerMicroformatRenderer']
except:
end_index = tmp.find(';var')
tmp = tmp[:end_index]
json_dict = json.loads(tmp)
status = json_dict['playabilityStatus']['status']
more_text = json_dict['microformat']['playerMicroformatRenderer']
if 'description' in more_text.keys():
more_text = json_dict['microformat']['playerMicroformatRenderer']['description']['simpleText']
else:
more_text = ''
start_index = temp.find('ytInitialData')
tmp = temp[start_index+16:]
end_index = tmp.find(';</script>')
tmp = tmp[:end_index]
json_dict = json.loads(tmp)
temp_dict = json_dict['contents']['twoColumnWatchNextResults']['results']['results']['contents']
for k in range(len(temp_dict)):
if 'videoPrimaryInfoRenderer' in temp_dict[k].keys():
dict_idx = k
temp_dict = temp_dict[dict_idx]['videoPrimaryInfoRenderer']
vod_title = temp_dict['title']['runs'][0]['text']
view_cnt = soup.find('meta',itemprop = "interactionCount").get('content')
pub_dt = soup.find('meta',itemprop = "datePublished").get('content')
creator_data.append([channel_name, follower, vod_link[i], vod_title, more_text, view_cnt, pub_dt])
변경 코드들을 보면 왜 어렵고 더러운지 알 수 있다.
인덱스를 확인해 데이터를 잘라 json으로 만들어주어야 하기 때문이다.
또한 수집하려는 class의 글자수에 따라 인덱스를 수정해주어야 한다.
이렇게 해당 부분을 변경하게 되면 전체소스코드만 가져오는 과정을 거치면 영상의 메타데이터를 수집할 수 있다.
requests
전체 소스코드 가져오기 → json화
뷰티풀스프
전체 소스코드 또는 json에서 데이터 추출
최종 결과
텍스트화 된 데이터를 전처리 하여 DB에 업로드
최종적으로는 아래의 코드와 같이 파이썬코드를 작성하여 스크래핑을 할 수 있다.
from bs4 import BeautifulSoup
import pandas as pd
import requests
import json
base_url = '<https://www.youtube.com>'
recent_order_para = '/videos'
about_suffix = '/about'
c_url = "<https://www.youtube.com/channel/UCSQ55iSysqYErLRDC6ARi9w>"
res = requests.get(c_url+recent_order_para)
temp = res.text
start_index = temp.find('ytInitialData')
tmp = temp[start_index+16:]
end_index = tmp.find(';')
tmp = tmp[:end_index]
json_dict = json.loads(tmp)
soup = BeautifulSoup(res.content, 'html.parser')
channel_name = soup.find('meta',property="og:title").get('content')
follower = json_dict['header']['c4TabbedHeaderRenderer']['subscriberCountText']['simpleText']
vod_link = []
temp_contents = json_dict['contents']['twoColumnBrowseResultsRenderer']['tabs'][1]['tabRenderer']
if 'content' in temp_contents.keys():
temp_contents = temp_contents['content']['richGridRenderer']['contents']
for cont in temp_contents:
if 'richItemRenderer' not in cont.keys():
continue
else:
temp_link = base_url+cont['richItemRenderer']['content']['videoRenderer']['navigationEndpoint']['commandMetadata']['webCommandMetadata']['url']
vod_link.append(temp_link)
range_target = len(title_link)
creator_data = []
for url_idx in range(range_target):
url = title_link[url_idx]
res = requests.get(url)
temp = res.text
soup = BeautifulSoup(res.content, 'html.parser')
start_index = temp.find('ytInitialPlayerResponse')
tmp = temp[start_index+25:]
try:
end_index = tmp.find(';')
tmp = tmp[:end_index]
json_dict = json.loads(tmp)
status = json_dict['playabilityStatus']['status']
more_text = json_dict['microformat']['playerMicroformatRenderer']
except:
end_index = tmp.find(';var')
tmp = tmp[:end_index]
json_dict = json.loads(tmp)
status = json_dict['playabilityStatus']['status']
more_text = json_dict['microformat']['playerMicroformatRenderer']
if 'description' in more_text.keys():
more_text = json_dict['microformat']['playerMicroformatRenderer']['description']['simpleText']
else:
more_text = ''
start_index = temp.find('ytInitialData')
tmp = temp[start_index+16:]
end_index = tmp.find(';')
tmp = tmp[:end_index]
json_dict = json.loads(tmp)
temp_dict = json_dict['contents']['twoColumnWatchNextResults']['results']['results']['contents']
for k in range(len(temp_dict)):
if 'videoPrimaryInfoRenderer' in temp_dict[k].keys():
dict_idx = k
temp_dict = temp_dict[dict_idx]['videoPrimaryInfoRenderer']
vod_title = temp_dict['title']['runs'][0]['text']
view_cnt = soup.find('meta',itemprop = "interactionCount").get('content')
pub_dt = soup.find('meta',itemprop = "datePublished").get('content')
creator_data.append([channel_name, follower, vod_link[i], vod_title, more_text, view_cnt, pub_dt])
pd.DataFrame(creator_data,columns=['채널명','구독자수','영상링크','영상제목','더보기내용', '조회수','업로드날짜'])
'[신.만.추]' 카테고리의 다른 글
신입이 만드는 추천시스템-6(웹서버 구축) (0) | 2022.12.10 |
---|---|
신입이 만드는 추천시스템-5(아이템 벡터화) (0) | 2022.12.10 |
신입이 만드는 추천시스템-4(한국어 전처리 및 워드임베딩) (0) | 2022.12.09 |
신입이 만드는 추천시스템-2(데이터 수집, 스크래핑) (2) | 2022.12.09 |
신입이 만드는 추천시스템-1(개요) (0) | 2022.12.09 |