파이썬을 이용해서 유튜브 플레이리스트 동영상을 받아보기 1 - PyTube 사용해보기

2018. 9. 16. 15:46Programming/Python

들어가기 전에

YouTube에 공유되는 영상들은 모두 저작권을 가지고 있는 영상입니다. 이를 다운로드 받아 무단으로 배포하거나, 저작권자의 허락 없이 임의로 수정하여 사용할 경우 법적 책임을 물을 수 있습니다. 이 포스트에서 YouTube 영상을 다운로드 받는 방법을 설명하고는 있으나, 이에 대해 발생하는 문제에 대해서는 저는 책임지지 않습니다.

PyTube 불러오기

지난 포스팅에선 PyTube를 설치해보고, 간단하게 사용해 봤었습니다. PyTube로 유튜브 영상을 받기 위해선, PyTube 안에서 YouTube 클래스를 불러올 필요가 있습니다.

from pytube import YouTube

yt = YouTube('https://www.youtube.com/watch?v=846cjX0ZTrk')

YouTube 객체는 YouTube('url')의 형식으로 생성할 수 있습니다. url에 받고 싶은 유튜브 영상 url을 String 형태로 넣어주면 됩니다.

YouTube 객체 알아보기

이제 yt라는 YouTube 객체를 만들어 보았습니다. 한번 yt에 있는 멤버와 메소드를 알아봅시다.

>>> yt.title
'[MV] 이달의 소녀 (LOONA) "Hi High"'
>>> yt.views
'16004332'
>>> yt.vid_descr
'[MV] 이달의 소녀 (LOOΠΔ) "Hi High"LOOΠΔ [+ +]‘+ +’ represents the ‘plus plus’ effect LOOΠΔ 1/3, ODD EYE CIRCLE, yyxy, and YeoJin gather to create.LOOΠΔ was born in its own universe with three uniquely structured teams. In other words, it was not designed as a group diving up into three units, but three teams were built to create one universe.LOOΠΔ 1/3 is a combination of girls present on planet earth. These girls portray the most realistic and practical sceneries that can easily be found on the streets or in school. HeeJin, HyunJin, HaSeul and ViVi start their stories in France, Japan, Iceland, and Hong Kong to become unified as one.LOOΠΔ / ODD EYE CIRCLE places itself in between the earth and the cosmos. The beginning of ODD EYE CIRCLE was by looking at three moons (ODD). Mutative, or varietal, girls gather to form the ODD EYE CIRCLE, and show themselves by taking control in love relationships instead of waiting passively.LOOΠΔ / yyxy was also known as ‘Edenism’ as it was placed beyond the earth of 1/3 and the middle earth of ODD EYE CIRCLE, in the place where we call ‘utopia’. But, the girls of Eden decide to deny the rules of Eden to venture off on a forbidden quest to find their identities. The emotions of each girls, faith, hope, love, and anger gather to form a being named yyxy.As the title suggests, the first track ‘+ +’ is a mash-up of the intro tracks of the albums by LOOΠΔ 1/3, ODD EYE CIRCLE, and yyxy.The titled track ‘Hi High’ is a song in the genre of Hi Energy, emitting positive energy that the gathering of the twelve members create. The desire to play hard-to-get is portrayed in the lyrics through the characters of each member. HeeJin says, “I don’t want to pass my love on to you so easily,” while HyunJin says, “because I’m that pretty girl,” and Choerry says, “I’m not playing hard-to-get but boys will be boys, watch out, watch out,” telling what is on their minds candidly. The high BPM like a high-speed sprint along with melodies pouring down, and splendid track arrangements forces the listeners to hold their breath and focus on the music.The official music video of ‘Hi High’, directed by the visual director Digipedi, narrates how LOOΠΔ 1/3 meets ODD EYE CIRCLE, and how these girls unite with yyxy to soar to the sky, with countless visual symbolisms hidden in the scenes.The lead-off single ‘favOriTe’ is the iconic signature sound of LOOΠΔ, completed with beats and synth stabs that previous girl-groups have never tried before. ‘열기 (9)’, is a track that proves a point that even without aggressive tempo, the temperature of the music can become heated up by the fervor of the girls. Along with it, a track with commercial sound that grabs you like love at first sight, ‘Perfect Love’, and a musical suggestion for you to become brave through the luxury of imagination that everyone cherishes in a corner of their hearts, ‘Stylish’ complete [+ +], the first album of LOOΠΔ with musical elements of LOOΠΔ 1/3, ODD EYE CIRCLE, and yyxy.LOOΠΔ, building LOONAverse with belief and certainty, finally gathers all twelve members and stands on their first stepping stone.#이달의소녀 #LOONA #HiHigh #MV #BlockBerry #BlockBerryCreativeMore about LOOΠΔ/iTunes : https://itunes.apple.com/us/album/ep/...Spotify : https://open.spotify.com/album/7e6TOK...Amazon Music : https://www.amazon.com/LOO%CE%A0%CE%9...LOOΠΔ Official : http://www.loonatheworld.comLOOΠΔ Instagram : https://www.instagram.com/loonatheworld/LOOΠΔ Twitter : https://twitter.com/loonatheworldLOOΠΔ Facebook : https://www.facebook.com/loonatheworld/LOOΠΔ Official Korean Fan Cafe : http://cafe.daum.net/loonatheworldCopyrights 2018 ⓒ BlockBerryCreative. All Rights Reserved.'

