본문 바로가기

Programming/Python

파이썬을 이용해서 유튜브 플레이리스트 동영상을 받아보기 2 - Playlist 클래스와 웹 크롤링

들어가기 전에

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를 별도로 설정하지 않으며, 코드가 더 간결해졌습니다. 다음 포스팅을 참조해주세요.

  • pl.download_all() 메소드 써봤는데 지금은 안먹히더라구요.
    pl.parse_links() 에 playlist?로 시작하는 링크를 넣으면 해당 플레이 리스트에 있는 모든 영상의 watch? 링크를 리턴시키길래 이쪽을 쓰는게 좋을 것 같습니다.
    200개 넘어도 전부 출력되더군요.