<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged tiktok</title>
<link>https://coderlegion.com/tag/tiktok</link>
<description>Powered by Question2Answer</description>
<item>
<title>TikTok Scraper in Python: Profiles, Videos and the Walls You'll Hit (2026)</title>
<link>https://coderlegion.com/30800/tiktok-scraper-in-python-profiles-videos-and-the-walls-youll-hit-2026</link>
<description>&lt;p&gt;&quot;How do I scrape TikTok?&quot; usually means one of three things: follower counts for a list of creators, the latest videos from an account with their views and likes, or tracking how those numbers change over time. This guide builds that in plain Python, shows what works without a browser, and explains where the DIY approach stops working.&lt;/p&gt;
&lt;h3 id=&quot;what-you-can-get-without-logging-in&quot;&gt;What you can get without logging in&lt;/h3&gt;
&lt;p&gt;TikTok's web app is a React app, but the first HTML response for a profile page (&lt;code&gt;https://www.tiktok.com/@handle&lt;/code&gt;) contains a big JSON blob in a script tag:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;script id=&quot;__UNIVERSAL_DATA_FOR_REHYDRATION__&quot; type=&quot;application/json&quot;&amp;gt;{...}&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inside it, &lt;code&gt;__DEFAULT_SCOPE__[&quot;webapp.user-detail&quot;][&quot;userInfo&quot;]&lt;/code&gt; holds the profile (&lt;code&gt;user&lt;/code&gt;) and its counters (&lt;code&gt;stats&lt;/code&gt;). That's public data, and you can read it with &lt;code&gt;requests&lt;/code&gt; and the standard &lt;code&gt;json&lt;/code&gt; module. You don't need BeautifulSoup.&lt;/p&gt;
&lt;p&gt;The video list is different. The grid loads from an internal endpoint (&lt;code&gt;/api/post/item_list/&lt;/code&gt;) that needs signed query parameters (&lt;code&gt;msToken&lt;/code&gt;, &lt;code&gt;X-Bogus&lt;/code&gt;, &lt;code&gt;X-Gnarly&lt;/code&gt;, and so on) produced by obfuscated JavaScript. The signing changes often, so we'll treat that part as a known limit and come back to it later.&lt;/p&gt;
&lt;h3 id=&quot;setup&quot;&gt;Setup&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python -m venv .venv &amp;amp;&amp;amp; source .venv/bin/activate
pip install requests
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;step-1-fetch-a-profile&quot;&gt;Step 1: Fetch a profile&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import json
import re
import time
import requests

SESSION = requests.Session()
SESSION.headers.update({
    &quot;User-Agent&quot;: (
        &quot;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 &quot;
        &quot;(KHTML, like Gecko) Chrome/129.0 Safari/537.36&quot;
    ),
    &quot;Accept-Language&quot;: &quot;en-US,en;q=0.9&quot;,
})

DATA_RE = re.compile(
    r'&amp;lt;script id=&quot;__UNIVERSAL_DATA_FOR_REHYDRATION__&quot;[^&amp;gt;]*&amp;gt;(.*?)&amp;lt;/script&amp;gt;',
    re.S,
)

class ProfileError(Exception):
    pass