title, views, vid_descr 같은 것들이 있습니다. 해당 영상의 간단한 정보들을 이렇게 취득해올 수 있습니다.

다음으로는 메소드들에 대해 알아봅시다. 일단 해당 비디오가 어떻게 구성되어 있는지 확인해볼까요.

for e in yt.streams.all():
    print(str(e))

<stream: itag="22" mime_type="video/mp4" res="720p" fps="30fps" vcodec="avc1.64001F" acodec="mp4a.40.2">
<stream: itag="43" mime_type="video/webm" res="360p" fps="30fps" vcodec="vp8.0" acodec="vorbis">
<stream: itag="18" mime_type="video/mp4" res="360p" fps="30fps" vcodec="avc1.42001E" acodec="mp4a.40.2">
<stream: itag="36" mime_type="video/3gpp" res="240p" fps="30fps" vcodec="mp4v.20.3" acodec="mp4a.40.2">
<stream: itag="17" mime_type="video/3gpp" res="144p" fps="30fps" vcodec="mp4v.20.3" acodec="mp4a.40.2">
<stream: itag="137" mime_type="video/mp4" res="1080p" fps="30fps" vcodec="avc1.640028">
<stream: itag="248" mime_type="video/webm" res="1080p" fps="30fps" vcodec="vp9">
<stream: itag="136" mime_type="video/mp4" res="720p" fps="30fps" vcodec="avc1.4d401f">
<stream: itag="247" mime_type="video/webm" res="720p" fps="30fps" vcodec="vp9">
<stream: itag="135" mime_type="video/mp4" res="480p" fps="30fps" vcodec="avc1.4d401e">
<stream: itag="244" mime_type="video/webm" res="480p" fps="30fps" vcodec="vp9">
<stream: itag="134" mime_type="video/mp4" res="360p" fps="30fps" vcodec="avc1.4d401e">
<stream: itag="243" mime_type="video/webm" res="360p" fps="30fps" vcodec="vp9">
<stream: itag="133" mime_type="video/mp4" res="240p" fps="30fps" vcodec="avc1.4d4015">
<stream: itag="242" mime_type="video/webm" res="240p" fps="30fps" vcodec="vp9">
<stream: itag="160" mime_type="video/mp4" res="144p" fps="30fps" vcodec="avc1.4d400c">
<stream: itag="278" mime_type="video/webm" res="144p" fps="30fps" vcodec="vp9">
<stream: itag="140" mime_type="audio/mp4" abr="128kbps" acodec="mp4a.40.2">
<stream: itag="171" mime_type="audio/webm" abr="128kbps" acodec="vorbis">
<stream: itag="249" mime_type="audio/webm" abr="50kbps" acodec="opus">
<stream: itag="250" mime_type="audio/webm" abr="70kbps" acodec="opus">
<stream: itag="251" mime_type="audio/webm" abr="160kbps" acodec="opus">

streams는 Query 객체로 처리가 됩니다. 이 자체로는 뭘 할 수가 없고, 뒤에 all() 혹은 first() 같은 것을 사용해서 실체(?)를 확인할 수 있습니다.

유튜브의 영상은 하나의 영상에 대해서 여러가지 화질과 여러가지 확장자의 버전을 제공하므로, 이렇게 하나의 video라 할지라도 실제로 streams에 query를 날려보면 이렇게 여러 목록이 나오게 됩니다.

위 Stream 중에서 mp4 확장자만 따로 추출하고 싶습니다. 이럴 경우엔 filter()를 사용하면 걸러낼 수 있습니다.

for e in yt.streams.filter(file_extension='mp4').all():
    print(str(e))

