웹 스크래핑과 HTML 파싱을 통해 틱톡 동영상을 다운로드하는 프로그램을 개발한 과정을 공유합니다. 기존의 라이브러리 기반 방식이 아닌, 직접 HTML을 파싱하여 비디오 소스를 추출하는 방식으로 구현했습니다.
프로젝트 개요
개발 목표
- 틱톡 URL에서 HTML을 직접 파싱하여 비디오 소스 추출
- 다양한 URL 형식 지원 (표준 URL, 짧은 URL)
- MP4 형식으로 비디오 다운로드
- 깔끔하고 유지보수 가능한 코드 구조
기술 스택
- 언어: Python 3
- 주요 라이브러리:
requests
, pathlib
, json
, re
- 파싱 방식: 정규표현식 + JSON 파싱
기술적 접근 방법
1. HTML 파싱의 장점
# 기존 라이브러리 방식
yt-dlp [URL] # 블랙박스
# HTML 파싱 방식
html = get_html(url) # 투명한 과정
video_data = parse_video_data(html)
download_video(video_data['video_url'])
HTML 파싱 방식의 장점:
- 동작 원리가 투명함
- 커스터마이징 가능
- 의존성 최소화
- 학습 효과
2. 다층 파싱 전략
틱톡은 비디오 데이터를 여러 방식으로 제공하므로, 단계적 접근법을 사용했습니다:
- 스크립트 태그 파싱:
__UNIVERSAL_DATA_FOR_REHYDRATION__
- 글로벌 객체 파싱:
SIGI_STATE
- 정규표현식 직접 추출: 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())
포인트:
- 모바일 User-Agent 사용으로 더 나은 호환성
- 세션 재사용으로 성능 최적화
- 적절한 헤더 설정
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
특징:
- HEAD 요청으로 효율적인 리디렉션 처리
- 다양한 짧은 URL 형식 지원
- 안전한 예외 처리
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.)
설계 원칙
- 단일 책임 원칙: 각 메서드는 하나의 기능만 담당
- 개방-폐쇄 원칙: 새로운 파싱 방식 추가 용이
- 의존성 역전: 구체적인 구현이 아닌 인터페이스에 의존
상수 정의로 유지보수성 향상
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
작업 완료!
개선 사항 및 확장 가능성
현재 구현의 강점
- 깔끔한 코드 구조
- 다양한 URL 형식 지원
- 단계별 파싱 전략
- 안전한 예외 처리
- 진행률 표시
향후 개선 방향
- 비동기 처리로 성능 향상
- 배치 다운로드 기능
- 품질 선택 옵션
- 재시도 메커니즘
- 로깅 시스템
마무리
이번 프로젝트를 통해 웹 스크래핑과 HTML 파싱의 실제 적용 사례를 경험할 수 있었습니다. 특히 다음과 같은 점들을 배웠습니다:
- 다층 파싱 전략: 하나의 방법이 실패해도 다른 방법으로 대응
- 사용자 경험: 진행률 표시로 사용자 친화적인 인터페이스
- 코드 구조: 유지보수 가능한 클린 코드의 중요성
- 예외 처리: 웹 스크래핑에서의 robust한 에러 핸들링
웹 스크래핑은 항상 변화하는 웹사이트 구조에 대응해야 하는 도전이 있지만, 체계적인 접근과 좋은 코드 구조로 이를 극복할 수 있다는 것을 확인했습니다.
이 프로젝트는 교육 목적으로 개발되었으며, 실제 사용 시에는 해당 플랫폼의 이용약관을 확인하시기 바랍니다.
댓글