지금 크롤링을 하고 있어서 시간이 나는 김에 글을 작성합니다. 크롤링도 크롤링이지만 이 데이터를 어떻게 정제할지가 더 고민이네요. 지난 번 글들을 활용해서 작성하오니 본인의 목적에 맞게끔 수정해서 사용하면 됩니다!
기능
- 특정 유튜브 채널에서 동영상 목록의 링크를 가져오기 (채널명, 구독자수)
- 제목, 조회수, 날짜, 좋아요 수, 싫어요 수, 댓글 개수
- 댓글 크롤링 (번역 기능 추가)
- 자동번역 자막 추출
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
from selenium import webdriver
from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import time
options = webdriver.ChromeOptions() # 크롬 옵션 객체 생성
user_agent = "Mozilla/5.0 (Windows NT 4.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36 "
options.add_argument('user-agent=' + user_agent)
options.add_argument('headless') # headless 모드 설정
options.add_argument("window-size=1920x1080") # 화면크기(전체화면)
options.add_argument("disable-gpu")
options.add_argument("disable-infobars")
options.add_argument("--disable-extensions")
options.add_argument("--mute-audio") #mute
options.add_argument('--blink-settings=imagesEnabled=false') #브라우저에서 이미지 로딩을 하지 않습니다.
options.add_argument('incognito') #시크릿 모드의 브라우저가 실행됩니다.
options.add_argument("--start-maximized")
#1
prefs = {
"translate_whitelists": {"en":"ko"},
"translate":{"enabled":"true"}
}
options.add_experimental_option("prefs", prefs)
#2
prefs = {
"translate_whitelists": {"your native language":"ko"},
"translate":{"enabled":"True"}
}
options.add_experimental_option("prefs", prefs)
#3
options.add_experimental_option('prefs', {'intl.accept_languages': 'ko,ko_kr'})
|
cs |
기본 셀레니움 webdriver 세팅입니다. prefs 기능은 영어를 번역할 때 필요한 기능이라서 끄셔도 상관 없습니다. 그리고 처음에 어떻게 돌아가는지 궁금하시면 # options.add_argument('headless') headless 기능을 꺼주세요.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
import os
import pandas as pd
import winsound
ytb = pd.read_csv('youtube_link.csv')
ytb_link = ytb.link.to_list()
for i in ytb_link :
driver = webdriver.Chrome('chromedriver.exe', options= options)
driver.get(i)
# 스크롤 다운
time.sleep(1.5)
endkey = 4 # 90~120개 / 늘릴때 마다 30개
while endkey:
driver.find_element_by_tag_name('body').send_keys(Keys.END)
time.sleep(0.3)
endk -= 1
channel_name = driver.find_element_by_xpath('//*[@id="text-container"]').text
subscribe = driver.find_element_by_css_selector('#subscriber-count').text
channel_name = re.sub('[=+,#/\?:^$.@*\"※~&%ㆍ!』\\‘|\(\)\[\]\<\>`\'…《\》]', '', channel_name)
# print(channel_name,subscribe)
# bs4 실행
html = driver.page_source
soup = BeautifulSoup(html, 'lxml')
video_list0 = soup.find('div', {'id': 'contents'})
video_list2 = video_list0.find_all('ytd-grid-video-renderer',{'class':'style-scope ytd-grid-renderer'})
base_url = 'http://www.youtube.com'
video_url = []
# 반복문을 실행시켜 비디오의 주소를 video_url에 넣는다.
for i in range(len(video_list2)):
url = base_url+video_list2[i].find('a',{'id':'thumbnail'})['href']
video_url.append(url)
driver.quit()
if subscribe :
channel = channel_name + ' - ' + subscribe
else :
channel = channel_name
directory = f'data/{channel}/subtitle'
if not os.path.exists(directory):
os.makedirs(directory)
print(channel, len(video_url))
ytb_info(video_url, channel)
print()
winsound.PlaySound('sound.wav', winsound.SND_FILENAME)
|
cs |
ytb_link : 본인이 수집하고자하는 채널을 리스트 형식으로 만들어주세요. 저는 csv 파일로 만들어서 컬럼 이름을 'link'로 하여 생성을 했습니다.
channel : 채널 이름으로 폴더를 만들기 때문에, 폴더 이름에 들어가면 오류가 생기는 부호들을 미리 전처리 합니다. subtitle까지 만든 건 미리 자막 파일을 저장할 수 있는 폴더도 같이 만들어놨습니다.
# 한 채널이 끝날 때마다 윈도우 플레이사운드로 알려줍니다. 시끄럽다고 생각하시면 끄면 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
|
import time
last_page_height = driver.execute_script("return document.documentElement.scrollHeight")
while True:
driver.execute_script("window.scrollTo(0, document.documentElement.scrollHeight);")
time.sleep(0.5)
if new_page_height == last_page_height:
break
last_page_height = new_page_height
time.sleep(0.75)
|
cs |
endkey : 본인이 수집하고자 하는 채널의 링크 개수를 결정합니다. 현재 설정으로는 90~120개를 수집합니다. time.sleep(2)으로 설정하시면 180개까지 크롤링을 합니다. endkey 개수를 늘리면 30개씩 추가가 됩니다. 에라 모르겠다하고 모든 링크를 크롤링하시려면 위에 코드를 입력해주세요.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
|
# 정보만 크롤링하고 싶을 때
from bs4 import BeautifulSoup
import pyautogui
import pandas as pd
import re
def ytb_info2(video_url,channel) :
print(f'{channel}',' 크롤링 시작')
driver = webdriver.Chrome('C:/work/python/Asia_GAN/myproject/youtube/chromedriver.exe', options= options)
#데이터 넣을 리스트
date_list = []
title_list = []
view_list = []
like_list = []
dislike_list = []
comment_list = []
#각 채널별 영상으로 크롤링
for i in range(len(video_url)):
start_url = video_url[i]
print(start_url, end= ' / ')
driver.get(start_url)
driver.implicitly_wait(1.5)
body = driver.find_element_by_tag_name('body')
#댓글 null 값 방지
num_of_pagedowns = 2
while num_of_pagedowns:
body.send_keys(Keys.PAGE_DOWN)
time.sleep(0.5)
num_of_pagedowns -= 1
time.sleep(0.5)
#크롤링 요소
try :
info = driver.find_element_by_css_selector('.style-scope ytd-video-primary-info-renderer').text.split('\n')
if '인기 급상승 동영상' in info[0] :
info.pop(0)
elif '#' in info[0].split(' ')[0] :
info.pop(0)
title = info[0]
divide = info[1].replace('조회수 ','').replace(',','').split('회')
view = divide[0]
date = divide[1].replace(' ','')
like = info[2]
dislike = info[3]
driver.implicitly_wait(1)
try:
comment = driver.find_element_by_css_selector('#count > yt-formatted-string > span:nth-child(2)').text.replace(',','')
except:
comment = '댓글x'
#리스트에 추가
title_list.append(title)
view_list.append(view)
date_list.append(date)
like_list.append(like)
dislike_list.append(dislike)
comment_list.append(comment)
# 크롤링 정보 저장
new_data = {'date':date_list, 'title':title_list, 'view':view_list, 'comment': comment_list, 'like':like_list, 'dislike':dislike_list}
df = pd.DataFrame(new_data)
df.to_csv(f'data/{channel}/{channel}.csv', encoding='utf-8-sig')
except :
continue
# 확인용
print(title, view, date, like, dislike, comment)
driver.quit()
|
cs |
자막과 댓글이 필요 없을 경우
제목, 날짜, 조회수, 좋아요 수, 싫어요 수, 댓글 수만 크롤링을 합니다. 정보 양이 많지 않기 때문에 셀레니움만으로도 가능합니다. html_source를 bs4로 넘겼을 때와 비교해도 얼마 차이가 나지 않습니다.
# print(title, view, date, like, dislike, comment) 만약 어떤 정보가 나오는지 확인할 필요가 없으시면 비활성화해주세요.
나는 댓글과 자막도 필요하신 분들은
밑으로
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
from youtube_transcript_api import YouTubeTranscriptApi
from konlpy.tag import Kkma
from pykospacing import Spacing
def ytb_subtitle(start_url, title) :
try:
code = start_url.split('=')[1]
srt = YouTubeTranscriptApi.get_transcript(f"{code}", languages=['ko']) #한글로, 딕셔너리 구조
text = ''
for i in range(len(srt)):
text += srt[i]['text'] + ' '
text_ = text.replace(' ','')
#문장 분리 / kss 사용해도 무방
kkma = Kkma()
text_sentences = kkma.sentences(text_)
#종결 단어
lst = ['죠','다','요','시오', '습니까','십니까','됩니까','옵니까','뭡니까',]
df = pd.read_csv('not_verb.csv',encoding='utf-8')
not_verb = df.stop.to_list()
#단어 단위로 끊기
text_all = ' '.join(text_sentences).split(' ')
for n in range(len(text_all)) :
i = text_all[n]
if len(i) == 1 : #한글자일 경우 추가로 작업x
continue
else :
for j in lst : #종결 단어
#질문형
if j in lst[4:]:
i += '?'
#명령형
elif j == '시오' :
i += '!'
#마침표
else :
if i in not_verb : #특정 단어 제외
continue
else :
if j == i[len(i)-1] : #종결
text_all[n] += '.'
spacing = Spacing()
text_all_in_one = ' '.join(text_all)
text_split = spacing(text_all_in_one.replace(' ','')).split('.')
text2one= []
for t in text_split:
text2one.append(t.lstrip())
w = '. '.join(text2one)
f = open(f'data/{channel}/subtitle/{title}.txt','w',encoding='utf-8')
f.write(w)
f.close()
print('O')
except:
print('X')
|
cs |
유튜브 자막 추출 다운과 관련해서는 이전 글을 참고해주시면 좋을 것 같습니다. not_verb.csv 파일의 경우 '다', '요'로 끝나는 단어 중 동사가 아닌 명사, 형용사 단어를 stop 컬럼으로 추가하시면 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
|
# 영어 번역 없음
import winsound as sd
from bs4 import BeautifulSoup
import pyautogui
import pandas as pd
import re
def beepsound():
fr = 2000 # range : 37 ~ 32767
du = 1000 # 1000 ms ==1second
sd.Beep(fr, du) # winsound.Beep(frequency, duration)
def ytb_info(video_url,channel) :
print(f'{channel}',' 크롤링 시작')
driver = webdriver.Chrome('chromedriver.exe', options= options)
# new_data = {'date': '', 'title': '', 'view': '', 'comment': '', 'like':'', 'dislike':''}
count = 1
#데이터 넣을 리스트
date_list = []
title_list = []
view_list = []
like_list = []
dislike_list = []
comment_list = []
try:
#각 채널별 영상으로 크롤링
for i in range(len(video_url)):
start_url = video_url[i]
print(start_url, end= ' / ')
driver.get(start_url)
driver.implicitly_wait(1.5)
body = driver.find_element_by_tag_name('body')
#댓글 null 값 방지
num_of_pagedowns = 1
while num_of_pagedowns:
body.send_keys(Keys.PAGE_DOWN)
time.sleep(0.5)
num_of_pagedowns -= 1
driver.implicitly_wait(1)
#크롤링 요소
try :
info = driver.find_element_by_css_selector('.style-scope ytd-video-primary-info-renderer').text.split('\n')
if '인기 급상승 동영상' in info[0] :
info.pop(0)
elif '#' in info[0].split(' ')[0] :
info.pop(0)
title = info[0]
divide = info[1].replace('조회수 ','').replace(',','').split('회')
view = divide[0]
date = divide[1].replace(' ','')
like = info[2]
dislike = info[3]
try:
comment = driver.find_element_by_css_selector('#count > yt-formatted-string > span:nth-child(2)').text.replace(',','')
except:
comment = '댓글x'
#리스트에 추가
title_list.append(title)
view_list.append(view)
date_list.append(date)
like_list.append(like)
dislike_list.append(dislike)
comment_list.append(comment)
# 크롤링 정보 저장
new_data = {'date':date_list, 'title':title_list, 'view':view_list, 'comment': comment_list, 'like':like_list, 'dislike':dislike_list}
df = pd.DataFrame(new_data)
df.to_csv(f'data/{channel}/-{channel}.csv', encoding='utf-8-sig')
except :
continue
# print(title, view, date, like, dislike, comment)
num_of_pagedowns = 1
while num_of_pagedowns:
body.send_keys(Keys.PAGE_DOWN)
time.sleep(0.5)
num_of_pagedowns -= 1
#페이지 다운
last_page_height = driver.execute_script("return document.documentElement.scrollHeight")
while True:
driver.execute_script("window.scrollTo(0, document.documentElement.scrollHeight);")
# driver.implicitly_wait(2) #오류남
time.sleep(0.5)
new_page_height = driver.execute_script("return document.documentElement.scrollHeight")
if new_page_height == last_page_height:
break
last_page_height = new_page_height
# driver.implicitly_wait(1)
time.sleep(0.75)
time.sleep(0.5)
# 댓글 크롤링
html = driver.page_source
soup = BeautifulSoup(html, 'lxml')
users = soup.select("div#header-author > h3 > #author-text > span")
comments = soup.select("yt-formatted-string#content-text")
user_list=[]
review_list=[]
for i in range(len(users)):
str_tmp = str(users[i].text)
str_tmp = str_tmp.replace('\n', '')
str_tmp = str_tmp.replace('\t', '')
str_tmp = str_tmp.replace(' ','')
str_tmp = str_tmp.replace(' ','')
user_list.append(str_tmp)
str_tmp = str(comments[i].text)
str_tmp = str_tmp.replace('\n', '')
str_tmp = str_tmp.replace('\t', '')
str_tmp = str_tmp.replace(' ', '')
review_list.append(str_tmp)
# 댓글 추가
pd_data = {"ID":user_list, "Comment":review_list}
youtube_pd = pd.DataFrame(pd_data)
title = re.sub('[-=+,#/\?:^$.@*\"※~&%ㆍ!』\\‘|\(\)\[\]\<\>`\'…《\》]', '', title)
youtube_pd.to_csv(f"data/{channel}/{title}.csv", encoding = 'utf-8-sig')#,index_col = False)
print('ㅁ',end='')
# 자막 추출
ytb_subtitle(start_url, title)
# 광고 끄기
if count :
# time.sleep(1)
try:
driver.implicitly_wait(0.5)
driver.find_element_by_css_selector("#main > div > ytd-button-renderer").click()
count -=1
except:
continue
except :
driver.quit()
beepsound()
driver.quit()
beepsound()
|
cs |
기본 정보 / 댓글 / 자막까지
기본 정보 크롤링 밑으로 추가된 기능은 스크롤 다운 후, html page_source를 bs4로 넘겨서 댓글을 크롤링 합니다. 양이 많기 때문에 셀레니움보다 가볍고 빠른 bs4를 사용하시는 것을 추천드립니다.
댓글을 다 크롤링하고, 자막까지 받았을 때 영상 1개당 33초 정도 걸렸습니다. 컴퓨터, 인터넷 사양에 따라서 다를 거라 생각합니다. 한 채널이 끝날 때마다 소리가 나게 했습니다. 필요 없으면 꺼주세요!
*주의사항 *
유튜브 댓글은 기본적으로 인기 댓글순으로 정렬이 되어있기 때문에, 뒤에 있는 댓글일수록 공감을 적게 받거나 관심이 적은 댓글일 확률이 높습니다. 저는 모든 댓글이 필요하지 않기 때문에, 가장 크롤링이 빠르면서 댓글들 정보를 모을 수 있게 시간 설정을 했습니다. 댓글이 적으면 모든 댓글을 크롤링하지만, 많아지면 60~90% 정도만 크롤링을 하게 됩니다.
모든 댓글들이 필요하신 분들은, time.sleep을 1초 이상으로 해주세요. driver.implicitly_wait의 경우 스크롤은 내려가는데 댓글들이 로딩이 되지 않는 경우가 있어서 time.sleep을 사용했습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
|
#영어 번역
import pyautogui
import pandas as pd
import re
def ytb_info(video_url,channel) :
print(f'{channel}',' 크롤링 시작')
driver = webdriver.Chrome('chromedriver.exe', options= options)
df = pd.DataFrame()
count = 1
#각 채널별 영상으로 크롤링
for i in range(len(video_url)):
start_url = video_url[i]
print(start_url, end= '/ ')
driver.implicitly_wait(1)
driver.get(start_url)
#영어 번역
pyautogui.hotkey('shift','F10')
for i in range(7):
pyautogui.hotkey('down')
pyautogui.hotkey('enter')
body = driver.find_element_by_tag_name('body')
#댓글 null 값 방지
num_of_pagedowns = 1
while num_of_pagedowns:
body.send_keys(Keys.PAGE_DOWN)
time.sleep(.75)
num_of_pagedowns -= 1
driver.implicitly_wait(1)
#크롤링 요소
info = driver.find_element_by_css_selector('.style-scope ytd-video-primary-info-renderer').text.split('\n')
if '인기 급상승 동영상' in info[0] :
info.pop(0)
elif '#' in info[0].split(' ')[0] :
info.pop(0)
title = info[0]
divide = info[1].replace('조회수 ','').replace(',','').split('회')
view = divide[0]
date = divide[1].replace(' ','')
like = info[2]
dislike = info[3]
try:
comment = driver.find_element_by_css_selector('#count > yt-formatted-string > span:nth-child(2)').text.replace(',','')
except:
comment = '댓글x'
# 크롤링 정보 저장
new_data = {'date':date, 'title':title, 'view':view, 'comment': comment, 'like':like, 'dislike':dislike}
df = df.append(new_data, ignore_index=True)
df.to_csv(f'data/{channel}/{channel}.csv', encoding='utf-8-sig')
# print(title, view, date, like, dislike, comment)
#페이지 다운
last_page_height = driver.execute_script("return document.documentElement.scrollHeight")
while True:
driver.execute_script("window.scrollTo(0, document.documentElement.scrollHeight);")
time.sleep(1)
new_page_height = driver.execute_script("return document.documentElement.scrollHeight")
if new_page_height == last_page_height:
break
last_page_height = new_page_height
time.sleep(1)
time.sleep(0.5)
#댓글 크롤링
review_list = []
user_list =[]
reviews = driver.find_elements_by_css_selector('#content-text')
users = driver.find_elements_by_css_selector('h3.ytd-comment-renderer a span')
num = 0
for i in range(len(users)):
review = reviews[i].text.replace('\n', ' ')
review_list.append(review)
user = users[i].text
user_list.append(user)
# 댓글
pd_data = {"ID":user_list, "Comment":review_list}
youtube_pd = pd.DataFrame(pd_data)
title = re.sub('[-=+,#/\?:^$.@*\"※~&%ㆍ!』\\‘|\(\)\[\]\<\>`\'…《\》]', '', title)
youtube_pd.to_csv(f"data/{channel}/{title}.csv", encoding = 'utf-8-sig')
print('ㅁ',end='')
# 자막 추출
ytb_subtitle(start_url, title)
# 광고 끄기
if count :
# time.sleep(1)
try:
driver.implicitly_wait(0.5)
driver.find_element_by_css_selector("#main > div > ytd-button-renderer").click()
count -=1
except:
continue
driver.quit()
|
cs |
해외 번역
단점 : headless으로 하면 안 된다. 마우스를 사용하지 못 한다. 시간이 진짜아아아아 엄처어어어엉 오래 걸린다. 굳이 이렇게 안 해도 될 거라고 생각이 드는데 혹시나 필요하신 분들을 위해서 남긴다.
가장 문제가 되는 부분이 번역을 한 정보는 bs4로 넘어가지 않는다. 셀레니움으로 모든 댓글과 닉네임들을 모아야 하기 때문에 시간이 오래 걸리는 것이다.
이 데이터들을 어떻게 사용할 것인지는 아직까지는 비밀.
'할 수 있다. 파이썬' 카테고리의 다른 글
(3) 실무 엑셀 함수 VLOOKUP, INDEX MATCH 시간 50배 단축, 파이썬으로 한 방에 잡자 (0) | 2022.01.21 |
---|---|
(2) 파이썬 엑셀 사무 자동화 : 보안 걸린 엑셀 한 번에 뚫기 openpyxl? xlwings? (2) | 2022.01.20 |
(1) 파이썬 엑셀 사무 자동화 : 회사 사내망 때문에 좌절한 당신... (0) | 2022.01.19 |
유튜브 크롤링(2) - ㄹㅇ 초간단 유튜브 자막 다운 & 추출 (문장분리까지) (1) | 2021.09.28 |
유튜브 크롤링(1) - 셀레니움 페이지 자동 번역, api 번역기 없이 가능! (키 입력, 마우스 입력) (2) | 2021.09.14 |