사용자 도구

사이트 도구


project:youtubedownload

파이썬을 이용한 유튜브 동영상 다운로드

유튜브를 모르시는 분들은 없겠죠? 이번에는 파이썬을 이용해 유튜브 동영상을 다운로드 받는 방법에 대해 적어 보겠습니다. 유튜브는 동영상을 스트리밍할 뿐, 따로 영상을 저장하는 기능은 제공하고 있지 않습니다. 그러나 유튜브의 동영상을 저장해 오프라인일 때에도 저장하고 싶다는 요구가 많이 있었죠. 그에 따라 유튜브의 영상을 어떻게든 추출해서 따로 저장하는 소프트웨어들이 정말 많이 나왔습니다. 운영체제에서 따로 돌아가는 응용 프로그램 형태로도, 웹브라우저의 확장 기능으로, 전용 웹서비스로, 쉘 스크립트로도, 또 모바일 앱 프로그램으로도요. 그래서 원하신다면 입맛에 맞는 유튜브 다운로드 프로그램을 이용하시면 매우 편리하게 영상을 다운로드 받을 수 있습니다.

그렇지만 어떻게 이런 프로그램들이 유튜브 동영상을 다운로드 받는지, 그리고 왜 유튜브는 그런 동영상을 다운로드 받는 것을 허용할 수 밖에 없는지에 대해 궁금증을 가져보신 적이 있으신지요? 저는 궁금했습니다. 그래서 (재미로) 한 번 해 보고, 이런 문서를 작성한 것입니다.

왜 막지 않을까?

유튜브가 동영상을 퍼가는 것을 제공하진 않지만, 굳이 막지는 않는 이유를 생각해 보았습니다. 이것은 제 생각을 적어 본 것이므로 사실과는 다를 수 있습니다.

유튜브도 HTTP Live Streaming을 합니다. 즉 동영상의 스트리밍이 웹페이지 파일 다운로드와 동일한 원리로 동작합니다. HTTP 프로토콜 설계 방식 상, 궁극적인 동영상의 추출 행위 차단은 매우 어렵습니다. 서버는 클라이언트에게 단순히 데이터를 제공하는 역할만을 하지, 그 이외의 것에 대해서는 별 관심 없습니다. 또 서버는 클라이언트가 왜 동영상을 받아가는지 알려고 하지 않습니다. 그리고 받아간 자료를 어떻게 사용할지도 알 수 없습니다.
이를 변경하려 하는 것은 HTTP 프로토콜 설계 근본을 건드리는 것과 같습니다. 그러므로 클라이언트의 의도와 동작을 서버가 강제하려고 들기 시작하면 일이 무진장 피곤해지고, 부작용도 일파만파 번지겠죠. 그럴 바에는 HTTP 기반의 스트리밍을 포기하는 것이 나을 지도 모릅니다. 그러나 그러기엔 HTTP 기반 스트리밍의 장점이 더 컸기에 사용하였겠죠. 기술적인 이유 이외에 여러 이유가 분명히 있겠지만, 제가 유튜브의 관계자가 아니므로 그런 것까지는 다 다룰 순 없겠군요.

다시 말하자면 유튜브 서버는 설계상 1회 감상을 위한 다운로드와 영구 저장을 위한 다운로드를 구분하는 것이 거의 불가능하며, 따라서 다운로드 받는 사용자의 의도를 일일이 구분하여 통제하는 것 또한 거의 불가능합니다. 또한 두 행위 자체가 기술적으로 별 차이점이 없습니다.

제 상식으로는 오프라인을 위해 개인적으로 영상을 다운로드 받아 따로 저장하는 것은 큰 문제가 없는 것으로 알고 있습니다. 하지만 저작권이 걸린 동영상을 법이 지정한 범위 밖으로 함부로 이용할 경우만 문제가 됩니다(그리고 이건 정말 만의 하나지만, 대한민국이 아닌 어떤 곳에서는 그런 행위 자체를 문제삼을 수 있을지도 모릅니다). 칼이 요리를 할 때 쓰이면 유용한 도구지만, 흉악한 범죄에 이용되면 흉기가 되는 것과 같은 이치입니다. 사용하는 사람이 올바른 지식과 적합한 의도를 갖고 사용해야 합니다.

