파이썬을 이용한 크롤링에서 주로 많이 쓰고 있는 라이브러리인 셀레니움과 BeautifulSoup을 이용해서 유튜브 채널 커뮤니티의 댓글 크롤링을 진행해보았다.
유튜브 동영상의 댓글같은 경우 GCP(Google Cloud Platform)에서 제공하는 Youtube Data API
를 이용하면 따로 셀레니움이나 뷰티풀숲을 이용하지 않아도 댓글 수집이 가능하다.
수집할 수 있는 데이터는 댓글 작성자, 내용, 좋아요 수, 작성 시간 등으로 더 알고 싶다면 공식 문서(링크)를 참고하셔요.
근데 Youtube Data API
는 특정 채널의 커뮤니티 게시글의 댓글 내용은 불러오지 못한다. 아직 업데이트가 안 된 건지 아니면 예정이 없는 것인지, 그것도 아니면 내가 못 찾은 건지 모르겠으나...
어쩔 수 없이 파이썬 크롤링 입문이면 무난하게 제시되는 방법으로 진행했다.
여기에 더해서 댓글 작성자 중 두 명을 뽑아 추첨하는 기능이 필요했기 때문에 해당 기능도 작성했다.
pip 명령어를 이용하여 총 3가지의 라이브러리를 설치한다.
pip install selenium
pip install beautifulsoup4
pip install ChromeDriverManager
크롬은 굉장히 업데이트가 잦은 브라우저 중 하나인데, 업데이트로 인하여 버전이 바뀌면 종종 selenium의 크롬을 실행하는 webdriver 선에서 문제가 생기는 경우가 있다.
그걸 방지하기 위해서 ChromeDriverManager를 설치하고, 서비스 옵션으로 설정해줄 것이다.
import time
import random
from bs4 import BeautifulSoup
from datetime import datetime
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from webdriver_manager.chrome import ChromeDriverManager
time
은 time.sleep() 메소드를 이용하여 자동화 활동 방지를 위해서 넣었는데, 지금 생각해보니 이 프로그램에서는 그럴 필요가 없는 것 같으니 빼도 무방하다.
random
은 앞서 말한 추첨을 위해 랜덤하게 값을 추출하기 위해서 import 하였다.
datetime
프로그램 시작 시간을 출력하기 위해서 넣은 것이므로 빼도 역시 빼도 무방하다.
options = webdriver.ChromeOptions()
options.add_argument("--disable-logging")
options.add_argument("--disable-blink-features=AutomationControlled")
options.add_argument("headless")
# 크롬 드라이버 설정
chrome_service = Service(ChromeDriverManager().install())
# 웹 드라이버 생성
driver = webdriver.Chrome(options = options, service = chrome_service)
각종 옵션값을 설정한다. 나의 경우엔 저 부분들을 설정하지 않으면 오류가 났기 때문에 적어둔 것이므로 테스트 해보고 목적과 환경에 맞게 적당히 빼는 것을 추천한다.
쓴 옵션 값들을 정리하면
--disable-logging
: 쓰잘데 없는 로깅을 비활성화하는 옵션--disable-blink-features=AutomationControlled
: 자동화 동작 감지 기능을 비활성화하는 옵션headless
: 프로그램 실행 시 화면에 창이 띄워지지 않는 모드 (헤드리스 모드)로 실행하는 옵션그리고 크롬 드라이버가 자동으로 인스톨 되도록 설정한 후 위에서 설정된 옵션과 서비스를 적용하여 웹 드라이버를 생성한다.
driver.get('[url]')
유튜브 동영상과 마찬가지로 커뮤니티 게시글의 경우도 댓글이 무한 스크롤 방식이기 때문에 전체 댓글을 모두 불러오기 위하여 셀레니움을 이용하여 무한 스크롤을 끝까지 내린다.
# 현재 페이지의 높이를 기록
last_height = driver.execute_script("return document.documentElement.scrollHeight")
# 무한 스크롤 루프를 시작
while True:
# 스크롤을 내린다.
driver.execute_script("window.scrollTo(0, document.documentElement.scrollHeight);")
time.sleep(1.5)
# 새로운 페이지의 높이를 기록한다.
new_height = driver.execute_script("return document.documentElement.scrollHeight")
# 현재 스크롤의 높이와 이전 스크롤의 높이가 같으면 무한 스크롤이 끝났다고 판단하고 루프를 빠져나온다.
if new_height == last_height:
break
# 현재 높이를 새로운 높이로 변경한다.
last_height = new_height
html_source = driver.page_source
soup = BeautifulSoup(html_source, 'html.parser')
list_raw = soup.select("#author-text > yt-formatted-string")
list_raw
에 담는다.나는 작성자의 ID가 필요했기 때문에 id에 해당하는 요소들만 긁어왔지만, 목적에 따라 댓글 내용도 수집이 가능하다.
크롬 개발자 도구의 html 소스를 볼 수 있는 Elements
탭에서 CSS Selector를 이용해서 작성자의 ID 요소를 선택할 수 있는데... 솔직히 CSS도 잘 모르고 셀렉터?도 잘 몰라서 어떻게 설명할지...ㅠ
특정 class의 위치를 짚어서 같은 위치에 존재하는 태그 요소를 전부 가져오는 것으로 이해했다.
그리고 이래저래 찾아보니 셀렉터로 크롤링 하는 건 너무 짜친다는 얘기도 있었다.
여러 페이지에서 크롤링을 진행해보면서 생각해봤을 때 그 이유는
1) 페이지가 생성될 때마다 class명이 랜덤한 문자열로 생성되는 경우
2) 페이지가 생성될 때마다 class명이 변경되는 경우
3) 서비스 업데이트로 페이지 구조 자체가 변경되어서 CSS Selector가 변경되는 경우
등이 있었다.
그래서 많이 권장하는 XPath
를 이용한 크롤링으로 진행하려 했는데 이 땐 시간이 없어서 어쩔 수 없이 셀렉터로 진행함... 데헷ㅎㅎ
여튼 list_raw 에는 위의 CSS Selector에 해당하는 태그 값들이 리스트 형태로 들어오게 된다.
excluded_ids = ["userID1", "userID2", "userID3"]
id_list = []
for i in range(len(list_raw)):
temp_id = list_raw[i].text
temp_id = temp_id.replace('\n', '')
temp_id = temp_id.replace('\t', '')
temp_id = temp_id.replace(' ', '')
if temp_id in excluded_ids:
continue
id_list.append(temp_id) # 댓글 작성자
id_list = list(set(id_list))
print('추첨을 시작합니다.')
result = random.sample(id_list, 2)
time.sleep(5)
output_format = "❤️ 당첨자: {}".format(", ".join(result))
print(output_format)
추첨에서 제외하고 싶은 유저 아이디를 excluded_ids
리스트에 넣는다.
for 반복문은 태그 형태로 이루어진 list_raw를 가공하기 위한 반복문이다.
text부분을 추출하고, 개행과 탭 그리고 공백 등을 제거하고
남는 temp_id
가 excluded_ids
리스트에 있는 요소들과 일치하는 지 여부를 판단,
아니라면 최종적으로 id_list
리스트에 temp_id
를 추가한다.
그 뒤 set을 이용하여 중복제거를 한 후 다시 리스트 형태로 id_list
를 되돌린다.
그 후에 random
모듈 중 random.sample()
메소드를 이용하여 요소들 중 n개의 값을 랜덤으로 추출한다.
random.sample()
: 리스트나 시퀀스에서 무작위로 일정 갯수의 요소를 추출, 이 코드에서는 id_list
리스트에서 2개의 요소를 무작위로 추출하고 result
에 저장한다.이외 코드는 추첨 결과를 출력하는 코드들이다.
import time
import random
from bs4 import BeautifulSoup
from datetime import datetime
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from webdriver_manager.chrome import ChromeDriverManager
options = webdriver.ChromeOptions()
options.add_argument("--disable-logging")
options.add_argument("--disable-blink-features=AutomationControlled")
options.add_argument("headless")
# 크롬 드라이버 설정
chrome_service = Service(ChromeDriverManager().install())
# 웹 드라이버 생성
driver = webdriver.Chrome(options = options, service = chrome_service)
driver.get('url')
# 프로그램 시작 시간
start_time = datetime.now()
start_time = start_time.strftime("%Y-%m-%d %H:%M:%S")
print(f'프로그램 시작({start_time})\n')
# 총 댓글 갯수 불러오기
cnt_xpath = "//h2[@id='count']//span[2]"
load_yn = WebDriverWait(driver, 10).until(EC.presence_of_element_located((
By.XPATH, cnt_xpath
)))
if load_yn:
comments_cnt = driver.find_element(By.XPATH, "//h2[@id='count']//span[@dir='auto'][2]").text
print(f'총 댓글 갯수: {int(comments_cnt)}')
# 무한 스크롤의 끝까지 스크롤 내리기
# 현재 페이지의 높이를 기록
last_height = driver.execute_script("return document.documentElement.scrollHeight")
# 무한 스크롤 루프를 시작
while True:
# 스크롤을 내린다.
driver.execute_script("window.scrollTo(0, document.documentElement.scrollHeight);")
time.sleep(1.5)
# 새로운 페이지의 높이를 기록한다.
new_height = driver.execute_script("return document.documentElement.scrollHeight")
# 현재 스크롤의 높이와 이전 스크롤의 높이가 같으면 무한 스크롤이 끝났다고 판단하고 루프를 빠져나온다.
if new_height == last_height:
break
# 현재 높이를 새로운 높이로 변경한다.
last_height = new_height
html_source = driver.page_source
soup = BeautifulSoup(html_source, 'html.parser')
list_raw = soup.select("#author-text > yt-formatted-string")
excluded_ids = ["userID1", "userID2", "userID3"]
id_list = []
for i in range(len(list_raw)):
temp_id = list_raw[i].text
temp_id = temp_id.replace('\n', '')
temp_id = temp_id.replace('\t', '')
temp_id = temp_id.replace(' ', '')
if temp_id in excluded_ids:
continue
id_list.append(temp_id) # 댓글 작성자
id_list = list(set(id_list)) # 중복 제거
print('추첨을 시작합니다.')
result = random.sample(id_list, 2)
time.sleep(5)
output_format = "❤️ 당첨자: {}".format(", ".join(result))
print(output_format)
프로그램 시작 시간을 추가했고,
댓글이 얼마나 달렸는지 갯수가 궁금하다는 의견이 있어서 추가했다. 여기서는 XPath를 이용했다. 이용 방법은 CSS Selector와 비슷하니 XPath를 이용한 크롤링
등의 키워드로 검색해보길 바란다.
친구가 쓸 일이 있다고 해서 하루만에 급하게 만드느라 누더기 코드가 되었다.
그래도 작동은 하지만 몇가지 아쉬운 점은 역시 유튜브 공식 API로 불러오는게 아니기 때문에 불러올 수 없는 데이터 혹은 불러오기 복잡한 데이터들이 있다는 것이다.
좋아요 여부나 like가 몇 개 있는지, 댓글을 쓴 일시가 정확히 언제인지 등을 알 수가 없어서 이 이상 정교한 작업은 불가능했다.
또 위에서 언급했듯이 xpath를 이용한 크롤링도 가능할 것 같은데 시간문제로 하지 못해서 많이 아쉬움이 남는 코드가 되었다...
하지만 이러나저러나 무사히 작동하니까 OK지롱ㅎㅎㅎ