<stream: itag="22" mime_type="video/mp4" res="720p" fps="30fps" vcodec="avc1.64001F" acodec="mp4a.40.2">
<stream: itag="18" mime_type="video/mp4" res="360p" fps="30fps" vcodec="avc1.42001E" acodec="mp4a.40.2">
<stream: itag="137" mime_type="video/mp4" res="1080p" fps="30fps" vcodec="avc1.640028">
<stream: itag="136" mime_type="video/mp4" res="720p" fps="30fps" vcodec="avc1.4d401f">
<stream: itag="135" mime_type="video/mp4" res="480p" fps="30fps" vcodec="avc1.4d401e">
<stream: itag="134" mime_type="video/mp4" res="360p" fps="30fps" vcodec="avc1.4d401e">
<stream: itag="133" mime_type="video/mp4" res="240p" fps="30fps" vcodec="avc1.4d4015">
<stream: itag="160" mime_type="video/mp4" res="144p" fps="30fps" vcodec="avc1.4d400c">
<stream: itag="140" mime_type="audio/mp4" abr="128kbps" acodec="mp4a.40.2">

filter에서 file_extension을 직접 지정해주니 mp4만 나왔습니다. 그런데 제일 아래를 보면 audio/mp4가 보이는데요, 이것은 mp4 파일이지만 영상은 존재하지 않는, 즉 영상의 음성만 담겨있는 파일입니다. 즉, mp4를 추려내긴 했지만, 영상이 아닌 것도 포함이 되어있단 소리입니다.

눈썰미가 좋은 사람은 눈치채셨겠지만, video인 mp4에서도 어떤 것들은 acodec이 있고, 어떤 것들은 acodec이 없단 것을 알 수 있습니다. 즉, acodec이 없는 파일들은 아까와는 반대로 영상만 존재하고 음성은 없는 파일들입니다. 이게 왜 발생하는 현상이며, 어떻게 해야 핸들링 할 수 있을지 다음에서 설명하겠습니다.

일단 지난 포스트에서 이 영상을 받는 방법은 다음과 같았습니다.

yt.streams.filter(progressive=True, file_extension='mp4').order_by('resolution').desc().first().download()

위 문장을 이해하는 것이 어렵진 않았을 겁니다. 필터된 것들 중 resolution(해상도)를 기준으로 정렬을 하되, 이를 내림차순 desc, 즉 해상도가 큰 것이 제일 위로 가도록 정렬하고, 거기에서 첫번째 것 first를 download하는 구문입니다. (progressive 옵션은 다음에서 설명하겠습니다)

요약하자면 Streams 중 mp4 확장자를 가지며 해상도가 가장 큰 파일을 받으라는 의미입니다. 이를 실행하고 나면 해당 python 파일이 실행된 자리에 영상이 다운로드 되어 있을 것입니다.

또한, download() 메서드는 다운로드 받을 곳을 지정할 수 있습니다. download('C:\Video') 이런 식으로 작성하면 C 드라이브의 Video 폴더에 저장이 됩니다.

이 외에도 Streams를 이용할 수 있는 방법은 많습니다. 특히 filter는 굉장히 많은 옵션을 제공하고 있으니 여러 개를 시도해보시기 바랍니다. 관련 문서는 이곳에서 확인하실 수 있습니다.

Streams 이해하기. Progressive Vs Adaptive(DASH)

위에서 streams를 조회할 때, mp4이면서 음성 혹은 영상만 있는 파일이 있는 걸 확인하셨을 겁니다. 그렇기 때문에 다음과 같은 문제가 생깁니다. 해상도가 가장 큰 것을 받기 위해서 progressive 옵션 없이 바로 입력하면

>>> yt.streams.filter(file_extension='mp4').order_by('resolution').desc().first().download()

Traceback (most recent call last):
  File "", line 1, in 
  File "/Users/zerobell/PycharmProjects/python35project/venv/lib/python3.5/site-packages/pytube/query.py", line 181, in order_by
    key=key,
  File "/Users/zerobell/PycharmProjects/python35project/venv/lib/python3.5/site-packages/pytube/query.py", line 175, in key
    def key(s): return integer_attr_repr[getattr(s, attribute_name)]
KeyError: None

이런 에러가 발생하게 됩니다. 이유는 간단합니다, file_extension='mp4' 옵션으로 얻어낸 streams중 음성만 있는 mp4가 섞여있기 때문인데요, 이는 resolution을 가지지 않으므로 정렬할 때 에러가 발생하는 것입니다. 그래서 progressive 옵션을 걸어줘야합니다. 그렇다면 progressive가 대체 뭐길래 그러는 걸까요?

YouTube를 이용하는 분들은 다 아는 이야기지만, YouTube 동영상을 시청할 때, 화질 설정이 자동으로 되어 있으면 전파 환경에 따라 수시로 해상도가 바뀌는 것을 경험하셨을 것입니다. 옛날의 동영상 스트리밍 서비스에서는 고화질로 보다가 끊김이 발생하면 설정에서 저화질로 바꿔주고 다시 새로고침을 했어야 했는데, 요즈음의 동영상 스트리밍은 상황에 맞춰 자동으로 변경하여 유저가 스스로 새로고침을 하는 수고를 하지 않도록 하고 있습니다.