그럼 어떻게 할까?

Firefox로 탐색해보기

저는 이럴 때 Firefox의 'HTTPFox' 부가 기능과 '문서 검사' 기능을 애용합니다. HttpFox는 Firefox의 모든 통신 내역을 아주 자세하게 보여줍니다. 그러므로 겉으로 보이지 않는 숨은 동작도 쉽게 파악할 수 있습니다. 그러면 아무 유튜브 동영상을 선택해, HttpFox를 동작시킨 상태에서 재생하도록 해 보겠습니다.

위 그림은 웹브라우저가 유튜브의 동영상 웹 페이지에 접근한 내역을 찾아 하이라이트시킨 것입니다. 확실히 text/html 문서로 응답을 받았습니다. 웹브라우저는 이제 이 문서를 파싱하여 문서가 요구하는 자바스크립트, 그림 등을 서버에서 받아 올 것입니다.

웹브라우저가 받아오는 여러 리소스(항목) 중에는 아래 그림과 같은 항목도 발견할 수 있을 것입니다. 이것은 동영상 파일임을 쉽게 눈치챌 수 있습니다. 이 파일을 읽어들여 웹 페이지의 플래시 플레이어가 비디오를 재생하는 것입니다. 보다 이 항목을 쉽게 발견하려면 동영상의 화질을 하나 선택해 고정하는 것이 좋습니다. 이유는 HTTP Live Streaming 문서를 보면 알 수 있을 것입니다.

비디오 재생을 잠시 멈추고 HttpFox에 있는 비디오 주소를 바로 복사해 Firefox 탭을 하나 열어 붙여 넣기를 합니다. 그러면…

이렇게 비디오가 받아집니다. 만일 지금 서버가 플래시 플레이어에서 재생할 목적으로 받는 것이 아님을 안다면, 그리고 그것을 강제할 의도라면 이런 일은 일어나지 않겠죠. 그러나 서버는 다행히도(?) 이를 구분할 방법이 없어 보입니다. 지금 이 결과를 보면 이렇게 생각할 수 있습니다.

유튜브 서버와 웹브라우저는 별다른 숨김이나 꼼수 없이, HTML 및 관련 기술만으로 동영상을 재생합니다(플래시 플러그인이 별도로 사용된 건 예외입니다). 한편 HttpFox는 동영상의 명시적인 주소를 붙잡았습니다. 그렇다면 이 명시적인 주소는 HTML 소스를 잘 분석하면 분명 금방 알아낼 수 있을 것입니다. 그러니까 모든 수수께끼의 답은 애초에 이미 HTML 소스가 가지고 있는 셈입니다. 사실 당연한 소리입니다. 무언가 저절로 하늘에서 뚝 떨어질 리 없지요 ;-) 우리는 이를 차근차근 해석해 내기만 하면 됩니다. 이제 파이어폭스의 '문서 검사' 기능이 활약할 시간입니다.

문서 검사를 활성화하고 유튜브의 재생 화면을 클릭해 유튜브의 재생기가 HTML의 어떤 요소인지를 알아봅니다. 'movie_player'라는 id를 가진 embed 태그가 재생 화면을 담당하고 있음을 쉽게 알 수 있습니다. 그리고 이 태그의 flashvars 라는 속성에 매우 길게 무엇인가가 줄줄 적힌 것을 볼 수 있습니다.

보통 embded 태그는 웹페이지에 플래시 등의 플러그인을 삽입하기 위해 많이 사용합니다. 그리고 Adobe의 도움말에 따르면 embed 태그에서 플래시에 여러 정보를 전달하기 위해 flashvars 속성을 사용한다고 적혀 있습니다. 무언가 알아낸 느낌입니다!

자바스크립트 코드 해석하기

다만 여기서 조심해야 할 사항이 있습니다. Firefox의 문서 검사에 나오는 HTML 문서는 서버가 Firefox로 보낸 원본 HTML 코드가 아닌, 파이어폭스가 이해한 바를 바탕으로 재구성한 HTML 문서라는 점입니다. 그러므로 반드시 원본 대조(?)를 통해 정말 flashvars 속성이 소스에 있는지 확인해아 합니다. 이건 어려운 일이 아닙니다. '소스 보기'로 소스를 열어 봅니다. 그리고 'movie_player'나 'flashvars'라는 키워드로 embed 태그 부분을 살펴보기로 하지요.

