TokyoAJ

도쿄아재

PYTHON 2025.06.28

TikTok 동영상 다운로더 개발기 - HTML 파싱 방식

웹 스크래핑과 HTML 파싱을 통해 틱톡 동영상을 다운로드하는 프로그램을 개발한 과정을 공유합니다. 기존의 라이브러리 기반 방식이 아닌, 직접 HTML을 파싱하여 비디오 소스를 추출하는 방식으로 구현했습니다.


프로젝트 개요

개발 목표

  1. 틱톡 URL에서 HTML을 직접 파싱하여 비디오 소스 추출
  2. 다양한 URL 형식 지원 (표준 URL, 짧은 URL)
  3. MP4 형식으로 비디오 다운로드
  4. 깔끔하고 유지보수 가능한 코드 구조

기술 스택

  1. 언어: Python 3
  2. 주요 라이브러리: requests, pathlib, json, re
  3. 파싱 방식: 정규표현식 + JSON 파싱

기술적 접근 방법

1. HTML 파싱의 장점

# 기존 라이브러리 방식
yt-dlp [URL] # 블랙박스

# HTML 파싱 방식
html = get_html(url) # 투명한 과정
video_data = parse_video_data(html)
download_video(video_data['video_url'])

HTML 파싱 방식의 장점:

  1. 동작 원리가 투명함
  2. 커스터마이징 가능
  3. 의존성 최소화
  4. 학습 효과

2. 다층 파싱 전략

틱톡은 비디오 데이터를 여러 방식으로 제공하므로, 단계적 접근법을 사용했습니다:

  1. 스크립트 태그 파싱: __UNIVERSAL_DATA_FOR_REHYDRATION__
  2. 글로벌 객체 파싱: SIGI_STATE
  3. 정규표현식 직접 추출: fallback 방식

핵심 기능 구현

1. HTTP 세션 관리

class TikTokHTMLDownloader:
DEFAULT_USER_AGENT = (
'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) '
'AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1'
)
def __init__(self, download_folder="downloads"):
self.session = requests.Session()
self.session.headers.update(self._get_default_headers())

포인트:

  1. 모바일 User-Agent 사용으로 더 나은 호환성
  2. 세션 재사용으로 성능 최적화
  3. 적절한 헤더 설정

2. 짧은 URL 처리

def _resolve_short_url(self, url):
"""짧은 URL을 원본 URL로 변환"""
short_domains = ['[vm.tiktok.com](<http://vm.tiktok.com>)', '[vt.tiktok.com](<http://vt.tiktok.com>)']
if any(domain in url for domain in short_domains) or '/t/' in url:
try:
response = self.session.head(url, allow_redirects=True)
resolved_url = response.url
print(f"리디렉션된 URL: {resolved_url}")
return resolved_url
except Exception:
pass
return url

특징:

  1. HEAD 요청으로 효율적인 리디렉션 처리
  2. 다양한 짧은 URL 형식 지원
  3. 안전한 예외 처리

3. 다단계 비디오 데이터 파싱

방법 1: 스크립트 태그 파싱