이를 가능하게 하기 위해선, 일단 YouTube에서는 한 동영상을 여러개의 화질 버전으로 가지고 있어야 하며, 중간에 화질변경이 되어도 끊김없이 시청할 수 있도록 각 Stream을 chunk라는 단위로 분할해서 또 가지고 있어야 합니다. 720p로 1분까지 잘 시청하다가 갑자기 전파환경이 나빠지면 1분 10초부터 480p의 영상을 재생하도록 서버측에서 chunk를 보내는 것이죠. 이런 기술을 DASH(Dynamic Adaptive Streaming over HTTP)라고 합니다. 이 DASH 기술은 비디오/오디오의 분리와 합성을 지원합니다.

여기까지 알고나니 왜 Streams에 오디오만 있는 mp4, 비디오만 있는 mp4가 따로 있었는지 알 것 같습니다. 유튜브에서는 음성과 영상을 따로 분리된 채로 가지고 있다가, 스트리밍할 때에 이 두 가지를 하나로 합쳐서 서비스하는 것입니다.

Streams에서 이런 DASH 관련 Stream을 제거하는 옵션이 바로 Progressive 옵션입니다.

for e in yt.streams.filter(progressive=True, file_extension='mp4').all():
    print(str(e))


이제 video와 audio 둘 다를 가지는 영상이 표시됩니다. 그러나 Progressive 옵션으로 Streams를 조회할 경우에 얻을 수 있는 최대 해상도는 720p입니다. 만일, 1080p가 꼭 필요하다면, DASH로 되어있는 Stream 중에서 1080p영상과 음성 mp4를 별도로 받은 다음에, ffmpeg 등의 프로그램으로 이를 합쳐주는 수밖에 없습니다.

DASH Streams만을 표시해주는 옵션은 Adaptive 옵션입니다.

or e in yt.streams.filter(adaptive=True, file_extension='mp4').all():
    print(str(e))

<stream: itag="137" mime_type="video/mp4" res="1080p" fps="30fps" vcodec="avc1.640028">
<stream: itag="136" mime_type="video/mp4" res="720p" fps="30fps" vcodec="avc1.4d401f">
<stream: itag="135" mime_type="video/mp4" res="480p" fps="30fps" vcodec="avc1.4d401e">
<stream: itag="134" mime_type="video/mp4" res="360p" fps="30fps" vcodec="avc1.4d401e">
<stream: itag="133" mime_type="video/mp4" res="240p" fps="30fps" vcodec="avc1.4d4015">
<stream: itag="160" mime_type="video/mp4" res="144p" fps="30fps" vcodec="avc1.4d400c">
<stream: itag="140" mime_type="audio/mp4" abr="128kbps" acodec="mp4a.40.2">

여기서 두 가지 선택이 생깁니다. 1) 720p에 만족하여 다운로드를 받거나, 2) 1080p를 위하여 영상과 음성을 따로 받는 받는 선택

만일 2번을 택했다면, adaptive로 받아야 할 것입니다. 그런데 adaptive로 filter를 하더라도 결국 음성 mp4와 영상 mp4가 섞여서 등장하므로 resolution으로 정렬을 할 수가 없습니다. 음성 mp4를 이 query에서 제거하고 싶다면, only_video 옵션을 사용하면 됩니다.

for e in yt.streams.filter(adaptive=True, file_extension='mp4', only_video=True).all():
    print(str(e))

<stream: itag="137" mime_type="video/mp4" res="1080p" fps="30fps" vcodec="avc1.640028">
<stream: itag="136" mime_type="video/mp4" res="720p" fps="30fps" vcodec="avc1.4d401f">
<stream: itag="135" mime_type="video/mp4" res="480p" fps="30fps" vcodec="avc1.4d401e">
<stream: itag="134" mime_type="video/mp4" res="360p" fps="30fps" vcodec="avc1.4d401e">
<stream: itag="133" mime_type="video/mp4" res="240p" fps="30fps" vcodec="avc1.4d4015">
<stream: itag="160" mime_type="video/mp4" res="144p" fps="30fps" vcodec="avc1.4d400c">

반대로, 음성만을 받고 싶다면 only_audio 옵션을 True로 켜주면 됩니다.

이상으로, PyTube의 전반적인 사용법을 알아보았습니다. 다음엔 웹 크롤링을 통해서 플레이리스트를 얻어오는 방법을 알아보겠습니다.