<div id="watch7-player" class="flash-player"></div>
<script>
      (function() {
        var encoded = [];
        for (var key in yt.playerConfig.args) {
          encoded.push(encodeURIComponent(key) + '=' + encodeURIComponent(yt.playerConfig.args[key]));
        }
        var swf = "      \u003cembed type=\"application\/x-shockwave-flash\"     s\u0072c=\"http:\/\/s.ytimg.com\/yts\/swfbin\/watch_as3-vflXPuwnw.swf\"     id=\"movie_player\"    flashvars=\"__flashvars__\"     allowscriptaccess=\"always\" allowfullscreen=\"true\" bgcolor=\"#000000\"\u003e\n  \u003cnoembed\u003e\u003cdiv class=\"yt-alert yt-alert-default yt-alert-error  yt-alert-player\"\u003e  \u003cdiv class=\"yt-alert-icon\"\u003e\n    \u003cimg s\u0072c=\"\/\/s.ytimg.com\/yts\/img\/pixel-vfl3z5WfW.gif\" class=\"icon master-sprite\" alt=\"\uc54c\ub9bc \uc544\uc774\ucf58\"\u003e\n  \u003c\/div\u003e\n\u003cdiv class=\"yt-alert-buttons\"\u003e\u003c\/div\u003e\u003cdiv class=\"yt-alert-content\" role=\"alert\"\u003e    \u003cspan class=\"yt-alert-vertical-trick\"\u003e\u003c\/span\u003e\n    \u003cdiv class=\"yt-alert-message\"\u003e\n            \uc774 \ub3d9\uc601\uc0c1\uc744 \ubcf4\ub824\uba74 Adobe Flash Player\uac00 \ud544\uc694\ud569\ub2c8\ub2e4. \u003cbr\u003e\u003ca href=\"http:\/\/get.adobe.com\/flashplayer\/\"\u003eAdobe\uc5d0\uc11c \ub2e4\uc6b4\ub85c\ub4dc\ud558\uc138\uc694.\u003c\/a\u003e\n    \u003c\/div\u003e\n\u003c\/div\u003e\u003c\/div\u003e\u003c\/noembed\u003e\n\n";
        swf = swf.replace('__flashvars__', encoded.join('&'));
        document.getElementById('watch7-player').innerHTML = swf;
      })();
    </script>

embed 태그는 온데간데 없고, 이상한 자바스크립트 코드만 있습니다. 이게 어떻게 된 걸까요? 틀릴 리 없습니다. 이 자바스크립트 코드를 자세히 살펴 보면 답을 알아낼 수 있습니다. 우선 코드 마지막의 다음과 같은 코드를 주목합니다.

document.getElementById('watch7-player').innerHTML = swf;

즉 id가 watch7-player인 녀석의 내부 HTML 코드가 swf 변수의 내용이 됩니다. 위 그림을 보면 watch7-player id를 가진 div 태그의 자식이 embed이므로, swf 라는 변수에 틀림없이 embed 태그의 존재가 있을 겁니다. 윗줄의 swf 변수는 무언가 굉장히 복잡한 암호문처럼 보이지만 '<'나 '>'같은 문자 유니코드로 표현했을 때 그 문자에 대응되는 숫자를 16진수로 적은 것 뿐입니다. 이에 관해서는 ' Maniadb.com OpenAPI를 이용한 MP3 태그 검색기'의 인코딩 부분을 참고하세요. swf 변수의 내용을 보다 보기 쉽게 바꾸어 보려면 다음과 같은 파이썬 코딩을 하면 됩니다.