def fetch_profile(handle: str) -&amp;gt; dict:
    handle = handle.lstrip(&quot;@&quot;)
    resp = SESSION.get(f&quot;https://www.tiktok.com/@{handle}&quot;, timeout=20)
    resp.raise_for_status()

    match = DATA_RE.search(resp.text)
    if not match:
        raise ProfileError(&quot;rehydration blob missing (blocked or layout changed)&quot;)

    data = json.loads(match.group(1))
    detail = data.get(&quot;__DEFAULT_SCOPE__&quot;, {}).get(&quot;webapp.user-detail&quot;, {})
    if detail.get(&quot;statusCode&quot;, 0) != 0:
        raise ProfileError(f&quot;TikTok statusCode {detail.get('statusCode')}&quot;)

    info = detail[&quot;userInfo&quot;]
    user, stats = info[&quot;user&quot;], info[&quot;stats&quot;]
    return {
        &quot;handle&quot;: user[&quot;uniqueId&quot;],
        &quot;id&quot;: user[&quot;id&quot;],
        &quot;sec_uid&quot;: user[&quot;secUid&quot;],
        &quot;nickname&quot;: user.get(&quot;nickname&quot;),
        &quot;verified&quot;: user.get(&quot;verified&quot;, False),
        &quot;private&quot;: user.get(&quot;privateAccount&quot;, False),
        &quot;bio&quot;: user.get(&quot;signature&quot;, &quot;&quot;),
        &quot;followers&quot;: stats.get(&quot;followerCount&quot;),
        &quot;following&quot;: stats.get(&quot;followingCount&quot;),
        &quot;likes&quot;: stats.get(&quot;heartCount&quot;) or stats.get(&quot;heart&quot;),
        &quot;videos&quot;: stats.get(&quot;videoCount&quot;),
    }

if __name__ == &quot;__main__&quot;:
    print(fetch_profile(&quot;@tiktok&quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A few details that will save you some debugging:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Check &lt;code&gt;statusCode&lt;/code&gt;, not just the HTTP status.&lt;/strong&gt; TikTok returns HTTP 200 for missing or banned accounts. The blob then carries a non-zero &lt;code&gt;statusCode&lt;/code&gt; (10221 or 10202 are common) and no &lt;code&gt;userInfo&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep &lt;code&gt;sec_uid&lt;/code&gt;.&lt;/strong&gt; Handles can change, but the numeric &lt;code&gt;id&lt;/code&gt; and &lt;code&gt;secUid&lt;/code&gt; don't. Use them as your primary key.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Don't parse the visible &quot;1.2M&quot;.&lt;/strong&gt; The JSON holds exact integers, so use those.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;step-2-store-snapshots-in-sqlite&quot;&gt;Step 2: Store snapshots in SQLite&lt;/h3&gt;
&lt;p&gt;A single scrape is a fact. A series of scrapes shows a trend. Store every run as a row, so growth is just a &lt;code&gt;SELECT&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import sqlite3
from datetime import datetime, timezone

def init_db(path=&quot;tiktok.db&quot;):
    db = sqlite3.connect(path)
    db.execute(&quot;&quot;&quot;
        CREATE TABLE IF NOT EXISTS profile_snapshots (
            user_id   TEXT NOT NULL,
            handle    TEXT NOT NULL,
            taken_at  TEXT NOT NULL,
            followers INTEGER,
            likes     INTEGER,
            videos    INTEGER,
            PRIMARY KEY (user_id, taken_at)
        )
    &quot;&quot;&quot;)
    return db

def save_snapshot(db, p):
    db.execute(
        &quot;INSERT OR IGNORE INTO profile_snapshots VALUES (?,?,?,?,?,?)&quot;,
        (p[&quot;id&quot;], p[&quot;handle&quot;],
         datetime.now(timezone.utc).strftime(&quot;%Y-%m-%dT%H:00Z&quot;),
         p[&quot;followers&quot;], p[&quot;likes&quot;], p[&quot;videos&quot;]),
    )
    db.commit()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;taken_at&lt;/code&gt; value is rounded to the hour, so if a cron job accidentally runs twice in an hour, &lt;code&gt;INSERT OR IGNORE&lt;/code&gt; drops the duplicate.&lt;/p&gt;
&lt;h3 id=&quot;step-3-loop-over-many-creators-politely&quot;&gt;Step 3: Loop over many creators politely&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import random

def run(handles, db):
    for h in handles:
        for attempt in range(3):
            try:
                save_snapshot(db, fetch_profile(h))
                print(&quot;ok&quot;, h)
                break
            except ProfileError as e:
                print(&quot;skip&quot;, h, e)
                break
            except requests.RequestException as e:
                wait = 2 ** attempt * 5
                print(&quot;retry&quot;, h, e, f&quot;in {wait}s&quot;)
                time.sleep(wait)
        time.sleep(random.uniform(3, 7))

if __name__ == &quot;__main__&quot;:
    db = init_db()
    run([&quot;tiktok&quot;, &quot;nasa&quot;, &quot;duolingo&quot;], db)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then the growth query looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT handle,
       MAX(followers) - MIN(followers) AS gained,
       COUNT(*) AS samples
FROM profile_snapshots
WHERE taken_at &amp;gt;= strftime('%Y-%m-%dT00:00Z', 'now', '-7 days')
GROUP BY handle
ORDER BY gained DESC;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-walls-you-ll-hit&quot;&gt;The walls you'll hit&lt;/h3&gt;
&lt;p&gt;This is where most TikTok tutorials stop being honest. Here's what actually happens once the script runs on a schedule:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Datacenter IPs get a different page.&lt;/strong&gt; From a cloud VM you will often get a page with no rehydration blob, a &quot;verify&quot; screen, or a redirect. It works from a home connection and fails on a server. Residential proxies help, but they cost money and you have to rotate them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The blob moves.&lt;/strong&gt; In the last few years the data has lived in &lt;code&gt;SIGI_STATE&lt;/code&gt;, then in &lt;code&gt;__UNIVERSAL_DATA_FOR_REHYDRATION__&lt;/code&gt;, with different key paths each time. Wrap the parsing in a single function (as above) and alert when it raises, so a layout change doesn't silently produce empty rows.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Videos need signatures.&lt;/strong&gt; The profile HTML sometimes includes a few items, but often none. Full pagination goes through &lt;code&gt;item_list&lt;/code&gt; with signed parameters. The DIY options are running a headless browser (Playwright) and capturing the XHR responses while scrolling, or reverse-engineering the signer. Both work, both are slow or fragile, and both break when TikTok ships a new bundle.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Region matters.&lt;/strong&gt; The same handle can return different counts or videos depending on the exit country. Pin your proxy region if you compare numbers over time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rate limits are soft.&lt;/strong&gt; You won't get a clean 429. Instead, responses quietly degrade to empty blobs. Track the empty-response rate as a health metric.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Respect the data.&lt;/strong&gt; Only collect public data, store what you actually need, and check TikTok's terms and your local privacy rules (GDPR applies to creator data in the EU).&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;when-diy-stops-being-worth-it&quot;&gt;When DIY stops being worth it&lt;/h3&gt;
&lt;p&gt;If you only need follower counts for a few dozen accounts, the script above, a cheap VPS, and maybe a proxy are enough. Once you need full video history with views, likes and comments, the signing problem eats your time.&lt;/p&gt;
&lt;p&gt;That's the point where I switch to an API. For example, &lt;a rel=&quot;nofollow&quot; href=&quot;https://scrapeatlas.com&quot;&gt;ScrapeAtlas*&lt;/a&gt; has a &lt;code&gt;/v1/tiktok/profile/videos&lt;/code&gt; endpoint that takes a handle, a sort order (&lt;code&gt;latest&lt;/code&gt;, &lt;code&gt;popular&lt;/code&gt;, &lt;code&gt;oldest&lt;/code&gt;), an optional &lt;code&gt;since&lt;/code&gt; date, and a &lt;code&gt;max_cursor&lt;/code&gt; for pagination. It returns the same &lt;code&gt;itemList&lt;/code&gt; shape TikTok uses internally:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import os
import requests

API = &quot;https://api.scrapeatlas.com/v1/tiktok/profile/videos&quot;
HEADERS = {&quot;x-api-key&quot;: os.environ[&quot;SCRAPEATLAS_API_KEY&quot;]}

def all_videos(handle, since=None):
    params = {&quot;handle&quot;: handle, &quot;sort_by&quot;: &quot;latest&quot;}
    if since:
        params[&quot;since&quot;] = since          # ISO 8601 or Unix seconds
    while True:
        r = requests.get(API, headers=HEADERS, params=params, timeout=60)
        r.raise_for_status()
        page = r.json()
        for v in page.get(&quot;itemList&quot;, []):
            s = v.get(&quot;stats&quot;, {})
            yield {
                &quot;id&quot;: v[&quot;id&quot;],
                &quot;created&quot;: v[&quot;createTime&quot;],
                &quot;desc&quot;: v.get(&quot;desc&quot;, &quot;&quot;),
                &quot;views&quot;: s.get(&quot;playCount&quot;),
                &quot;likes&quot;: s.get(&quot;diggCount&quot;),
                &quot;comments&quot;: s.get(&quot;commentCount&quot;),
                &quot;shares&quot;: s.get(&quot;shareCount&quot;),
            }
        if not page.get(&quot;hasMore&quot;):
            break
        params[&quot;max_cursor&quot;] = page[&quot;cursor&quot;]

for v in all_videos(&quot;nasa&quot;, since=&quot;2026-09-01&quot;):
    print(v)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since the fields match TikTok's own &lt;code&gt;stats&lt;/code&gt; object, the SQLite schema above only needs a &lt;code&gt;video_snapshots&lt;/code&gt; table next to &lt;code&gt;profile_snapshots&lt;/code&gt;, and the rest of your pipeline stays the same.&lt;/p&gt;
&lt;h3 id=&quot;wrap-up&quot;&gt;Wrap-up&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Profiles: &lt;code&gt;requests&lt;/code&gt; plus the &lt;code&gt;__UNIVERSAL_DATA_FOR_REHYDRATION__&lt;/code&gt; blob, keyed on &lt;code&gt;id&lt;/code&gt;/&lt;code&gt;secUid&lt;/code&gt;, with &lt;code&gt;statusCode&lt;/code&gt; checks.&lt;/li&gt;
&lt;li&gt;Store hourly snapshots in SQLite and compute growth with SQL.&lt;/li&gt;
&lt;li&gt;Expect IP blocking, layout changes and signed video endpoints. Decide early whether you'll maintain a browser or signer, or hand that part off.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you've found a more stable way to get the video list, let me know in the comments.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;*I build it.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30800/tiktok-scraper-in-python-profiles-videos-and-the-walls-youll-hit-2026</guid>
<pubDate>Mon, 12 Oct 2026 10:59:19 +0000</pubDate>
</item>
<item>
<title>tiktok pixel tracking</title>
<link>https://coderlegion.com/29718/tiktok-pixel-tracking</link>
<description>&lt;p&gt;Here is a text-only summary and technical breakdown designed for a post on Coder Legion.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;enterprise-grade-adobe-tags-extension-for-tiktok-pixel-amp-server-side-events-api&quot;&gt;Enterprise-Grade Adobe Tags Extension for TikTok Pixel &amp;amp; Server-Side Events API&lt;/h4&gt;
&lt;p&gt;Modern digital analytics and ad tracking face massive conversion data loss due to ad-blockers, browser privacy features like Safari ITP, and network instability. Relying solely on standard client-side tracking tags leads to incomplete dataset reporting, broken attribution models, and inefficient marketing spend.&lt;/p&gt;
&lt;p&gt;To solve this challenge, we built an open-source extension for Adobe Experience Platform (AEP) Tags (formerly Launch) that merges client-side pixel tracking with server-side conversion delivery.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;key-architectural-pillars&quot;&gt;Key Architectural Pillars&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Dual-Track Execution&lt;/strong&gt;: Captures events directly in the browser via TikTok's client-side library while simultaneously firing a parallel server-side payload to TikTok's Business Events API endpoint using asynchronous network calls.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cross-Channel Event Deduplication&lt;/strong&gt;: Every event invocation automatically resolves or generates a shared RFC 4122 v4 UUID event identifier. This matching identifier is attached to both the client-side call and the server-side payload, allowing TikTok’s processing engine to merge redundant hits seamlessly and avoid double-counting conversions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;In-Browser PII Normalization &amp;amp; Hashing&lt;/strong&gt;: Privacy regulations and API specifications require that personally identifiable information (PII) such as email addresses, phone numbers, and external user IDs are hashed before transmission. The extension normalizes user input (stripping whitespace and forcing lowercase) and executes SHA-256 hashing directly inside the client engine using the browser's native Web Crypto API, backed by an optimized JavaScript fallback for legacy environments.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Single Page Application (SPA) Resilience&lt;/strong&gt;: Modern JavaScript frameworks do not reload the page on route changes. The extension monkey-patches the browser's native History API routes (pushState and replaceState) and attaches popstate listeners to automatically capture virtual pageview events without requiring manual custom code triggers for every page navigation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fault-Tolerant Execution Pipeline&lt;/strong&gt;: Unhandled tracking exceptions should never halt other analytics or tag management rule executions. All client-side calls are wrapped in robust error handling, script loading is bound to a strict 5-second timeout, and server-side API requests enforce a 10-second request timeout to guarantee non-blocking promise resolution.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h4 id=&quot;project-package-structure-overview&quot;&gt;Project Package Structure Overview&lt;/h4&gt;
&lt;p&gt;The extension is structured according to the official Adobe Extension Development Kit (SDK) specifications:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Manifest Configuration&lt;/strong&gt;: Maps all underlying JavaScript modules, action definitions, and user interface views for global configuration and rule building.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Core Action Libraries&lt;/strong&gt;: Separate execution files handling script injection, standard event processing, custom event parsing, user identification calls, and manual SPA pageview triggers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data Element Resolvers&lt;/strong&gt;: Individual interface files allowing tag managers to map values dynamically from their existing Adobe data layer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Utility Modules&lt;/strong&gt;: Lightweight functions handling client-side SHA-256 hashing, UUID v4 generation, SPA URL listeners, and console/badge debugging helpers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User Interface Views&lt;/strong&gt;: HTML and JavaScript extension views leveraging Adobe's Extension Bridge script to capture configuration settings and provide data element pickers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automated Test Suites&lt;/strong&gt;: Comprehensive unit testing using Jest to validate data resolution, PII hashing correctness, API payload formatting, and timeout fallbacks.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;how-you-can-contribute-amp-support&quot;&gt;How You Can Contribute &amp;amp; Support&lt;/h4&gt;
&lt;p&gt;Maintaining enterprise tracking software requires continuous updates to support changing privacy standards, browser updates, and API version upgrades.&lt;/p&gt;
&lt;p&gt;We are inviting developers, martech engineers, and analytics specialists to participate in the project. You can help by testing the codebase in your tag management sandboxes, contributing bug fixes or feature requests on GitHub, or supporting the project through GitHub Sponsors to help fund our upcoming Adobe Exchange Marketplace certification and server-side Edge Network implementation.&lt;/p&gt;
</description>
<category>Launches</category>
<guid isPermaLink="true">https://coderlegion.com/29718/tiktok-pixel-tracking</guid>
<pubDate>Sun, 04 Oct 2026 11:25:10 +0000</pubDate>
</item>
</channel>
</rss>