def _extract_from_script_tag(self, html):
pattern = r'<script id="__UNIVERSAL_DATA_FOR_REHYDRATION__" type="application/json">(.*?)</script>'
match = [re.search](<http://re.search>)(pattern, html, re.DOTALL)
if match:
try:
json_data = json.loads([match.group](<http://match.group>)(1))
return self._find_video_in_data(json_data)
except json.JSONDecodeError:
pass
return None

방법 2: 글로벌 객체 파싱

def _extract_from_sigi_state(self, html):
pattern = r'window\["SIGI_STATE"\]\s*=\s*({.*?});'
match = [re.search](<http://re.search>)(pattern, html, re.DOTALL)
if match:
try:
json_data = json.loads([match.group](<http://match.group>)(1))
return self._find_video_in_data(json_data)
except json.JSONDecodeError:
pass
return None

방법 3: 정규표현식 직접 추출

VIDEO_PATTERNS = [
r'"playAddr":"([^"]*mp4[^"]*)",'
r'"downloadAddr":"([^"]*mp4[^"]*)",'
r'"play_addr":\s*{\s*"url_list":\s*\["([^"]*)",'
r'"download_addr":\s*{\s*"url_list":\s*\["([^"]*)",'
r'videoObject":\s*{\s*"contentUrl":\s*"([^"]*)",'
]

4. 진행률 표시 다운로드

def _save_file_with_progress(self, response, file_path, total_size):
"""진행률과 함께 파일 저장"""
with open(file_path, 'wb') as f:
downloaded = 0
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
downloaded += len(chunk)
if total_size > 0:
progress = (downloaded / total_size) * 100
print(f"\r다운로드 진행률: {progress:.1f}%", end='', flush=True)

코드 구조 및 설계

클래스 구조

TikTokHTMLDownloader
├── 상수 정의 (패턴, User-Agent 등)
├── __init__() - 초기화
├── process_url() - 메인 처리 로직
├── _get_html() - HTML 취득
├── _parse_video_data() - 비디오 데이터 파싱
├── _download_video() - 파일 다운로드
└── 헬퍼 메서드들 (_extract_*, _generate_*, etc.)

설계 원칙

  1. 단일 책임 원칙: 각 메서드는 하나의 기능만 담당
  2. 개방-폐쇄 원칙: 새로운 파싱 방식 추가 용이
  3. 의존성 역전: 구체적인 구현이 아닌 인터페이스에 의존

상수 정의로 유지보수성 향상

class TikTokHTMLDownloader:
# 비디오 URL 추출 패턴
VIDEO_PATTERNS = [
r'"playAddr":"([^"]*mp4[^"]*)",'
r'"downloadAddr":"([^"]*mp4[^"]*)",'
# ... 더 많은 패턴
]
# 제목 추출 패턴
TITLE_PATTERNS = [
r'"desc":"([^"]*)",'
r'<title>([^<]*)</title>',
# ... 더 많은 패턴
]

주요 구현 포인트

1. 안전한 파일명 생성

def _generate_filename(self, video_data):
"""안전한 파일명 생성"""
author = video_data.get('author', 'unknown')
title = video_data.get('title', 'untitled')
# 파일명에서 특수문자 제거
safe_author = re.sub(r'[^\w\-_.]', '', author)
safe_title = re.sub(r'[^\w\-_.\s]', '', title)[:50] # 제목은 50자로 제한
return f"{safe_author}_{safe_title}.mp4"

2. URL 디코딩 처리

def _decode_url(self, url):
"""URL 디코딩"""
return url.replace('\\u002F', '/').replace('\\/', '/')

3. 패턴 기반 정보 추출

def _extract_from_patterns(self, html, patterns, default):
"""패턴 리스트를 사용하여 HTML에서 정보 추출"""
for pattern in patterns:
match = [re.search](<http://re.search>)(pattern, html, re.DOTALL)
if match:
return [match.group](<http://match.group>)(1).strip()
return default

사용 방법

기본 사용법

# 인스턴스 생성
downloader = TikTokHTMLDownloader()

# URL 처리
url = "<https://www.tiktok.com/@username/video/1234567890>"
downloader.process_url(url)

커맨드라인 실행

python tiktok_[downloader.py](<http://downloader.py>) "<https://www.tiktok.com/@username/video/1234567890>"

다운로드 과정

처리 시작: <https://www.tiktok.com/@username/video/1234567890>
HTML에서 비디오 데이터 파싱 중...
비디오 URL 발견: <https://v16-webapp.tiktok.com/>...
비디오 다운로드 시작: username_[title.mp](<http://title.mp>)4
다운로드 진행률: 100.0%
다운로드 완료: downloads/username_[title.mp](<http://title.mp>)4
작업 완료!

개선 사항 및 확장 가능성

현재 구현의 강점

  1. 깔끔한 코드 구조
  2. 다양한 URL 형식 지원
  3. 단계별 파싱 전략
  4. 안전한 예외 처리
  5. 진행률 표시

향후 개선 방향

  1. 비동기 처리로 성능 향상
  2. 배치 다운로드 기능
  3. 품질 선택 옵션
  4. 재시도 메커니즘
  5. 로깅 시스템

마무리

이번 프로젝트를 통해 웹 스크래핑과 HTML 파싱의 실제 적용 사례를 경험할 수 있었습니다. 특히 다음과 같은 점들을 배웠습니다:

  1. 다층 파싱 전략: 하나의 방법이 실패해도 다른 방법으로 대응
  2. 사용자 경험: 진행률 표시로 사용자 친화적인 인터페이스
  3. 코드 구조: 유지보수 가능한 클린 코드의 중요성
  4. 예외 처리: 웹 스크래핑에서의 robust한 에러 핸들링

웹 스크래핑은 항상 변화하는 웹사이트 구조에 대응해야 하는 도전이 있지만, 체계적인 접근과 좋은 코드 구조로 이를 극복할 수 있다는 것을 확인했습니다.

이 프로젝트는 교육 목적으로 개발되었으며, 실제 사용 시에는 해당 플랫폼의 이용약관을 확인하시기 바랍니다.

댓글