replace_swf_text.py
swf = "      \u003cembed type=\"application\/x-shockwave-flash\"     s\u0072c=\"http:\/\/s.ytimg.com\/yts\/swfbin\/watch_as3-vflXPuwnw.swf\"     id=\"movie_player\"    flashvars=\"__flashvars__\"     allowscriptaccess=\"always\" allowfullscreen=\"true\" bgcolor=\"#000000\"\u003e\n  \u003cnoembed\u003e\u003cdiv class=\"yt-alert yt-alert-default yt-alert-error  yt-alert-player\"\u003e  \u003cdiv class=\"yt-alert-icon\"\u003e\n    \u003cimg s\u0072c=\"\/\/s.ytimg.com\/yts\/img\/pixel-vfl3z5WfW.gif\" class=\"icon master-sprite\" alt=\"\uc54c\ub9bc \uc544\uc774\ucf58\"\u003e\n  \u003c\/div\u003e\n\u003cdiv class=\"yt-alert-buttons\"\u003e\u003c\/div\u003e\u003cdiv class=\"yt-alert-content\" role=\"alert\"\u003e    \u003cspan class=\"yt-alert-vertical-trick\"\u003e\u003c\/span\u003e\n    \u003cdiv class=\"yt-alert-message\"\u003e\n            \uc774 \ub3d9\uc601\uc0c1\uc744 \ubcf4\ub824\uba74 Adobe Flash Player\uac00 \ud544\uc694\ud569\ub2c8\ub2e4. \u003cbr\u003e\u003ca href=\"http:\/\/get.adobe.com\/flashplayer\/\"\u003eAdobe\uc5d0\uc11c \ub2e4\uc6b4\ub85c\ub4dc\ud558\uc138\uc694.\u003c\/a\u003e\n    \u003c\/div\u003e\n\u003c\/div\u003e\u003c\/div\u003e\u003c\/noembed\u003e\n\n"
 
print swf.decode('unicode-escape')

결과는 아래처럼 나옵니다. 그냥 전형적인 HTML embed 코드임을 알 수 있습니다.

      <embed type="application\/x-shockwave-flash"     src="http:\/\/s.ytimg.com\/yts\/swfbin\/watch_as3-vflXPuwnw.swf"     id="movie_player"    flashvars="__flashvars__"     allowscriptaccess="always" allowfullscreen="true" bgcolor="#000000">
  <noembed><div class="yt-alert yt-alert-default yt-alert-error  yt-alert-player">  <div class="yt-alert-icon">
    <img src="\/\/s.ytimg.com\/yts\/img\/pixel-vfl3z5WfW.gif" class="icon master-sprite" alt="알림 아이콘">
  <\/div>
<div class="yt-alert-buttons"><\/div><div class="yt-alert-content" role="alert">
    <span class="yt-alert-vertical-trick"><\/span>
    <div class="yt-alert-message">
            이 동영상을 보려면 Adobe Flash Player가 필요합니다. <br><a href="http:\/\/get.adobe.com\/flashplayer\/">Adobe에서 다운로드하세요.<\/a>
    <\/div>
<\/div><\/div><\/noembed>

아마 __flashvars__란 문자열은 swf = swf.replace('__flashvars__', encoded.join('&'));란 자바스크립트 코드에 의해 무언가 내용이 변화되었을 겁니다. 변화된 내용은 encoded라는 변수와 관련이 있을 것이고, 이것은 코드 좀 더 위에 있는 yt.playerConfig.args 란 부분과 깊은 관련이 있을 것입니다. 그러면 HTML 문서에서 playerConfig란 자바스크립트 변수를 찾아 보도록 하겠습니다. 제시한 위 자바스크립트 코드의 조금 윗부분에 아래와 같은 꽤 많은 양의 문자열을 발견할 수 있을 것입니다. 내용이 너무 길어 자세한 사항은 생략하겠습니다.

<div id="watch7-video-container">
      <div id="watch7-video">
            <script>
if (window.yt.timing) {yt.timing.tick("bf");}    </script>
 
            <script>
      yt.playerConfig = { ... };
    </script>

많은 내용들이 키=값 형태로 나열되어 있는 것을 확인할 수 있습니다. 일단 동영상을 다운로드 받는다는 목적에만 집중하겠습니다. 잡다한 키는 넘어가고 url_encoded_fmt_stream_map 이라는 키와 값에만 주목하도록 하겠습니다.

스트림 맵 분석하기

