构建一个可重复分析的 TikTok 数据管道
Senior Web Scraping Engineer
TL;DR:
- TikTok 数据管道应在标准化之前保留原始响应。 源有效负载使解析器更改和字段漂移可审查。
- 数据采集运行需要自己的状态表。 失败的请求是覆盖缺口,而不是空的档案、帖子列表或产品。
- TikTok 标识符应放在文本列中。 字符串存储可以防止长 ID 被数值转换所更改。
- 快照表描述随时间变化的观察。 它们不应覆盖之前的创作者、帖子或商店值。
- 历史覆盖取决于收集的页面和验证的续集数据。 第一页面的响应并不是完整的账户历史。
- 免费开始。 新的 Scrapeless 账户通过 Scrapeless 控制面板包含免费信用。
引言:分析始于收集证据
仪表板只能解释到达其数据库的记录。没有原始有效负载、运行状态和收集时间戳,空图表无法区分没有活动、失败的收集或解析器更改。
本指南从 Scrapeless TikTok 演员到 SQLite 构建一个紧凑的 TikTok 数据管道。该设计保留原始 JSON,标准化创作者、帖子和商店快照,进行数据质量检查,并为分析暴露小型 SQL 查询。调度、生产数据库操作和商业智能交付仍然是应用程序的职责。
TikTok 演员指南 记录了收集器使用的演员映射。
管道概览
| 阶段 | 动作 | 输出 |
|---|---|---|
| 收集 | 调用档案、帖子和可选商店演员 | API 响应 |
| 保存 | 以演员和运行元数据存储原始 JSON | 不可变源层 |
| 标准化 | 解析 ID、计数器、时间戳和维度 | 快照表 |
| 验证 | 检查键、类型、空值和运行覆盖 | 数据质量结果 |
| 查询 | 汇总更改和当前状态 | 分析视图 |
该管道记录每个请求返回的内容。除非收集并验证所有必需的页面和续集值,否则不声称拥有完整的 TikTok 历史。
先决条件
- 来自 Scrapeless 控制面板 的 Scrapeless 账户和 API 密钥
- 配有标准库和 SQLite 的 Python 3
- 一个公开的 TikTok 用户名用于档案和帖子收集
- 可选的 TikTok 商店产品 ID 和地区用于产品快照
- 存储位置、保留政策和收集计划
SCRAPELESS_API_KEY和TIKTOK_USERNAME用于收集器;商店环境变量是可选的
端到端代码是一个先决条件缺口,因为实时 Scrapeless 凭证和目标标识符来自读取者。演员名称、输入字段和标准化列遵循所提供的接口文档,而不呈现捏造的输出。
阶段 1:为每个收集尝试赋予身份
为逻辑收集创建一个 run_id,每个演员请求一行。存储演员名称、请求输入、收集时间、状态及任何错误详细信息。原始有效负载表应引用相同的运行并记录解析器版本。
HTTP 响应处理应区分成功的应用程序响应和失败。 HTTP 语义规范 定义了客户端和服务器使用的状态代码框架。
覆盖表可以回答三个基本问题:
- 请求了哪些实体?
- 哪些请求产生了可用的有效负载?
- 哪些标准化表从每个有效负载接收了行?
不要将失败的帖子请求表示为空 items 数组。这些状态具有不同的分析含义。
阶段 2:在解析之前保留原始 JSON
原始响应是 TikTok API 到数据库工作流的审计层。将演员名称、请求输入、收集时间、响应 JSON 和解析器版本一起存储。Python 的 JSON 文档 定义了在示例中使用的序列化接口。
原始存储有三个实际用途:
- 在模式更改后,可以重新运行解析器。
- 可疑的标准化值可以追溯到其源字段。
- 新字段可以从保留的有效负载中回填,而无需重复收集。
在写入请求元数据之前,删除秘密。API 密钥应在处理配置中,并且绝不应进入原始有效负载或运行表。
开始使用 Scrapeless 抓取
用 Scrapeless 提升您的网络抓取和自动化工作流!
今天注册并获取 5 美元的免费信用 — 无需信用卡。
立即在 Scrapeless Dashboard 中领取您的免费积分
阶段 3:标准化创作者、帖子和产品快照
将自然标识符作为文本保留,并在每个快照键中包含 collected_at。创作者个人资料可以更改,帖子计数器可以增加,商店产品的价格、库存、评级或评论数量可以发生变化。
标准化层可以从以下表格开始:
| 表格 | 实体键 | 快照字段 |
|---|---|---|
profile_snapshots |
账户 ID + 采集时间 | 用户名、粉丝、点赞、视频 |
post_snapshots |
帖子 ID + 采集时间 | 创作者 ID、时长、播放次数、点赞数、评论数、分享数 |
product_snapshots |
产品 ID + 地区 + 采集时间 | 名称、价格、货币、库存、评级、评论数量 |
post_hashtags |
帖子 ID + 采集时间 + 话题标签 | 标准化标签 |
SQLite的 CREATE TABLE 文档 描述了该模型背后的主键和类型约束。
注意:下面的代码需要实时的
SCRAPELESS_API_KEY和TIKTOK_USERNAME值。TIKTOK_SHOP_PRODUCT_ID和TIKTOK_SHOP_REGION是可选的,启用商店分支。
python
import json
import os
import sqlite3
import uuid
from datetime import datetime, timezone
from urllib.request import Request, urlopen
ENDPOINT = "https://api.scrapeless.com/api/v1/scraper/request"
PARSER_VERSION = "tiktok-v1"
def utc_now():
return datetime.now(timezone.utc).isoformat()
def request_actor(actor, actor_input):
body = json.dumps({"actor": actor, "input": actor_input}).encode()
request = Request(
ENDPOINT,
data=body,
headers={
"content-type": "application/json",
"x-api-token": os.environ["SCRAPELESS_API_KEY"],
},
method="POST",
)
with urlopen(request, timeout=60) as response:
return json.load(response)
def integer(value):
try:
return int(value)
except (TypeError, ValueError):
return None
database = sqlite3.connect("tiktok-analytics.sqlite3")
database.executescript("""
CREATE TABLE IF NOT EXISTS collection_runs (
run_id TEXT NOT NULL, actor TEXT NOT NULL, collected_at TEXT NOT NULL,
request_json TEXT NOT NULL, status TEXT NOT NULL, detail TEXT,
PRIMARY KEY (run_id, actor)
);
CREATE TABLE IF NOT EXISTS raw_payloads (
run_id TEXT NOT NULL, actor TEXT NOT NULL, parser_version TEXT NOT NULL,
payload_json TEXT NOT NULL, PRIMARY KEY (run_id, actor)
);
CREATE TABLE IF NOT EXISTS profile_snapshots (
collected_at TEXT NOT NULL, account_id TEXT NOT NULL, unique_id TEXT,
followers INTEGER, likes INTEGER, videos INTEGER,
PRIMARY KEY (collected_at, account_id)
);
CREATE TABLE IF NOT EXISTS post_snapshots (
collected_at TEXT NOT NULL, post_id TEXT NOT NULL, account_id TEXT NOT NULL,
video_duration INTEGER, play_count INTEGER, like_count INTEGER,
comment_count INTEGER, share_count INTEGER,
PRIMARY KEY (collected_at, post_id)
);
CREATE TABLE IF NOT EXISTS post_hashtags (
collected_at TEXT NOT NULL, post_id TEXT NOT NULL, hashtag TEXT NOT NULL,
PRIMARY KEY (collected_at, post_id, hashtag)
);
CREATE TABLE IF NOT EXISTS product_snapshots (
collected_at TEXT NOT NULL, product_id TEXT NOT NULL, region TEXT NOT NULL,
name TEXT, sale_price TEXT, currency TEXT, available_quantity INTEGER,
rating TEXT, review_count INTEGER,
PRIMARY KEY (collected_at, product_id, region)
);
""")
run_id = str(uuid.uuid4())
collected_at = utc_now()
def collect(actor, actor_input):
request_json = json.dumps(actor_input, sort_keys=True)
try:
payload = request_actor(actor, actor_input)
database.execute(
"INSERT INTO raw_payloads VALUES (?, ?, ?, ?)",
(run_id, actor, PARSER_VERSION, json.dumps(payload, ensure_ascii=False)),
)
status, detail = "success", None
except Exception as error:
payload = None
status, detail = "failed", f"{type(error).__name__}: {error}"
database.execute(
"INSERT INTO collection_runs VALUES (?, ?, ?, ?, ?, ?)",
(run_id, actor, collected_at, request_json, status, detail),
)
return payload
profile = collect(
"scraper.tiktok.user.detail",
{"unique_id": os.environ["TIKTOK_USERNAME"]},
)
if profile:
stats = profile.get("statistics") or {}
account_id = str(profile.get("account_id") or "")
database.execute(
"INSERT INTO profile_snapshots VALUES (?, ?, ?, ?, ?, ?)",
(
collected_at, account_id, profile.get("unique_id"),
integer(stats.get("followers")), integer(stats.get("likes")),
integer(stats.get("videos")),
),
)
posts = collect(
"scraper.tiktok.user.work",
{"sec_uid": profile["sec_uid"], "cursor": "0", "count": 10},
)
if posts:
for post in posts.get("items") or []:
post_id = str(post.get("post_id") or post.get("video_id") or "")
database.execute(
"INSERT INTO post_snapshots VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
(
collected_at, post_id, account_id,
integer(post.get("video_duration")),
integer(post.get("play_count")), integer(post.get("like_count")),
integer(post.get("comment_count")), integer(post.get("share_count")),
),
)
for hashtag in set(post.get("hashtags") or []):
normalized = str(hashtag).strip().removeprefix("#").casefold()
if normalized:
database.execute(
"INSERT INTO post_hashtags VALUES (?, ?, ?)",
(collected_at, post_id, normalized),
)
product_id = os.getenv("TIKTOK_SHOP_PRODUCT_ID")
product_region = os.getenv("TIKTOK_SHOP_REGION")
if product_id and product_region:
product = collect(
"scraper.tiktok.shop.page",
{"product_id": product_id, "region": product_region},
)
if product:
price = product.get("price") or {}
stock = product.get("stock") or {}
database.execute(
"INSERT INTO product_snapshots VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
(
collected_at, str(product.get("product_id") or product_id),
str(product.get("region") or product_region).casefold(),
product.get("name"), price.get("sale_price"),
price.get("currency"), integer(stock.get("available_quantity")),
str(product.get("rating")) if product.get("rating") is not None else None,
integer(product.get("review_count")),
),
)
database.commit()
database.close()
该示例仅收集第一个请求的帖子页面。它不承诺完整的账户历史,因为没有假设在已验证响应之外存在继续字段。
阶段 4:在分析之前添加数据质量检查
质量检查应在采集和标准化层运行。至少标记:
collection_runs中失败的参与者请求- 产生了没有预期实体行的成功原始有效负载
- 为空的账户、帖子或产品 ID
- 负计数
- 在持续时间分析中缺少或为零的视频持续时间
- 缺少地区或货币上下文的产品行
- 针对一个采集时间戳的重复实体键
将检查结果保留为可查询的结果,而不是仅限控制台的消息。仪表板可以在其影响的指标旁显示覆盖差距。
阶段 5:查询快照而不删除时间
窗口函数将每个实体与其先前观察进行比较。SQLite的 窗口函数文档 为此用例定义了 LAG()。
sql
-- Illustrative follower-change query over the normalized schema.
SELECT
account_id,
collected_at,
followers,
followers - LAG(followers) OVER (
PARTITION BY account_id ORDER BY collected_at
) AS follower_change
FROM profile_snapshots;
-- Illustrative run-coverage query.
SELECT actor, status, COUNT(*) AS run_count
FROM collection_runs
GROUP BY actor, status
ORDER BY actor, status;
使用当前状态视图用于仪表板,同时保持底层表格为附加仅。相同的模式支持帖子计数器更改、话题标签报告、持续时间区间比较、产品价格变动、库存事件和评级监控。
Scrapeless通过 Scraping API 提供 TikTok 参与者。在选择实体覆盖和采集频率之前,请查看 当前定价页面。
负责任地处理 TikTok 数据
收集定义分析目的所需的公共字段,限制对原始有效负载的访问,并设置创作者级数据的保留限制。 NIST 隐私框架 为隐私风险治理和数据最小化提供一般指导。
将操作配置保留在与分析师共享的数据库行之外。 API 密钥、内部警报路由和访问凭证应保留在由应用程序环境控制的秘密系统中。
结论:使覆盖在每个指标旁可见
可靠的 TikTok 数据管道保持原始 JSON、运行状态、解析器版本、采集时间和通过稳定 ID 连接的标准化快照。该结构使分析师能够区分真实的零活动和缺失的采集,在字段更改后重新运行解析器,并追踪每个指标到一个观察。生产调度、存储扩展和 BI 交付可以围绕相同的证据模型发展。
准备构建 TikTok 分析管道了吗?
加入 Scrapeless Discord 或 Telegram 社区,讨论快照和仓库架构。在第一个实体列表准备好时,在 Scrapeless Dashboard 中创建一个账户。
常见问题
问:TikTok API 应如何连接到数据库?
TikTok API 应通过一个收集器连接到数据库,该收集器记录运行状态、保留原始 JSON、验证字段并插入带时间戳的实体快照。
问:为什么存储原始 TikTok 响应?
原始 TikTok 响应使团队可以追踪标准化值、审查架构更改,并对保留的源数据重新运行解析器。
问:TikTok ID应该使用整数列吗?
TikTok ID应该使用文本列,因为标识符是模糊的字符串,不应通过数字转换进行更改。
问:第一次发布请求包含完整的账户历史吗?
第一次发布请求并不建立完整的账户历史。覆盖范围取决于收集的页面和验证的继续数据。
问:Scrapeless管理调度程序和数据仓库吗?
Scrapeless提供结构化的演员响应进行收集。调用者管理调度、数据库操作、转换、质量监控和商业智能交付。
问:抓取公共TikTok数据合法吗?
合法性取决于管辖权、目的、访问方法、适用条款和收集的字段。出于允许的目的使用公共数据,并为预期的流程寻求法律建议。
在Scrapeless,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。



