What the TikTok Video scraper collects
Reads an account's recent videos or any named video from TikTok's server-rendered embed and video pages. Each row carries the caption, creation date, exact likes, comments, shares and saves, the sound, hashtags and mentions, caption tracks, direct media URLs and photo-post stills, as JSON or CSV.
Account's recent videos
Input: A handle, @handle or profile URL (comma-separated for several)Reads the account's embed window, then each video's own page for the full row — engagement, sound, hashtags, subtitles.
Named videos
Input: Video URLs or bare 19-digit video idsOne full row per video; every video has its own address, so they can be fetched in parallel.
Fast embed-only rows
Input: A handleSkips the per-video pages: caption, rounded play count and media URLs only, about 6× faster, with data_source marking every row as embed.
From zero to TikTok Video data in 3 steps
Install
Clone the repository and install one browser engine. Python 3.9+.
$ git clone https://github.com/2scraper/tiktok-video-scraper && cd tiktok-video-scraper $ python3 -m venv venv $ ./venv/bin/pip install -r requirements.txt -r requirements-playwright.txt $ ./venv/bin/python playwright_scraper.py --url nasa
Run a scraper
Pick what you need and run it. Every flag is documented in the README.
$ ./venv/bin/python playwright_scraper.py --url nasa,zachking --format both$ ./venv/bin/python playwright_scraper.py --mode video --concurrency 3 --url "7665075736742530317,7686600539525762318,7681723633164717325"$ ./venv/bin/python playwright_scraper.py --url nasa --no-enrichGet structured data
Rows land in JSON or CSV (
--format), with a.meta.jsonsidecar recording status, pages fetched and why the run stopped.
TikTok Video data fields
57 columns per row in a predictable schema. The most useful ones:
| Field | What it holds |
|---|---|
video_id | TikTok's 19-digit video id |
description | The video caption |
created_at | Creation time (UTC) |
author_username | The creator's handle |
play_count | Play count, rounded by TikTok |
digg_count | Exact like count |
comment_count | Exact comment count |
share_count | Exact share count |
collect_count | Exact save count |
music_title | The sound's title |
hashtags | Hashtags from TikTok's own entity data |
mentions | Mentioned accounts by real handle, not display text |
subtitle_url | Downloadable WebVTT caption track |
download_addr | Direct media URL (signed, expires) |
content_type | video or photo (photo-mode carousel) |
data_source | Which route(s) produced the row |
[
{
"source": "tiktok.com",
"scraped_at": "2026-09-22T15:16:46Z",
"url": "https://www.tiktok.com/@nasa/video/7665075736742530317",
"sku": "7665075736742530317",
"title": "Something big just landed on TikTok.",
"video_id": "7665075736742530317",
"description": "Something big just landed on TikTok.",
"created_at": "2026-07-21T20:09:02+00:00",
"author_username": "nasa",
"author_id": "7664638705177150477",
"author_nickname": "NASA",
"author_sec_uid": "MS4wLjABAAAAU9BRVzC8oCaegVnia8IbqWhPb_-dbU7s00Y3wS1_Nx8g5RUaYvyXrpejgjdxTwd6",
"author_verified": true,
"play_count": 1400000,
"digg_count": 91500,
"comment_count": 2756,
"share_count": 3078,
"collect_count": 6767,
"repost_count": 0,
"stats_source": "statsV2",
"content_type": "video",
"image_count": null,
"image_urls": null,
"image_title": null,
"duration_seconds": 25,
"width": 720,
"height": 1280,
"ratio": "720p",
"definition": "720p",
"video_format": "mp4",
"video_size_bytes": 3356735,
"bitrate": 1066942,
"play_addr": "https://v16-webapp-prime.tiktok.com/video/tos/alisg/tos-alisg-ve-37c799-sg/okRjOkUA6WdC…",
"download_addr": "https://v16-webapp-prime.tiktok.com/video/tos/alisg/tos-alisg-ve-37c799-sg/osBro6wCGRle…",
"cover_url": "https://p16-common-sign.tiktokcdn-eu.com/tos-useast5-p-0068-tx/oAUhkWfCZC7g6ALj2HZgOeEL…",
"dynamic_cover_url": "https://p16-common-sign.tiktokcdn-eu.com/tos-useast5-p-0068-tx/ogCUKkOrAAd2Hj0YAgL4vokG…",
"media_expires_at": "2026-09-24T15:17:12+00:00",
"music_id": "7665075815390645006",
"music_title": "original sound",
"music_author": "NASA"
},
{
"source": "tiktok.com",
"scraped_at": "2026-09-22T15:16:46Z",
"url": "https://www.tiktok.com/@nasa/video/7686893699099413773",
"sku": "7686893699099413773",
"title": "Another big week in the sky and beyond at NASA 🚀 🔭 Roman gets a major mission-life bo…",
"video_id": "7686893699099413773",
"description": "Another big week in the sky and beyond at NASA 🚀 🔭 Roman gets a major mission-life bo…",
"created_at": "2026-09-18T16:00:00+00:00",
"author_username": "nasa",
"author_id": "7664638705177150477",
"author_nickname": "NASA",
"author_sec_uid": "MS4wLjABAAAAU9BRVzC8oCaegVnia8IbqWhPb_-dbU7s00Y3wS1_Nx8g5RUaYvyXrpejgjdxTwd6",
"author_verified": true,
"play_count": 130500,
"digg_count": 9698,
"comment_count": 316,
"share_count": 348,
"collect_count": 781,
"repost_count": 0,
"stats_source": "statsV2",
"content_type": "video",
"image_count": null,
"image_urls": null,
"image_title": null,
"duration_seconds": 92,
"width": 720,
"height": 1280,
"ratio": "720p",
"definition": "720p",
"video_format": "mp4",
"video_size_bytes": 10727129,
"bitrate": 926049,
"play_addr": "https://v16-webapp-prime.tiktok.com/video/tos/alisg/tos-alisg-ve-37c799-sg/oc5dqgYAEIsc…",
"download_addr": "https://v16-webapp-prime.tiktok.com/video/tos/alisg/tos-alisg-ve-37c799-sg/o8abmgkGVCqm…",
"cover_url": "https://p16-common-sign.tiktokcdn-eu.com/tos-useast5-p-0068-tx/o4BjAVnVuiCAcCqAE1iB7Sf3…",
"dynamic_cover_url": "https://p19-common-sign.tiktokcdn-eu.com/tos-useast5-p-0068-tx/o4BjAVnVuiCAcCqAE1iB7Sf3…",
"media_expires_at": "2026-09-24T15:18:20+00:00",
"music_id": "7686893746084547342",
"music_title": "original sound - NASA",
"music_author": "NASA"
}
]First 2 of 11 rows from a real run — sample_output.json. Long values shortened here.
Handles what breaks naive TikTok Video scrapers
Mentions resolved to the right account
The text after an @ in a caption is a display name: "@sadie" pointed at the account sad_i_e while @sadie is a different real account. Hashtags and mentions come from TikTok's own entity data, never from slicing the caption.
Photo posts are not broken videos
Photo-mode carousels share the /video/ URL with zero duration and no play address. They come out as content_type photo with image_count, image_urls and their own image_title.
Empty answers are not empty accounts
TikTok's paginated feed answers HTTP 200 with an empty body. That gets its own state, counted as blocked (exit 3), instead of reporting an account with no videos.
WAF interstitial handled by a browser
TikTok's "Please wait..." WAF page is a JavaScript challenge, not a captcha. --transport auto recognises it and switches to a browser, which cleared it 3 of 3 times in testing.
Scraping infrastructure included
The same tested core runs every 2scraper repository, so the hard parts work the same way on every site.
CAPTCHA handling
Detects the challenge a page really serves and, with a 2Captcha key, solves it only when it blocks the data — a capped number of solves per page.
Proxy rotation
Single proxy or a pool from a file, rotated on blocks with a fresh browser per exit. Credentials stay out of argv and logs.
Fingerprints
Optional complete device identity from the 2Captcha Fingerprint API — user agent, client hints, locale and timezone that agree with each other.
Scraping Browser API
Point --cdp-endpoint at a managed Chromium with its own exit country — no browser infrastructure to run yourself.
Honest run metadata
Every run writes a .meta.json sidecar and distinct exit codes: complete, blocked, empty and partial are never confused, and a failed run never overwrites good data.
Diff two runs
diff_runs.py compares two complete TikTok Video snapshots by id — new, removed and changed rows for monitoring.
What teams do with TikTok Video data
Creator and campaign analytics
Track exact likes, comments, shares and saves on an account's latest videos or a list of campaign videos.
Sound and trend research
See which sounds, hashtags and mentions recent videos use, read from TikTok's own structured data.
Transcript collection
Download machine-generated WebVTT caption tracks where a video carries one.
Media archiving
Capture direct video, cover and photo URLs with their expiry time, so you know how long each one stays valid.
Free to run. Pay only for what the site requires.
Runs with no key, proxy or account
Both routes are server-rendered and were served to a bare datacentre address with no credentials (measured 2026-09-22); a daily canary re-tests that from a GitHub runner with no secrets. The paid products buy volume from many addresses, a specific exit country and no browser infrastructure of your own — not access. Through a residential pool a plain HTTP client met TikTok's WAF interstitial more often, and a browser cleared it, which is why --transport auto falls back to one.
Optional 2Captcha services — captcha solving, the Scraping Browser API, residential proxies and fingerprints — work behind one key.
Good to know
- Only an account's 10-12 most recent videos are reachable; the back catalogue is not.
- Follower counts are not included; they live in tiktok-profile-scraper.
- Hashtag feeds and keyword search are not implemented.
2scraper vs. hosted scraper APIs
| 2scraper | Typical hosted API | |
|---|---|---|
| Code | Open source, MIT — read and patch it | Closed |
| Cost | Free; optional services billed only when used | Subscription or per request / per result |
| Runs on | Your machine, server or CI | The vendor's infrastructure |
| Output | JSON/CSV files + run metadata | API responses |
| When the site changes | Fix it yourself or open an issue / PR | Wait for the vendor |
TikTok Video scraper FAQ
How many videos per account can it get?
The 10 to 12 most recent that the embed route serves. The paginated back-catalogue feed answers with an empty body, so each run's sidecar marks the window as a sample of the account.
Why is play_count rounded but likes exact?
TikTok rounds the play count in both its stats objects, while likes, comments, shares and saves are exact.
Do I need a proxy or a key?
No. Both routes answered plain HTTPS from a datacentre address. Proxies and the Scraping Browser API are for volume and geography.
Can it scrape hashtag feeds or search?
No — those pages server-render no videos and the request that fills them is refused, so this repo refuses them with that reason. Profile stats are in tiktok-profile-scraper.
Do media URLs last?
No, they are signed and expire. media_expires_at says when, and diff_runs.py ignores URL changes so videos are not reported as changed on every run.
Is scraping TikTok legal?
It reads public pages TikTok serves to any visitor, with no login. Public data; follow the site's terms and the law.