여기서는 url_encoded_fmt_stream_map의 값을 일컬어 '스트림 맵'이라고 부르겠습니다. 스트림 맵의 생짜 값(raw value)은 상당히 긴 문자열인데, 바로 여기에 우리가 원하는 동영상의 실제 주소가 숨겨져 있습니다. 이 문자열은 CSV 형태로 되어 있고, 각 필드의 값은 urlencode가 되어 있습니다. 일단 콤마(,)를 기준으로 문자열 분리를 해야 올바르게 하나의 의미를 가진 덩어리를 나눌 수 있습니다. 우선 당장 프로그래밍을 들이대지 말고 텍스트 편집기를 이용해 차근차근 손으로 해 보도록 하겠습니다.

스트림 맵을 텍스트 편집기로 붙여 넣습니다. 그리고 콤마 문자 하나를 검색합니다. 처음 발견된 콤마를 기준으로 공백을 입력해 값을 위, 아래 두 덩어리로 떼어 냅니다. 추출해 낸 윗부분 덩어리의 처음은 itag=XX\u0026sig=XXX… 와 같이 시작하는 것을 확인할 수 있습니다. \u0026은 유니코드 문자임이 분명합니다.

print unichr(0x26)

위 코드를 실행하면 '&' 문자가 출력됩니다. 그러므로 텍스트 편집기에서 \u0026을 찾아 '&'로 변경합니다. 변경할 때 '&' 기호 뒤에 개행 문자까지 넣으면 시각적으로 더 보기 좋겠죠. 그러면 다음과 같이 분리됩니다.

itag=...&
sig=...&
url=...&
fallback_host=...&
quality=...&
type=...,

이제 거의 다 왔습니다. url 부분이 보이죠? 바로 저기가 실제 동영상의 주소입니다. 나머지 분리하지 않은 덩어리도 같은 방식으로 나누어지므로 일일이 나누어 확인해 봅니다. 값을 다 살펴보면 아시겠지만, 서버가 다양한 포맷과 해상도로 인코딩해 놓은 것을 정리한 목록이 이 스트림 맵입니다. 결국 순수하게 HTML 코드 및 자바스크립트만 분석하면 동영상 주소를 찾아낼 수 있는 것이네요. 물론 이 방법이 언제까지 통하리란 보장은 없습니다만, 그렇다고 동영상의 주소를 숨바꼭질하는 것처럼 꽁꽁 숨겨두지는 않을 겁니다.

마지막으로 첫번째 덩어리의 url 부분의 문자열을 한 번 urldecode하면 웹브라우저에도 입력 가능한 url 주소가 됩니다. 단, 이 주소를 바로 넣으면 '403 forbidden' 에러가 납니다. 반드시 sig라는 키의 값을 붙여 넣어야 합니다. 주소로 적을 때는 왠일인지 키 이름은 sig에서 signature로 변경됩니다. 이렇게 signature까지 붙여 넣은 주소를 사용하면 동영상에 직접 접근이 가능합니다. 그러므로 이 주소만 있으면 HTTP 스트림 재생을 지원하는 다른 재생기에서도 재생이 가능하며, 우리의 최종 목적인 동영상 다운로드도 가능합니다. 이제 프로그램을 작성하기 위한 모든 준비가 끝났습니다.

코드로 작성

그러면 지금까지 한 내용을 파이썬 코드로 옮겨 보겠습니다. 완전 프로토타입의 코드이므로 쓰기 편하게 만드려면 고쳐야 할 부분이 많습니다. 그러나 일단 복잡한 기능은 배제하여 작동을 확인하는 수준으로 최대한 간결하게 작성해 보겠습니다.

youtube_dn_proto.py
# -*- coding: utf-8  -*-
import urllib
import urllib2
import re
import codecs
import sys
 
def get_html(url):
    # url을 열어 html 문서를 받아옵니다.
    # 문서는 utf-8 인코딩입니다.
    resp = urllib2.urlopen(url)
    html = resp.read().decode('utf-8')
 
    # 인코딩된 문서를 미리 저장하고, 저장된 문서를 이용합니다.
    # 일일이 네트워크를 이용해 문서를 받고 싶지 않을 때 사용합니다.
    # 적절히 주석으로 조절하세요.
    with codecs.open('sample.html', 'w', 'utf-8') as f:
        f.write(html)
 
    #with codecs.open('sample.html', 'r', 'utf-8') as f:
    #    html = f.read()
 
    return html
 
