ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 인프런 동영상 웹 스크래핑하기 - 3 : 네트워크 분석 - 2
    개발/웹크롤링 2022. 1. 16. 15:11

    사실, 강의 형식으로 준비하려고 했는데, 강의로 하기에는 제 지식도 많지 않고 일일히 다 설명하려면 까마득한데다, 내용이 다 안 담길 거 같아서 코드 리뷰 형식으로 진행하겠습니다...

    우선 인프런의 요청을 가로채기 위해서 selenium-wire를 사용했습니다. selenium-wire에서 requests를 받아오는 방법은 다음과 같습니다.

    >>> driver.requests
    [Request(method='POST', url='https://accounts.google.com/ListAccounts?gpsia=1&sour...

    비디오가 들어 있는 url 패턴을 가로채서 해당 url에 get 요청을 보낸 후 6초 단위의 동영상 바이트 덩어리를 한 파일에 다 합쳐서 저장할 것입니다. url 패턴 중에서도 비디오의 url 리스트들을 모두 받아낼 수 있는 url이 있습니다. 이를 간단하게 다음과 같이 받아왔습니다.

    for request in self._driver.requests:
        if "https://vod.inflearn.com" in request.url and '.m3u8' in request.url:
            root_url = request.url[:request.url.rfind('/')] + '/'
            headers.update(request.headers)
            resp = requests.get(url=request.url, headers=headers)
            lines = [line for line in resp.text.strip().split('\n') if '#' not in line]
            meta_info_url = lines[-1]
            break

    regex를 쓸 수도 있지만 귀찮아서 안 썼습니다. root_url은 지금 보고 있는 영상의 url을 말합니다. headers를 업데이트한 것은 혹시 몰라서 넣어주었습니다. resp.text를 보면 여러 줄에 걸쳐서 동영상의 옵션들에 따른 링크 주소가 담긴 목록을 반환합니다. #으로 시작하는 건 주석인 것 같아서 lines를 받아올 때 생략하고 받았습니다. resp.text의 예시는 다음과 같습니다.

    #EXTM3U
    #EXT-X-VERSION:3
    #EXT-X-INDEPENDENT-SEGMENTS
    #EXT-X-STREAM-INF:BANDWIDTH=788643,AVERAGE-BANDWIDTH=429903,CODECS="avc1.640028,mp4a.40.5",RESOLUTION=960x540,FRAME-RATE=29.970
    0f5abd5dd85c257f848ebe8aa983e7792b88f127_Ott_Hls_Ts_Avc_Aac_16x9_960x540p_30Hz_3.5Mbps_qvbr.m3u8
    #EXT-X-STREAM-INF:BANDWIDTH=909792,AVERAGE-BANDWIDTH=512858,CODECS="avc1.640028,mp4a.40.5",RESOLUTION=1278x718,FRAME-RATE=29.970
    0f5abd5dd85c257f848ebe8aa983e7792b88f127_Ott_Hls_Ts_Avc_Aac_16x9_1280x720p_30Hz_3.5Mbps_qvbr.m3u8
    #EXT-X-STREAM-INF:BANDWIDTH=2427362,AVERAGE-BANDWIDTH=1172231,CODECS="avc1.640028,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=29.970
    0f5abd5dd85c257f848ebe8aa983e7792b88f127_Ott_Hls_Ts_Avc_Aac_16x9_1920x1080p_30Hz_8.5Mbps_qvbr.m3u8

    저는 이 중에서도 가장 화질이 좋은 마지막 url에서 다운로드하고 싶기 때문에, meta_info_url에 lines[-1]을 할당했습니다. 참고로 이 목록 중 첫 번째 url에 get을 보내면 다음과 같은 데이터가 반환됩니다.

    #EXTM3U
    #EXT-X-VERSION:3
    #EXT-X-TARGETDURATION:7
    #EXT-X-MEDIA-SEQUENCE:1
    #EXT-X-PLAYLIST-TYPE:VOD
    #EXTINF:6,
    0f5abd5dd85c257f848ebe8aa983e7792b88f127_Ott_Hls_Ts_Avc_Aac_16x9_960x540p_30Hz_3.5Mbps_qvbr_00001.ts
    #EXTINF:6,
    0f5abd5dd85c257f848ebe8aa983e7792b88f127_Ott_Hls_Ts_Avc_Aac_16x9_960x540p_30Hz_3.5Mbps_qvbr_00002.ts
    #EXTINF:6,
    0f5abd5dd85c257f848ebe8aa983e7792b88f127_Ott_Hls_Ts_Avc_Aac_16x9_960x540p_30Hz_3.5Mbps_qvbr_00003.ts
    #EXTINF:6,

    이 목록들은 지금 페이지에서 시청 중인 비디오를 6초 단위로 쪼개어서 받아올 수 있는 경로 모음입니다.

     

    이를 응용해서 다음과 같이 코드를 짰습니다.

    resp = requests.get(url=(root_url + meta_info_url), headers=headers)
    if resp.status_code != 200:
        print(resp.text)
        return None
    # get source url list
    sources = [src for src in resp.text.strip().split('\n') if '#' not in src]

    이제 sources에 담긴 url에 요청만 하면 6초 단위의 비디오를 받아올 수 있는 겁니다. 이제 이 6초 단위의 비디오를 videos라는 배열에 담은 후, 파일에 써줍니다. 이 과정은 다음 코드로 구현됩니다.

    videos = []
    for idx, src in enumerate(sources):
        print(f'영상 다운로드 중... ({idx / len(sources) * 100:<4.1f}%)', end='\r')
        resp = requests.get(url=(root_url + src), headers=headers)
        if resp.status_code == 200:
            videos.append(resp.content)
    print('영상 다운로드 완료. 파일로 다운로드합니다.')
    
    # 다운로드 받을 장소.
    src_path = os.path.join(DEST_PATH, lecture_title)
    if not os.path.isdir(src_path):
        os.mkdir(src_path)
    with open(os.path.join(src_path, course_filename), 'wb') as f:
        for idx, vid in enumerate(videos):
            print(f'파일 합치는 중... ({idx / len(videos) * 100:<4.1f}%)', end='\r')
            f.write(vid)
        print('다운로드 완료.', lecture_title, '-', course_title)
        videos.clear()

    이상으로 인프런의 강의 영상을 하나의 파일로 다운로드 받을 수 있게 되었습니다. 완성된 코드는 깃허브에 올리겠습니다.

Designed by Tistory.