TikTok Competitor Analysis: Build a Weekly Content Report
Web Data Collection Specialist
TL;DR:
- TikTok competitor analysis begins with a fixed account set. Record which public brand accounts belong in the report before collecting any posts.
- Comparable samples matter more than unlabeled totals. Use the same requested count and collection window for each account, then disclose returned item counts.
- Content categories need reviewable rules. Store matched phrases and hashtags beside each theme label so a reviewer can correct the classification.
- Medians reduce the influence of one viral post. Report distributions and observed sample cadence alongside totals.
- A weekly report describes observed samples. Without verified full-period coverage, it cannot claim an account's exact overall posting frequency.
- Free to start. New Scrapeless accounts include free credit; create an account in the Scrapeless Dashboard.
Introduction: competitor reports fail at the sampling step
Public brand feeds are easy to compare badly. One account may contribute 30 observed posts, another 8, and a single outlier can dominate average engagement. A polished chart does not repair that mismatch.
This workflow builds a weekly TikTok competitor content report from selected public accounts. It collects bounded post samples, labels content with simple reviewable rules, summarizes engagement with medians and ranges, and makes collection scope visible in the final report.
Use the TikTok influencer analysis guide when the account set is made of creators rather than brand competitors.
Pipeline at a Glance
A practical TikTok competitor analysis pipeline has five stages:
- Define the competitor roster and reporting period.
- Resolve each public username to
sec_uid. - Collect the same bounded post sample for every account.
- Classify descriptions and hashtags with transparent rules.
- Aggregate the observed sample into a weekly report.
The pipeline stores raw responses and normalized rows before calculating any summary. That separation keeps a reporting change from forcing another collection run.
Prerequisites
- A Scrapeless account and API token from the Scrapeless Dashboard
- A reviewed list of public brand usernames
- A fixed requested post count and a collection timestamp
- A small category dictionary that a human can inspect
- A live token exported as
SCRAPELESS_API_KEYfor the code below
The assembled example is marked as a prerequisite gap because it needs the reader's API credential and selected accounts. The transformation logic is included, but the article does not contain private credentials or fabricated actor output.
Stage 1: Define a Comparable Competitor Set
A competitor roster is part of the report specification. Give every account a stable internal label, a public username, the reason it was included, and the date the roster was approved. Avoid silently adding a large publisher or a personal creator to a set of similarly sized brand accounts because its content volume will distort the comparison.
A compact roster can look like this:
| account_label | username | inclusion_reason |
|---|---|---|
| Brand A | brand_a |
Direct category competitor |
| Brand B | brand_b |
Same audience and price tier |
| Brand C | brand_c |
Adjacent category benchmark |
Keep the roster user-supplied. The actors used here do not discover a complete competitive market or prove that two accounts compete commercially.
Stage 2: Collect the Same Bounded Post Sample
The collection stage resolves every username through scraper.tiktok.user.detail, then supplies the returned sec_uid to scraper.tiktok.user.work. The post request accepts cursor and count; this workflow starts at the documented cursor "0" with the same requested count for every account.
Store the requested count and returned count separately. If one account returns fewer usable items, do not pad its metrics with zero-value rows. Disclose the difference in the weekly report.
The post response can include descriptions, hashtags, creation time, public play and engagement counts, URLs, pinned status, language, music, subtitles, and media fields. Optional values may be empty. A missing value should remain missing until the source semantics justify another interpretation.
Start Scraping with Scrapeless
Power up your web scraping and automation workflow with Scrapeless!
Sign up today and get $5 in free credit — no credit card required.Claim your free credit now in the Scrapeless Dashboard.
Stage 3: Classify Content With Reviewable Rules
Content classification should preserve the evidence behind every label. A simple rules table is often enough for a weekly operating report:
| Theme | Description phrases | Hashtags |
|---|---|---|
| Product education | how to, tips, explained |
tutorial, howto |
| Launch | new, introducing, available now |
launch, newproduct |
| Promotion | sale, discount, limited offer |
sale, deal |
| Community | duet, challenge, your turn |
challenge, community |
These labels are editorial rules, not actor-provided facts. Save the matched term and allow uncategorized when no rule fits. A reviewer can then distinguish a classification error from a collection error.
Rule-based labels also expose overlap. A post may discuss a product launch and a promotion at the same time. Either allow multiple labels or define a documented priority order; do not let dictionary order decide invisibly.
Stage 4: Summarize Engagement and Observed Cadence
Public engagement counts are cumulative observations captured at collection time. Compare them within the declared sample rather than treating them as a complete measure of brand performance.
Useful per-account fields include:
- Returned post count
- Earliest and latest creation time in the observed sample
- Median play, like, comment, and share counts
- Minimum and maximum values to show spread
- Theme share within the classified sample
- Pinned-post count in the sample
- Observed gaps between adjacent sampled post dates
Python's statistics module documentation defines the median operation used in the implementation. A median gives the middle observed value and limits the influence of one unusually large post, but it does not eliminate sampling bias.
Posting cadence needs careful wording. If the collected sample does not prove complete coverage of the reporting period, report “observed posts in the collected sample” and the date span between its earliest and latest items. Do not present that number as the account's exact weekly posting frequency.
Stage 5: Assemble the Python Pipeline
The script below collects a fixed initial sample for each supplied username, assigns one or more content themes, and writes both post-level rows and an account-level summary. Python's CSV module documentation covers the export format, and the NIST Privacy Framework provides a useful structure for access, retention, and data-minimization decisions.
Note: The code below requires a live Scrapeless API token in
SCRAPELESS_API_KEYand real public usernames supplied by the reader.
python
import csv
import json
import os
from datetime import datetime, timezone
from statistics import median
from urllib.request import Request, urlopen
ENDPOINT = "https://api.scrapeless.com/api/v1/scraper/request"
TOKEN = os.environ["SCRAPELESS_API_KEY"]
ACCOUNTS = [name.strip().lstrip("@") for name in os.environ[
"TIKTOK_COMPETITOR_USERNAMES"
].split(",") if name.strip()]
REQUESTED_COUNT = 20
THEMES = {
"product_education": {"how to", "tips", "explained", "tutorial", "howto"},
"launch": {"new", "introducing", "available now", "launch", "newproduct"},
"promotion": {"sale", "discount", "limited offer", "deal"},
"community": {"duet", "challenge", "your turn", "community"},
}
def run_actor(actor, actor_input):
data = json.dumps({"actor": actor, "input": actor_input}).encode()
request = Request(
ENDPOINT,
data=data,
headers={"x-api-token": TOKEN, "content-type": "application/json"},
method="POST",
)
with urlopen(request, timeout=60) as response:
return json.load(response)
def labels_for(item):
hashtag_values = []
for tag in item.get("hashtags") or []:
hashtag_values.append(
str(tag.get("name", "")) if isinstance(tag, dict) else str(tag)
)
searchable = " ".join([
str(item.get("description") or ""),
" ".join(hashtag_values),
]).lower()
labels = [
theme for theme, terms in THEMES.items()
if any(term in searchable for term in terms)
]
return labels or ["uncategorized"]
def metric(item, name):
value = item.get(name)
return value if isinstance(value, (int, float)) else None
collected_at = datetime.now(timezone.utc).isoformat()
rows = []
for username in ACCOUNTS:
profile = run_actor("scraper.tiktok.user.detail", {"unique_id": username})
response = run_actor(
"scraper.tiktok.user.work",
{"sec_uid": profile["sec_uid"], "cursor": "0", "count": REQUESTED_COUNT},
)
with open(f"{username}-posts-raw.json", "w", encoding="utf-8") as raw_file:
json.dump(response, raw_file, ensure_ascii=False, indent=2)
for item in response.get("items") or []:
rows.append({
"account": username,
"collected_at": collected_at,
"requested_count": REQUESTED_COUNT,
"post_id": str(item.get("id") or item.get("post_id") or ""),
"post_url": item.get("url") or item.get("post_url") or "",
"created_at": item.get("create_time") or item.get("date") or "",
"themes": "|".join(labels_for(item)),
"play_count": metric(item, "play_count"),
"like_count": metric(item, "like_count"),
"comment_count": metric(item, "comment_count"),
"share_count": metric(item, "share_count"),
"is_pinned": item.get("is_pinned"),
})
with open("competitor-posts.csv", "w", newline="", encoding="utf-8") as output:
writer = csv.DictWriter(output, fieldnames=rows[0].keys() if rows else ["account"])
writer.writeheader()
writer.writerows(rows)
with open("competitor-weekly-summary.csv", "w", newline="", encoding="utf-8") as output:
fields = ["account", "observed_posts", "median_plays", "median_likes"]
writer = csv.DictWriter(output, fieldnames=fields)
writer.writeheader()
for username in ACCOUNTS:
account_rows = [row for row in rows if row["account"] == username]
plays = [row["play_count"] for row in account_rows if row["play_count"] is not None]
likes = [row["like_count"] for row in account_rows if row["like_count"] is not None]
writer.writerow({
"account": username,
"observed_posts": len(account_rows),
"median_plays": median(plays) if plays else "",
"median_likes": median(likes) if likes else "",
})
The summary intentionally says observed_posts. Extend it with theme counts, value ranges, and sampled date spans after confirming the exact timestamp representation in the live response.
Weekly Competitor Content Report Template
A useful report can fit on one page before the appendix:
| Section | Include | Scope note |
|---|---|---|
| Collection summary | Account roster, collection time, requested and returned counts | State that results cover collected samples |
| Content mix | Theme counts and shares, uncategorized rate | Attach the current rules dictionary |
| Engagement | Median and range by metric and account | Public cumulative counts at collection time |
| Cadence | Observed post dates and gaps | Do not claim full-period frequency without coverage |
| Notable posts | Post URL, theme, metric context, reviewer note | Separate observations from interpretation |
| Actions | Questions to test in the next report | Avoid copying protected creative assets |
Place post-level rows in an appendix or linked CSV. The main report should let a reader trace every conclusion to a sampled account, post URL, collection time, and classification rule.
Handle Public Account Data Responsibly
A public profile can still contain personal data. Limit the account set to the reporting purpose, restrict access to raw responses, define a deletion schedule, and avoid inferring sensitive traits from captions, language, or visual content.
Competitor analysis should describe observable content patterns. It should not republish media, imitate protected creative work, or assign hidden intent to a brand or creator. Keep human review in the loop for labels that could affect commercial decisions.
Scrapeless provides the collection actors through Scraping API. Check the current pricing page when translating the requested account and sample counts into a production plan.
Conclusion: make the comparison auditable
TikTok competitor analysis becomes useful when every number carries its scope. Fix the account roster, request comparable samples, preserve raw responses, expose classification rules, and report medians with returned counts. The result is a weekly content report that supports review without pretending the sample is a complete view of every account.
Ready to Build a Weekly Competitor Report?
Join the Scrapeless Discord or Telegram community to discuss collection and reporting patterns. Create an account in the Scrapeless Dashboard when the roster and sample policy are ready.
FAQ
Q: What is TikTok competitor analysis?
TikTok competitor analysis compares observable public content and engagement patterns across a user-defined set of accounts. A defensible report also states its collection time, sample size, and classification rules.
Q: Which metrics belong in a weekly competitor report?
A weekly competitor report can include observed post count, sampled date span, content-theme mix, median public engagement counts, ranges, and links to notable posts. Each metric should be labeled as a sample observation.
Q: Can the sample show exact posting frequency?
The sample shows exact posting frequency only when collection has verified complete coverage of the stated period. Otherwise, report observed dates and gaps within the collected sample.
Q: Why use median engagement instead of average engagement?
Median engagement reduces the influence of one unusually large observed value. It still depends on sample quality, so publish the sample count and range beside it.
Q: Does the workflow need a browser parser or proxy setup?
The API returns the supported public account and post fields for the supplied identifiers. The reporting pipeline still defines accounts, sample limits, storage, classification, and review.
Q: Is collecting public competitor content legal?
Legality depends on the jurisdiction, purpose, data, access method, and applicable terms. Collect only necessary public fields, respect intellectual property, limit retention, and obtain legal advice for the specific use.
At Scrapeless, we only access publicly available data while strictly complying with applicable laws, regulations, and website privacy policies. The content in this blog is for demonstration purposes only and does not involve any illegal or infringing activities. We make no guarantees and disclaim all liability for the use of information from this blog or third-party links. Before engaging in any scraping activities, consult your legal advisor and review the target website's terms of service or obtain the necessary permissions.



