이 저장소가 대체 무엇인가.
douyin-downloader는 抖音(Douyin, 중국 본토판 TikTok)의 콘텐츠를 터미널 명령 한 줄 또는 설정 파일 하나로 대량 수집하는 파이썬 도구다. 단일 영상부터 특정 크리에이터의 전체 게시물·좋아요·컬렉션·음악·즐겨찾기까지, 그리고 라이브 방송 녹화까지 다룬다.
앱에서 영상을 보는 건 자판기 앞에서 버튼을 눌러 캔을 받는 것과 같다. 하지만 자판기(앱)는 캔에 스티커(워터마크)를 붙이고, 한 번에 하나씩만 준다. 이 도구는 자판기가 재고를 채울 때 쓰는 뒷문(내부 API)을 찾아, 스티커 붙기 전의 원본 캔을 트럭째 받아온다.
문제는 그 뒷문에 "직원만 아는 암호 노크"(요청 서명)가 걸려 있다는 것. 이 프로젝트 코드의 절반은 그 노크 소리를 정확히 재현하는 데 쓰인다.
버전은 2.0.0이며, CLI 도구 + REST API 서버 모드 + (베타) 데스크톱 GUI(Douzy) 세 형태를 같은 파이썬 백엔드로 공유한다. 라이선스는 관대한 MIT. 저자는 README에 "무료! 무료! 무료!"를 반복하며 상업적 의도가 없음을 강조한다.
이 도구는 비공개 API를 역이용하고 워터마크(저작자 표시)를 제거한다. 이는 대상 플랫폼의 이용약관 위반일 수 있고, 남의 창작물을 무단 재배포하면 저작권 침해가 된다. 이 문서는 스크래핑·비동기·안티봇 우회의 소프트웨어 엔지니어링을 학습하기 위한 자료이지, 무단 수집을 권하는 것이 아니다. 실제 사용은 본인이 올린 콘텐츠 백업 등 합법적 범위로 한정할 것.
트렌딩 이유와, 흔한 "다운로더 스크립트"와의 결정적 차이.
GitHub에 널린 다운로더 대부분은 requests.get() 한 뭉치의 스파게티 스크립트다. 이 프로젝트는 다르다. 109개 파이썬 파일이 core / control / storage / auth / config / utils로 계층 분리돼 있고, 60개 이상의 테스트 파일(property-based 테스트 포함)과 GitHub Actions CI, Docker, 심지어 각 폴더마다 AGENTS.md(AI 코딩 에이전트용 규칙서)까지 있다.
보통 다운로더 = 라면 하나 끓이는 즉석 조리. 이 프로젝트 = 주방을 구역별로 나누고(전처리·조리·플레이팅), 레시피를 문서화하고, 위생 점검(테스트)까지 통과시킨 영업용 주방. 배우기엔 후자가 압도적으로 값지다.
더우인 서버는 자동화 요청을 막으려고 요청마다 X-Bogus·A-Bogus라는 서명 파라미터를 요구한다. 브라우저가 자바스크립트로 만들어 붙이는 값인데, 이 프로젝트는 그 알고리즘을 파이썬으로 포팅(이식)해 두었다(utils/xbogus.py, utils/abogus.py). 여기에 더해:
즉, "서버가 막으면 어떻게 우회하나"의 여러 계층을 한 코드베이스에서 관찰할 수 있다.
비슷한 상용 서비스는 유료 구독이거나, 사용자의 링크를 자기네 서버로 보낸다. 이 도구는 내 컴퓨터에서 로컬 실행되어 데이터가 밖으로 안 나간다. 자기호스팅(Self-hosted)·프라이버시 선호 흐름과 맞물려 TrendShift 상위권에 올랐다. 중국어권 개발자 커뮤니티(linux.do 등)에서의 화제성도 모멘텀에 기여했다.
대상이 더우인이라는 점은 부차적이다. 진짜 가치는 비동기 네트워크 프로그래밍 · 안티봇 우회 · 견고한 재시도/속도제어 · 계층형 아키텍처 · 전략/팩토리 패턴을 실전 규모의 코드로 통째로 볼 수 있다는 것.
무엇으로 만들어졌나 — 계층별로.
| 기술 | 역할 | 왜 이걸 썼나 |
|---|---|---|
| Python ≥3.9 | 전체 언어 | 스크래핑 생태계(요청·파싱) 풍부, 3.9~3.12 지원 |
| asyncio | 동시성 엔진 | 수백 개 다운로드를 스레드 없이 동시 처리(I/O 대기 시간 활용) |
| aiohttp | 비동기 HTTP 클라이언트 | API 호출·파일 다운로드를 논블로킹으로 |
| httpx | 보조 HTTP | storage/file_manager.py가 런타임에 사용(요구사항에 명시 고정) |
| aiofiles | 비동기 파일 I/O | 디스크 쓰기도 이벤트 루프를 막지 않게 |
| aiosqlite | 비동기 SQLite | 다운로드 이력 DB를 async로 조회/기록 |
| 기술 | 역할 |
|---|---|
| xbogus.py | X-Bogus 서명 생성(구형 엔드포인트용). Evil0ctal 프로젝트 기반 Apache-2.0 이식 |
| abogus.py | A-Bogus 서명 + 브라우저 지문(fingerprint) 생성(신형 엔드포인트용, 선택적) |
| gmssl | 중국 국산 암호(SM3 등) — 서명 알고리즘 내부 해시에 필요 |
| auth/cookie_manager, ms_token_manager | 쿠키·ms_token 로드/정제/갱신, 민감 쿠키 차단 목록 관리 |
| 모듈 | 역할 |
|---|---|
| control/rate_limiter.py | 초당 요청 수 제한(기본 2 req/s) + 랜덤 지터로 봇 탐지 회피 |
| control/retry_handler.py | 지수 백오프 재시도(1s→2s→5s, 기본 3회) |
| control/queue_manager.py | 동시 다운로드 개수 제한(기본 5) — 세마포어식 큐 |
| 기술 | 역할 |
|---|---|
| SQLite (aiosqlite) | 다운로드 이력·중복제거·증분 다운로드의 단일 진실원 |
| rich | 터미널 진행바·컬러 로그(예쁜 CLI UX) |
| imageio-ffmpeg (0.6.0 고정) | 번들 ffmpeg 정적 바이너리 — 음성 추출 후 전사(transcription)에 사용 |
| PyYAML | config.yml 설정 파싱 |
| Playwright optional [browser] | 실제 브라우저 폴백(페이지네이션 차단·CAPTCHA 대응) |
| openai-whisper optional [transcribe] | 영상 음성 → 자막 텍스트 전사 |
| FastAPI + uvicorn optional [server] | --serve REST API 서버 모드 |
| pytest + hypothesis + ruff [dev] | 테스트(속성기반 포함) + 린트 |
pip install "douyin-downloader[browser]"처럼 대괄호로 골라 까는 추가 패키지 묶음. 무거운 Playwright·Whisper를 안 쓰는 사람은 안 깔아도 되게 하는 배려.데이터가 링크 한 줄에서 파일까지 흐르는 전 과정.
DownloaderFactory.create(url_type, ...)가 URL 종류에 따라 알맞은 다운로더 객체를 만들어 돌려준다. 호출부(cli/main.py)는 "어떤 다운로더가 필요한지" 몰라도 되고, 새 종류(예: 라이브)를 추가할 때 팩토리에 분기 한 줄만 더하면 된다.
# core/downloader_factory.py — 타입 → 객체 매핑 (핵심만)
if url_type == "video": return VideoDownloader(**common_args)
elif url_type == "user": return UserDownloader(**common_args)
elif url_type == "collection": return MixDownloader(**common_args)
elif url_type == "music": return MusicDownloader(**common_args)
elif url_type == "live": return LiveDownloader(**common_args)
# common_args = config·api_client·file_manager·database·
# rate_limiter·retry_handler·queue_manager ... (의존성 주입)
한 크리에이터를 긁을 때도 게시물(post) · 좋아요(like) · 컬렉션(mix) · 음악(music) · 즐겨찾기(collect)는 API 엔드포인트와 페이지네이션 규칙이 제각각이다. 이걸 core/user_modes/ 아래 전략 클래스로 하나씩 떼어냈다. base_strategy.py가 공통 뼈대, 각 *_strategy.py가 모드별 차이만 구현한다.
전략 패턴은 "교체형 드릴 비트"다. 드릴 본체(UserDownloader)는 하나지만, 나무용·콘크리트용·금속용 비트(전략)를 상황에 맞게 갈아 끼운다. 새 재질이 나와도 비트만 새로 만들면 본체는 안 건드린다.
APIClient·RateLimiter·RetryHandler·Database 같은 무거운 객체는 한 번 만들어 모든 다운로더에 주입한다. 덕분에 속도 제한이 전체에 일관 적용되고, DB 커넥션도 공유된다. common_args 딕셔너리가 그 주입 통로다.
# control/rate_limiter.py — 지터가 "락 안"에 있는 이유
async with self._lock:
# 최소 간격(1/2초) 못 채웠으면 그만큼 대기
if time_since_last < self.min_interval:
await asyncio.sleep(self.min_interval - time_since_last)
# 지터도 락 안에서 — 다음 요청이 "실제 발사 시각" 기준으로 대기하도록
await asyncio.sleep(random.uniform(0, 0.5))
self.last_request = time.time()
지터를 락 밖에 두면 두 요청이 거의 동시에 나갈 수 있다 — 코드 주석이 이 함정을 정확히 짚는다. pyproject.toml조차 "imageio-ffmpeg 0.6.0은 macOS arm64 휠을 처음 제공하는 버전이라 고정" 같은 결정의 이유를 남긴다. 협업·유지보수를 진지하게 고려한 코드베이스라는 신호.
더우인이 status_code == 2483(또는 "请先登录=먼저 로그인하세요")를 주면 LoginRequiredError가 발생하고, cli/main.py의 _run_with_relogin 래퍼가 이를 잡아 대화형 재로그인 후 자동 재시도한다. 긴 배치 작업 중 세션이 끊겨도 멈추지 않게 하는 실전 배려다(docs/superpowers/specs/2026-06-25-auto-relogin-design.md에 설계 문서까지 있음).
폴더 이름만 봐도 책임이 보이도록 나눴다.
core = 무엇을 받나, control = 어떻게 안전하게 받나, storage = 어디에 어떻게 쌓나, auth = 어떻게 서버를 통과하나. 이 4개 폴더만 이해하면 전체가 손에 잡힌다. 나머지(cli·config·utils·server)는 이들을 감싸는 껍데기·도구.
주목할 관례: 폴더마다 AGENTS.md가 있어 Claude Code 같은 AI 에이전트가 그 폴더를 수정할 때 지켜야 할 규칙을 명시한다. 또 CLAUDE.md는 이 CLI가 데스크톱 앱과 백엔드 코드를 공유하니 "공유 로직을 고치면 양쪽에 똑같이 반영하라"고 지시한다. "사람+AI 협업"을 전제로 설계된 최신 리포의 전형.
이 코드에서 구체적으로 무엇을 훔쳐 배울 수 있나.
튜토리얼의 asyncio.sleep 예제를 넘어, 실제 네트워크·파일·DB를 전부 async로 엮은 코드를 볼 수 있다. aiohttp(요청)·aiofiles(디스크)·aiosqlite(DB)가 한 이벤트 루프 위에서 협력한다. asyncio.Lock으로 공유 상태(마지막 요청 시각)를 보호하는 법도 rate_limiter에서 배운다.
utils/xbogus.py는 커스텀 문자셋("Dkdpgh4ZKsQB80/Mfvw...")과 자체 base64·해시로 서명을 조립한다. "브라우저 JS가 하는 일을 서버 언어로 재현"하는 리버스 엔지니어링의 살아있는 예다. (이 파일은 Evil0ctal의 Douyin_TikTok_Download_API 프로젝트에서 온 Apache-2.0 코드다.)
실서비스 스크래퍼가 반드시 갖춰야 할 속도 제한 + 지수 백오프 재시도 + 동시성 제한 + 지터를 각각 독립 모듈로 구현한 참고서. 이 패턴은 더우인뿐 아니라 어떤 API 클라이언트에도 그대로 이식 가능하다.
팩토리·전략·DI 덕에 각 부품을 가짜(mock)로 갈아끼워 단위 테스트할 수 있다. tests/의 60여 개 파일, 특히 hypothesis 기반 속성 기반 테스트(무작위 입력 수천 개로 불변식 검증) — test_notifier_redactor_property.py(민감정보 마스킹이 항상 성립하는가), test_proxy_validator_parity.py 등을 읽어볼 것.
control/rate_limiter.py는 40줄 남짓이고 외부 의존이 없다. 내가 만든 아무 API 호출 루프에 붙여 "초당 N회 + 지터"를 걸어보고, 지터를 락 밖으로 빼면 어떻게 동시 발사가 생기는지 로그로 확인해보라.
돌리는 데 뭐가 필요한가.
| 항목 | 요구 |
|---|---|
| Python | 3.9 ~ 3.12 |
| OS | Windows / macOS / Linux (크로스플랫폼). Docker 이미지 제공 |
| 기본 설치 | pip install -e . — aiohttp·httpx·rich·SQLite 등 경량 |
| 디스크 | 다운로드 대상만큼. 도구 자체는 수십 MB(ffmpeg 정적 바이너리 포함) |
| 브라우저 폴백 | [browser] 설치 + Playwright 크로미움(수백 MB) |
| 전사(자막) | [transcribe] Whisper — CPU로도 되나 GPU면 빠름. OpenAI 호환 전사 API 키(선택) |
| 필수 준비물 | 더우인 쿠키(로그인 상태). 즐겨찾기·좋아요 등은 로그인 필수 |
| 네트워크 | 중국 서비스라 지역/프록시 설정이 필요할 수 있음(proxy 설정 지원) |
핵심은 무겁지 않다는 것. GPU도, 대용량 RAM도 기본 사용엔 불필요하다. 병목은 오직 네트워크 대역폭과 서버의 속도 제한이다 — 그래서 코드가 속도제한·재시도에 그토록 공을 들인다.
난이도별로 — 코드를 읽고 손대며 배우기. (합법적 범위: 본인 콘텐츠·공개 학습용 소규모로)
core/url_parser.py의 _extract_video_id 등 정규식이 /video/(\d+) 같은 패턴으로 ID를 뽑는 과정을 따라가고, 각 URL 타입(video·user·collection·gallery·music·live)이 어떤 ID로 매핑되는지 표로 정리해보라.
config.example.yml을 복사해 쿠키를 넣고, 본인 계정에 올린 테스트 영상 링크 하나로 douyin-dl --url ...을 실행. Downloaded/작성자/날짜_제목/ 폴더 구조와 함께 저장되는 *.json 메타데이터·download_manifest.jsonl을 열어 내용을 확인하라.
설정에서 rate_limit과 동시성(concurrency)을 낮췄다/높였다 하며 로그의 재시도 빈도를 비교. retry_handler의 백오프가 실제로 1s→2s→5s로 늘어나는지 로그로 검증하라. (과도한 요청은 계정 차단 위험 — 소규모로만.)
utils/notifier.py는 Bark·Telegram·Webhook을 지원한다. 이를 참고해 Slack 또는 Discord 웹훅 알림을 추가하고, test_notifier_redactor_property.py 스타일로 "토큰이 로그에 노출되지 않는가"를 검증하는 테스트를 작성하라.
core/user_modes/base_strategy.py를 상속해 새 수집 전략(예: 특정 해시태그·검색 결과 기반)을 구현하고 user_mode_registry.py에 등록. 기존 다운로더 본체를 건드리지 않고 기능이 붙는 "개방-폐쇄 원칙(OCP)"을 몸으로 체감하라.
이 레포를 발판 삼아 어디로 뻗어갈까 — 4주 코스.
| 주차 | 주제 | 학습 내용 · 연결점 |
|---|---|---|
| 1주차 | 파이썬 비동기 기초 | coroutine · event loop · async/await · asyncio.gather · Semaphore/Lock. → 이 레포의 core·control 재독 |
| 2주차 | HTTP·웹 스크래핑 원리 | 요청/응답 헤더·쿠키·User-Agent · 개발자도구 Network 탭으로 비공개 API 관찰 · robots.txt·이용약관의 의미 |
| 3주차 | 안티봇 & 리버스 엔지니어링 | 요청 서명·브라우저 지문·CAPTCHA · Playwright로 헤드리스 브라우저 제어 · 왜 서명 알고리즘을 포팅하나 |
| 4주차 | 견고성 & 아키텍처 | 지수 백오프·서킷브레이커·idempotency · 팩토리/전략/DI 패턴 · pytest + hypothesis 속성기반 테스트 작성 |
배운 걸 종합해 "내가 좋아하는 합법적 공개 API(예: GitHub·Reddit·공공데이터)용 견고한 수집기"를 만들어보라. 이 레포의 control·storage 계층을 뼈대로 재사용하면, 대상만 바꿔 곧바로 프로덕션급 클라이언트가 나온다.
이 문서에 나온 용어 총정리.
| 용어 | 뜻 |
|---|---|
| 스크래핑 | 웹사이트/앱의 데이터를 프로그램으로 자동 수집하는 것 |
| 비공개 API | 앱 내부에서만 쓰라고 만든, 문서화되지 않은 서버 엔드포인트 |
| X-Bogus / A-Bogus | 더우인이 요청 진위를 검증하려 요구하는 서명 파라미터. 이 레포가 파이썬으로 재현 |
| ms_token | 세션마다 발급되는 보안 토큰. 만료되면 요청 거절 → 자동 갱신 필요 |
| 워터마크 제거 | 서버가 붙이기 전의 원본(무워터마크) 소스를 선택해 받는 것 |
| Rate limiting | 초당 요청 수 제한. 서버 과부하·차단 방지 + 봇 탐지 회피용 지터 동반 |
| 지수 백오프 | 재시도 간격을 1→2→5초처럼 점점 늘리는 전략 |
| 지터(jitter) | 대기 시간에 섞는 무작위 값. 요청이 기계처럼 규칙적이지 않게 |
| 팩토리 패턴 | 입력에 따라 알맞은 객체를 대신 만들어주는 설계 |
| 전략 패턴 | 교체 가능한 알고리즘(모드)을 클래스로 캡슐화해 갈아끼우는 설계 |
| DI(의존성 주입) | 필요한 객체를 밖에서 만들어 넣어주는 것 — 테스트·재사용에 유리 |
| 속성 기반 테스트 | 수천 개 무작위 입력으로 "항상 성립해야 할 규칙"을 검증(hypothesis) |
| 브라우저 폴백 | API가 막히면 실제 브라우저(Playwright)를 띄워 우회/CAPTCHA 대응 |
| 증분 다운로드 | 이미 받은 것은 건너뛰고 새 것만 받기(SQLite 이력 기반) |
| AGENTS.md | AI 코딩 에이전트가 해당 폴더 작업 시 지켜야 할 규칙 문서 |
더 파고들 곳.