2018. 10. 8. 13:51ㆍProgramming/Python
들어가기 전에
YouTube에 공유되는 영상들은 모두 저작권을 가지고 있는 영상입니다. 이를 다운로드 받아 무단으로 배포하거나, 저작권자의 허락 없이 임의로 수정하여 사용할 경우 법적 책임을 물을 수 있습니다. 이 포스트에서 YouTube 영상을 다운로드 받는 방법을 설명하고는 있으나, 이에 대해 발생하는 문제에 대해서는 저는 책임지지 않습니다.
Playlist 사용해보기
사실 처음에 이 포스트를 작성할 때는 PyTube에 Playlist 다운로드 기능까지 구현되어 있는 줄은 몰랐습니다. 그래서 웹 크롤링으로 뻘짓을 했었는데, 굳이 그럴 필요가 없더군요.
Playlist 객체는 Youtube 객체와 사용 방법이 비슷합니다.
>>> from pytube import Playlist >>> pl = Playlist("https://www.youtube.com/watch?v=NtLNPueUdGk&t=5s&list=PLFkwr6HjQax2KPy8Zhs7VTvFwJD8E2eka&index=2") >>> pl.download_all()
Playlist 객체를 생성할 때 인자로 해당 플레이리스트가 들어간 url을 입력하면 이 Playlist는 알아서 url을 파싱하여 해당 플레이리스트에 속하는 모든 비디오 클립 url을 Youtube 객체로 생성합니다. 단, 유튜브에서 플레이리스트를 볼 때 플레이리스트 안의 클립 수가 200을 넘어가면 더 이상 보여주지 않으므로 아마도 이 경우엔 전부 찾아서 추가하진 않을 것입니다. PyTube Github페이지에서는 이 제한도 해제하는 버전도 Pull Request에 등록되어 있던데 정식 버전에 들어가 있는 지는 모르겠네요.
pl.download_all()을 실행하면 객체가 생성될 때 만들어 놓은 Youtube 객체들을 차례로 다운 받습니다. 이 때, 다운로드 되는 설정은 해당 Youtube 객체의 Steam 중 Progressive 옵션으로 가장 높은 화질로 다운로드 받게 됩니다. 즉, 최대 720p의 화질로 자동 다운로드가 된다는 이야기죠. 아직까진 이 세부 설정을 손대는 옵션은 PyTube에 없습니다. 향후 패치로 추가될 수도 있을 것이라고 봅니다.
또한, pl.download_all('/path/video') 식으로 직접 다운로드 할 경로를 지정해서 받을 수도 있습니다.
웹 크롤링으로 Playlist 정보 얻어오기
이 방법은 제가 PyTube에 Playlist 객체가 있다는 걸 몰랐을 때 사용한 방법입니다. 위에서 언급한 Playlist의 download_all()이 세부 설정을 손댈 수 없다는 문제점을 해결할 수 있단 점에서는 좀 더 낫다고도 볼 수 있습니다만, 일단 웬만해선 그 세부 설정을 손댈 일도 없고, PyTube 자체에서도 조만간 그 문제점이 해결될 것으로 보이니 아래 글은 읽을 필요 없을지도 모르겠습니다.
파이썬에서 웹 크롤링을 하기 위한 방법은 여러가지가 있습니다만, 여기선 지난번 이마트 휴점일 크롤링 때와 똑같이 requests를 써보도록 하겠습니다.
import requests
request 라이브러리가 없다면 pip로 받을 수 있습니다. 이 때, request가 아닌 requests라는 점에 유의하세요. pip에는 이 2개가 다 있습니다.
특정 url에 request를 보내고 그 response를 받아오는 방법은 다음과 같습니다.
res = requests.get('https://www.youtube.com/watch?v=NtLNPueUdGk&t=5s&list=PLFkwr6HjQax2KPy8Zhs7VTvFwJD8E2eka&index=2') print(res.text)
res로 받아온 것을 그대로 print하면 다음과 같은 결과가 나옵니다.
굉장히 깁니다. 그런데, 이 방식으로 얻어낸 소스코드는 실제 브라우저에서 해당 url에 접속하여 열어본 소스코드와 다릅니다. 아무래도 브라우저를 통한 접속이 아닐 경우엔 유튜브에서 로봇으로 간주하여 조금 다른 response를 주는 게 아닌가 싶습니다.
실제 브라우저에서 소스 코드를 분석해보면, 자바스크립트 단에서 window["InitialData"] 부분에 Playlist에 속하는 videoId가 저장되어 있습니다. 그러나, requests를 통해 얻어온 response에선 발견이 되지 않습니다.
이 문제를 해결하기 위해서 저는 request를 넣을 때 Header 정보로 User-agent 값을 넣어줬습니다.
headers = { 'User-Agent' : 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36' } res = requests.get('https://www.youtube.com/watch?v=NtLNPueUdGk&t=5s&list=PLFkwr6HjQax2KPy8Zhs7VTvFwJD8E2eka&index=2', headers=headers)
실제 제 Chrome에서 넣은 Request Header의 User-Agent부분만을 복사하여 붙여넣었습니다. 이렇게 헤더 설정을 한 뒤에 Requests의 get()에 인자로 넣어주면 유튜브에서도 제대로 브라우저 접속으로 판정하고 제대로 된 소스코드를 돌려줍니다.
이 부분만 해결한다면 나머지는 사실상 막노동입니다. 아래에 완성본 소스코드를 첨부하겠습니다.
from pytube import YouTube from bs4 import BeautifulSoup import requests import sys import json import os import time workdir = os.path.dirname(os.path.realpath(__file__)) sys.stdout.write('url : ') url = sys.stdin.readline().rstrip() #받고 싶은 playlist가 속의 클립 url을 입력받습니다. headers = { 'User-Agent' : 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36' } res = requests.get(url, headers=headers) source = res.text #Response의 바디를 source라는 변수에 저장합니다. 이는 Raw Text 입니다. soup = BeautifulSoup(source, 'html.parser') #BeautifulSoup로 Response 값을 분석합니다. scripts = soup.find_all('script') # <script> 태그가 있는 부분만 찾아내어 Set으로 반환합니다. found_i = -1 for (i, x) in enumerate(scripts): if 'window["ytInitialData"] = ' in str(x): #ytInitialData가 담긴 객체를 검색합니다. found_i = i break if found_i < 0: print('Cannot find playlist') #만일 ytInitialData가 담긴 객체를 찾지 못했다면 그냥 종료합니다. exit() raw_data = scripts[found_i].get_text() str1 = raw_data.strip().split('window["ytInitialData"] = ')[1].split(';\n')[0] #분석할 수 있도록 중간 부분만을 슬라이스해서 잘라옵니다. j1 = json.loads(str1, encoding='utf8', strict=False) #String을 JSON Parsing하여 파이썬에서 사용할 수 있도록 해줍니다. #이 때 strict를 false로 하지 않으면 인코딩 문제로 loads()가 제대로 실행되지 않을 수 있습니다. toGet = [] for i in j1['contents']['twoColumnWatchNextResults']['playlist']['playlist']['contents']: if 'unplayableText' not in i['playlistPanelVideoRenderer']: #비공개된 동영상일 경우엔 videoId가 나오지 않고 unPlayableText가 표시됩니다. toGet.append(i['playlistPanelVideoRenderer']['videoId']) for i in toGet: getStr = 'https://www.youtube.com/watch?v=' + i yt = YouTube(getStr) file_name = yt.title print('Downloading %s %s' % (file_name, time.time())) yt.streams.filter(progressive=True, file_extension='mp4', only_audio=False).order_by('resolution').desc().first().download()
다음엔 adaptive로 video와 audio를 각각 따로 받아 ffmpeg으로 결합하는 과정을 설명하겠습니다.
2019. 02. 13 추가 : 위의 웹 크롤링 코드는 변경되었습니다. 새로 짜여진 코드는 header를 별도로 설정하지 않으며, 코드가 더 간결해졌습니다. 다음 포스팅을 참조해주세요.