Profile picture

[Python] 유튜브(Youtube) 데이터 수집하기

JaehyoJJAng2024년 02월 21일

▶︎ 개요

플레이리스트를 전문적으로 올리는 유튜버의 새로운 영상을 MP3 파일로 다운로드 하는 작업을 파이썬으로 자동화해보려고 한다.

유튜브 영상을 MP3로 변환하는 가장 간단한 방법은 Youtube API를 사용하는 것인데

나는 Youtube API 없이 유튜브 동영상 데이터를 추출해보기로 하였다.


▶︎ 데이터 수집 이슈

Youtube 영상 다운로드 오픈소스들의 기능들을 살펴보면 대부분 Watch URL을 입력 받는다.

즉, 유튜브의 영상을 다운로드하기 위해서는 다운로드 받고자하는 영상의 URL 주소가 필수적으로 존재해야 한다.

내가 구현해야 할 것은 하나의 영상을 다운로드 하는 것이 아닌, 지금까지 업로드된 모든 플레이리스트 영상이 대상이다.

모든 영상이 올라오는 페이지는 다음과 같다.
image

여기서부터 F12 키를 눌러 BeautifulSoup 라이브러리를 사용하여 HTML을 파싱하거나, Network 트래픽을 디버깅하여 요청 URL을 분석하면 될 줄 알았지만 다음과 같은 이슈가 있었다.

  1. 이 페이지를 기준으로 requests를 사용하여 BeautifulSoup로 HTML 파싱 시 video에 대한 HTML이 존재하지 않았다.
  2. F12 키를 눌러 Network 트래픽을 디버깅 해본 결과 영상의 정보를 불러오는 endpoint는 발견하였지만 해당 endpoint에서 요구하는 파라메터 값이 무엇인지 파악할 수 없었다.

1번 이슈의 경우 해결 방법이 생각나지 않았고,

2번 이슈의 경우 Network 트래픽에서 어떠한 값을 보내고 있는지 확인할 수 있으므로,

밑에서 확인한 태그에 들어있는 데이터로부터 몇 개 정도는 채워 넣을 수 있을 것 같다.


▶︎ HTML 내에 존재하는 유튜브 데이터

동영상 페이지에서 '마우스 우클릭 - 페이지 원본 보기'로 해당 페이지에 대한 소스를 열어본 후, ytInitialData를 검색해보면

다음과 같이 영상관련 데이터가 태그에 들어있음을 확인할 수 있다.
image
추측만 해보자면 진입점으로 삼은 동영상 페이지의 처음 로딩될 영상 관련 데이터가 아닐까?

이를 파이썬에서 다루려면 아래와 같이 BeautifulSoup로 script 태그를 가져와 text에 ytInitialData가 있는지 확인해야 한다.

@cached_property
def get_ytb_initialdata(self) -> str|None:
    """ JavaScript의 'ytInitialData' 데이터 추출 """
    exceed_number: int = 5000        
    if self.soup:
        for x in self.soup.select('script'):
            if 'ytInitialData' in x.text and len(x.text) > _guess_exceed_number:
                return x.text

exceed_number 변수는 추출한 script 태그에서 ytInitialData가 영상 데이터를 담고 있는 변수인지를 len 함수로 검증하기 위해 임의로 할당한 정수 값이다.

🤷 @cached_property

이 게시글을 참고해보도록 하자.


추출한 ytInitialData의 값을 확인해보자.
image
webCommandMetadata라는 데이터가 수백개 존재하는데 여기서 watch_url이 포함되어 있는 값들이 있다.


watch_url을 추적하기 위해 정규표현식을 작성해보자.

@cached_property
def get_urls_from_ytb_initialdata(self) -> list[str]:
    pattern = r'"videoId":"([\w-]+)"'
    urls = set()
    for url in set(re.findall(pattern=pattern, string=self.get_ytb_initialdata)):
        convert_to_link = 'https://www.youtube.com' + url
        urls.add(convert_to_link)
    return list(urls)

image
이렇게 해서 얻어낸 watch_url의 개수는 총 30개이다.


▶︎ 유튜브 동영상 추가 데이터 로딩

이쯤에서 유튜브 동영상 페이지를 열어놓고 추가 데이터가 로딩되는 상황에서 Network 트래픽을 살펴보면 다음과 같은 요청이 발생하는 것을 확인할 수 있다.
image
image
정확히 어떤 동작을 하는지는 모르겠지만, 추가 데이터를 로딩하려면 continuation 이라는 요청 페이로드가 필요한 것으로 보인다.


추가로 context 정보도 필요한데 이는 대략 아래와 같다.
image
꽤나 많은 데이터를 필요로 하는 것을 볼 수 있다.


위와 같은 페이로드를 전부 다 채워서 요청을 줄 수는 없기에 github에서 비슷한 소스코드가 있는지 검색한 결과

다음과 같은 정보만 요청해도 데이터를 읽어오는 데에 문제가 없다는 걸 확인하였다.

{
  "context": {
    "client": {
      "clientName": "WEB",
      "clientVersion": "2.20240509.00.00"
    }
  },
  "browseId": "",
  "continuation": ""
}

browseId의 경우 Youtube Channel ID를 채우면 되는데, continuation 값은 어디서 가져와야 할까.


‣ 추가 데이터 로딩에 필요한 continuation token

추가 데이터 로딩을 요청하기 위해 필요한 continuation token은 앞서 소개한 ytInitialData에 들어가있다.
image


이 또한 정규표현식을 사용하여 추출해보도록 하자.

@cached_property
def get_continuation_token(self) -> str:
    """ continuationCommand 추출 """
    pattern = r'"continuationCommand"\s*:\s*{"token"\s*:\s*"[^"]{1000,}"'
    match: re.Match = re.search(pattern=pattern, string=self.get_ytb_initialdata)
    
    continuation_token: str = match.group(0).split(':')[-1].replace('\"','')
    return continuation_token

continuationCommand를 찾는데 1000자 이상을 지정하는 이유는 continuationCommand가 여러개 있기 때문이다.

위 사진과 같이 토큰의 경우 토큰 길이가 상당하므로, 1000자가 넘지 않는 경우 토큰 값이 아니라고 판단했다.
그러나 continuation_token이 어떤 용도로 사용되는지 파악하지는 못하였다.


▶︎ 데이터 수집 포인트

앞서 얻은 continuation token을 통해 계속해서 같은 endpoint에 요청을 날리면 추가 데이터가 로딩될 것만 같았지만, 그렇지 않았다.

데이터에 접근하기 위해서는 3가지 접근 포인트를 기억해야 한다.

  1. ytInitialData 30개
  2. continuation token을 포함한 요청 30개
  3. continuation token을 포함하지 않은 요청 15개

3번 방법의 경우 continuation token을 포함하지 않고 channel_id만 요청 페이로드에 넣어도 watch_url을 얻을 수 있었다.


▶︎ Youtube video download

watch_url 수집 작업을 완료했으니 수집한 URL을 통해 영상을 파일로 다운로드하면 된다.

여기서는 yt-dlp 외부 라이브러리를 사용할 것이다.

해당 라이브러리를 사용하여 mp4와 같은 영상 데이터를 mp3로 변환하려면 현재 서버 또는 컴퓨터에 ffmpeg 바이너리가 필요하다.

ffmpeg에서 자신의 운영체제와 호환되는 버전을 설치하면 된다.


▶︎ 마무리

이 글은 소스코드에 대한 간략한 이해를 증진 시키기 위해 작성한 글이어서 아주 자세하게 다루지는 않았다.

소스코드는 다음과 같다.


Loading script...