def extract_format_info(v_value):
    # v_value는 동영상의 아이디입니다.
    URL_BASE = 'http://www.youtube.com/watch?v='
    url      = URL_BASE + v_value
    html     = get_html(url)
 
    # playerConfig의 모든 값을 가져옵니다.
    t = re.search(r'yt.playerConfig = {(.+)};', html)
    # print t.groups()[0]
    # print
 
    # 그 중 스트림 맵 부분만 추출해냅니다.
    r = re.search(r'"url_encoded_fmt_stream_map": "(.+?)"', t.groups()[0].strip())
 
    # 스트림맵은 CSV이므로 콤마로 분리합니다.
    rawstrings = r.groups()[0].strip().split(',')
    items      = []
 
    for rawstring in rawstrings:
        item = {}
 
        # \uXXXX 부분을 실제 문자열로 변경합니다.
        uniesc = rawstring.decode('unicode-escape')
 
        # 실제 문자열로 변경된 부분 중 '&'기호가 있습니다.
        # 이것은 재차 키=값 문자열을 분리하는 역할을 합니다.
        for oneitem in uniesc.split('&'):
            # 키와 값을 분리, 딕셔너리로 저장합니다.
            key, val = oneitem.split('=')
            item[key] = urllib.unquote(val.strip())
 
        items.append(item)
 
    # 값들을 프린트
    #for item in items:
    #    for it in item.items():
    #        print it[0]+': '+it[1]
    #    print
 
    return items
 
if __name__ == '__main__':
    items  = extract_format_info(sys.argv[1])
 
    # 동영상을 실제로 받아 봅니다. url과 sig 필드의 값이 필요합니다.
    # 영상의 첫번째 항목을 열어 저장합니다.
    url    = '%s&signature=%s' % (items[0]['url'], items[0]['sig'])
 
    # 쿠키 등의 장치가 필요 없습니다. 단순히 열면 됩니다.
    resp   = urllib2.urlopen(url)
 
    # 수신 상태를 확인
    print resp.info()
 
    # 동영상의 인코딩 상태를 파악하여 다운로드 받는 것이 아니므로
    # 동영상 파일의 확장자는 적절히 변경하세요.
    f = open('result.flv', 'wb')
    f.write(resp.read())
    f.close()
    resp.close()

소스의 동영상 아이디만 적절히 바꾸면 파일이 다운로드 될 것입니다. 초라한 형태지만 제대로 동작함을 확인할 수 있습니다. 유튜브의 동영상 URL이 http://www.youtube.com/watch?v=a1cTyDOuop0라면

youtube_dn_proto.py a1cTyDOuop0

이렇게 입력하면 됩니다.

마치며

지금까지 간단하게 유튜브 동영상을 다운로드 받는 방법에 대해 조사하고 과정을 기록하였습니다. 다운로드를 할 수 있는 툴은 엄청나게 많지만 그 방법에 대해서 자세하게 소개한 문서가 없어 직접 해 보았습니다. 단순한 궁금증에서 시작했는데 은근히 재밌네요 :-) 기본적인 원리는 HTML 코드 파싱 –> 원하는 정보 추출 –> 다운로드로 별반 다른 것이 없습니다. 여타 프로그램들은 꽤 다양한 옵션들을 제공하고 쓰기도 편리하기도 할 겁니다. 그러나 기본적인 큰 틀은 제가 제시한 방법과 크게 다르지는 않을 것입니다.

마지막으로 파이썬 기반으로 작성된 유튜브와 같은 스트리밍 동영상의 다운로드 CLI 프로그램인 youtube-dl을 소개하고 문서를 마칩니다. 원클릭으로 실행되는 여타 다른 프로그램들 보다는 조금 불편하긴 합니다만, 유튜브 뿐만 아니라 다른 사이트의 동영상도 다운로드 가능하고, 여러 가지 옵션들이 잘 정리되어 있으며, 무엇보다 소스가 공개되어 있으므로 파이썬 공부를 위해서는 한 번 살펴볼만한 프로그램입니다.

project/youtubedownload.txt · 마지막으로 수정됨: 2014/10/09 21:24 저자 127.0.0.1

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki