标签 tiktok 下的文章

Oracle 数据中心断电,引发 TikTok 大面积瘫痪

近日,短视频平台 TikTok 在美国出现了一次短暂的服务中断。值得玩味的是,这次中断的时间点,恰好卡在 TikTok 刚完成一项美国业务重组安排之后。

 

根据这份重组安排,由 Oracle 与一组美国本土投资者共同组建的新合资实体,将接管 TikTok 在美国的运营相关事务,并被称为 TikTok USDS。

 

TikTok USDS 承诺将用户数据通过 Oracle 公司拥有的数据中心进行传输。

 

刚重组完没几天,TikTok 就出现了大面积瘫痪,许多美国用户反映,他们无法上传视频到 TikTok,也无法观看大多数新视频,包括美国以外用户成功上传的新视频。

 

另一些用户表示,他们的算法似乎“重置”了,但目前尚不清楚这是否也与停电有关。

 

事情不断发酵,逼得 TikTok USDS 不得不出面回应了。

 

TikTok USDS 发言人 Jamie Favazza 在给 The Verge 的一封电子邮件中指出,该公司在其新创建的 X 账户上发布了一份声明,声明称,由于美国数据中心发生电力中断,影响了 TikTok 和我们运营的其他应用程序,公司一直在“努力恢复服务”。

既然问题出在了数据中心,数据中心当然也要出来回应。

Oracle 回应:完全怪天气

 

面对不断升温的质疑,Oracle 公司于当地时间 1 月 27 日通过电子邮件向媒体作出正式回应。

 

Oracle 发言人迈克尔·埃格伯特(Michael Egbert)表示,上周末美国遭遇的一场强烈冬季风暴,导致 Oracle 一处数据中心发生了暂时性停电,从而影响了 TikTok 在美国的服务。

 

“上周末,Oracle 数据中心因天气原因发生暂时性停电,影响了 TikTok 的服务。” 埃格伯特在声明中写道。他进一步解释称,美国 TikTok 用户在停电后所遇到的问题,主要源于恢复过程中出现的技术故障,目前 Oracle 正与 TikTok 合作,尽快修复相关问题。

 

这一回应明确否认了服务异常与内容审查之间存在直接关联,并将原因归结为基础设施层面的突发事故。Oracle 方面的说法,也与当时美国多地遭遇极端冬季天气的事实相吻合。

 

在随后的声明中,TikTok 指出,其工程团队正在持续推进恢复工作,并在 1 月 27 日表示,已在恢复美国系统方面取得“重大进展”,但仍提醒用户,某些技术问题可能在短期内持续存在,尤其是在发布新内容时。

作为此次事件中的关键基础设施提供方,Oracle 的角色也受到资本市场关注。

 

据 Benzinga Pro 报道,Oracle 公司股票在事件曝光当日收于 174.90 美元,下跌 4.13%,但在盘后交易中回升 1.16% 至 176.93 美元。Benzinga 的 Edge 股票排名显示,Oracle 股票在动量和价值维度上的评分均处于较低水平,反映出其在短期至长期内的价格趋势承压。

随着 TikTok USDS 合资企业逐步接管美国业务,其基础设施稳定性、内容审核机制以及与地方和联邦监管机构的互动方式,仍将持续受到审视。

网友:不只可以怪天气,还可以怪 AI

 

随着最终达成的合资协议,被外界普遍视为 TikTok 在美国“生死攸关”的一次妥协安排。

 

值得注意的是,此次技术中断发生之际,正值 TikTok 更新其美国隐私政策之后。新政策与合资架构调整相配套,但其中关于可能收集的数据类型的表述,引发部分用户不安。

 

市场情报公司 Sensor Tower 向 CNBC 提供的数据显示,在过去五天内,美国地区 TikTok 的每日应用删除量较此前三个月的平均水平增长了近 150%。

 

在 Reddit 上,一条关于 Oracle 数据中心与 TikTok 服务中断的帖子吸引了大量关注,不少网友在评论区提出了各类猜测、调侃与个人经验分享,这些反馈在一定程度上折射出技术社区对事件的怀疑态度,以及对 TikTok 内容机制与 Oracle 云服务能力的长期刻板印象。

 

一位 ID 名为transcriptoin_error的用户提出了一种“看似合理的推测”。

 

他认为,如果平台在系统中新增了内容过滤机制,那么在将相关流量迁移到新系统的过程中,确实有可能引发故障。他指出,在大规模系统迁移或数据转移时,出现配置错误或小规模失效并不罕见,尤其是在新旧系统并行、过滤规则叠加的情况下。

 

这条评论获得了数十次点赞,被不少用户视为“至少在工程逻辑上说得通”的一种解释,但评论者本人也并未声称这是事实,而是明确将其界定为推测。

 

在另一条高赞的长篇幅评论中,该用户进一步构建了一套完整但高度假设性的系统模型。

 

他设想,如果 TikTok 平台不愿直接改动现有代码,以避免引发更大规模的系统崩溃,那么新增的内容过滤功能很可能会被设计成一个独立服务,甚至可能基于人工智能模型运行。

 

在这种设想下,所有潜在“敏感内容”都会被发送至一个新的 AI 服务进行判断,只有在得到“允许发布”的反馈后,内容才会正常上线。

 

该用户进一步推测,如果这一 AI 服务发生宕机,而系统默认策略又是“未通过即阻止”,那么大量内容就可能被一并拦截,从而在用户侧表现为算法行为的“剧烈变化”。

 

这条评论虽然点赞不高,但在讨论中被多次引用,成为部分网友解释“为什么技术故障会影响内容分发”的逻辑模板。

 

也有网友对上述推测持明显怀疑态度。

 

一位在评论区拥有较高影响力的用户指出,至今仍然没有人能够清楚解释,为什么一次服务器层面的故障,会导致推荐算法或内容分发逻辑出现如此明显的变化。在他看来,如果问题仅限于数据中心断电或服务恢复过程中的技术瑕疵,那么算法层面的“性格突变”仍然缺乏合理解释

 

除了针对 TikTok 的讨论,Oracle 本身也成为 Reddit 用户情绪的集中投射对象。

 

一位用户直言,

 

“能力并非问题所在,科技圈里没人能忍受 Oracle。”

 

他还引用了一句在技术圈流传已久的说法:“Oracle 没有客户,只有囚犯。”这类评论并未直接指向此次事件的具体责任,但反映出 Oracle 在开发者与工程师群体中的长期口碑问题。

参考链接:

https://economictimes.indiatimes.com/tech/technology/oracle-says-data-center-outage-causing-issues-faced-by-us-tiktok-users/articleshow/127667105.cms?from=mdr&utm_source=contentofinterest&utm_medium=text&utm_campaign=cppst

https://slate.com/technology/2026/01/tiktok-outage-oracle-ice-shooting.html

https://www.reddit.com/r/news/comments/1qpbtv5/oracle_says_data_center_outage_causing_issues/

TikTok的推荐算法对访问行为和账号历史极为敏感,跨地区运营、高频内容发布和多账号矩阵管理中,高频操作、设备切换和IP重复使用都会触发系统风控,导致账号限流或封禁。运营团队需要在策略和技术手段之间找到平衡点,以保证长期稳定的运营。
平台会通过访问模式、账号历史和IP行为综合判断异常操作。仅靠内容优化或短期操作难以达到安全与效率兼顾的效果。在这种环境下,住宅代理技术成为核心保障。

静态住宅IP的运营价值

静态住宅IP通过真实ISP网络提供固定IP,使账号操作表现如同自然用户,显著降低封号和限流风险。这类IP能够维持账号的登录状态,支持长期数据抓取和广告投放,同时减少系统误判概率。静态住宅IP具有高纯净度,为跨地区运营、多账号矩阵和长期策略提供技术保障。
在TikTok跨境运营中,住宅代理不仅支持不同地区的访问模拟,还能为内容测试和广告优化提供稳定数据来源。通过住宅IP维持连续、自然的访问行为,团队能够在保障账号安全的前提下,实现内容策略和数据采集的优化。

封号限流的根源分析

TikTok对异常访问高度敏感。数据中心IP易被识别,短期高频操作和相似访问模式会触发限制。多账号共享IP或相似访问路径会增加账号关联风险。住宅代理通过模拟自然访问行为,降低系统误判概率,为策略执行提供可靠保障。
通过住宅IP,团队可以建立连续的访问轨迹,实现跨账号矩阵的安全管理。这种方式不仅降低了封号风险,也保证了内容和广告策略在全球范围内的高效执行,为长期运营提供技术基础。

策略与住宅代理的协同

住宅代理不仅是安全工具,更是策略执行的基础。在跨境运营、广告投放和多账号矩阵管理中,高质量住宅IP提供可控的访问环境,使策略执行连续、数据可靠。代理服务支持灵活IP分配和高可用性,为长期运营提供稳固基础。通过住宅代理与策略协同,团队能够优化内容推荐效果,同时保证数据抓取和广告投放的精准性。

长期运营的技术保障

在长期运营中,策略的连续性和数据的可靠性至关重要。高质量住宅IP能够保持账号独立性和访问轨迹的自然性,为跨地区、多账号运营提供基础支撑。代理服务的高可用性和灵活策略,使团队能够在复杂环境下维持长期稳定运营,实现策略优化和业务目标的同步推进。

结语

无论是YouTube还是TikTok,高频、多账号和跨地区运营已经成为常态。内容优化、账号管理和策略执行必须与住宅代理结合,才能在安全、效率和可控性之间取得平衡。付费代理提供的高质量原生住宅IP不仅是访问工具,更是跨境社媒运营的战略基础设施。通过策略化使用住宅代理,团队可以降低封号和限流风险,保障账号长期稳定运行,并提升数据抓取和广告投放效率,实现跨境社媒运营的可持续发展。

背景

一开始是通过Api获取数据,但是最近他们增加X-Gnarly参数,而且在github上没有找有效的方案后,放弃api请求,改用页面爬取的方式。彻底避免参数加密校验。

我的环境

    python 3.11 
    selenium 4.39.0
    playwright 1.57.0

评论页面

实现啦抓取第一页和第二页的评论,你们要是抓更多页可以吧第二页改成循环。
执行脚本后会在当前目录生成一份json文件,里面是/api/comment/list/接口返回的数据。

 python3.11 comment_scraper.py "@mahi.islam.oliva/video/7565942090039954706"

image.png

代码如下:

import json
import time
import sys
import base64
import re,os
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import argparse



def merge_comments(first_page, second_page):
    """合并两页的评论数据"""
    merged_data = first_page.copy()
    if 'comments' in second_page:
        if 'comments' not in merged_data:
            merged_data['comments'] = []
        merged_data['comments'].extend(second_page['comments'])
    return merged_data

def extract_tiktok_filename(path: str) -> str:
    """
    从 TikTok 路径(如 '@username/video/123456')中提取 'username_123456'
    支持带或不带 @、带 URL 等情况
    """
    # 匹配模式:可选的 @ + 用户名(字母数字下划线.)+ /video/ + 数字ID
    match = re.search(r'@?([\w.]+)/video/(\d{16,})', path)
    if match:
        username = match.group(1)
        video_id = match.group(2)
        return f"{username}_{video_id}"
    else:
        # 如果格式不符,回退到清理后的通用方式
        safe = re.sub(r'[\\/:*?"<>|\s]+', '_', path.strip('@/'))
        return safe[:100]


class TiktokScraper:
    def __init__(self):
        self.comments_data = []
        self.setup_driver()


    def setup_driver(self):

        chrome_options = Options()
        chrome_options.set_capability("goog:loggingPrefs", {"performance": "ALL"})

        chrome_options.add_argument("--start-maximized")
        chrome_options.add_argument("--no-sandbox")
        chrome_options.add_argument("--headless=new")
        chrome_options.add_argument("--disable-dev-shm-usage")
        chrome_options.add_argument("--disable-blink-features=AutomationControlled")
        chrome_options.add_argument("--disable-infobars")
        chrome_options.add_argument("--disable-extensions")
        chrome_options.add_argument("--disable-gpu")  # 减少 WebGL 差异(可选)
        chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
        chrome_options.add_experimental_option('useAutomationExtension', False)

        user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"
        chrome_options.add_argument('user-agent={0}'.format(user_agent))

        self.driver = webdriver.Chrome(options=chrome_options)

        self.driver.execute_cdp_cmd("Emulation.setDeviceMetricsOverride", {
            "width": 1440,
            "height": 900,
            "deviceScaleFactor": 2,  # macOS Retina
            "mobile": False
        })

        # 覆盖 WebGL 参数(关键!)
        self.driver.execute_cdp_cmd("Emulation.setHardwareConcurrencyOverride", {"hardwareConcurrency": 8})
        # 1. 设置基础 UA(CDP 安全方式)
        self.driver.execute_cdp_cmd("Emulation.setUserAgentOverride", {
            "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
            "platform": "MacIntel"
        })

        # 2. 用 JS 覆盖高级指纹(包括 userAgentData)
        self.driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
            "source": """
#             delete navigator.__proto__.webdriver;

            Object.defineProperty(navigator, 'platform', { get: () => 'MacIntel' });
            Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });

            // 伪造 userAgentData
            if (!navigator.userAgentData) {
                Object.defineProperty(navigator, 'userAgentData', {
                    value: {
                        brands: [
                            { brand: "Chromium", version: "120" },
                            { brand: "Google Chrome", version: "120" },
                            { brand: "Not:A-Brand", version: "99" }
                        ],
                        mobile: false,
                        platform: "macOS",
                        getHighEntropyValues: async (hints) => ({
                            architecture: "x86_64",
                            model: "",
                            platform: "macOS",
                            platformVersion: "13.5",
                            uaFullVersion: "120.0.6099.0"
                        })
                    },
                    writable: false,
                    configurable: false
                });
            }
            """
        })

        # 覆盖 WebGL 渲染器(防指纹关键)
        self.driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
            "source": """
            const getParameter = WebGLRenderingContext.prototype.getParameter;
            WebGLRenderingContext.prototype.getParameter = function(param) {
                if (param === 37445) return 'Apple Inc.'; // UNMASKED_VENDOR_WEBGL
                if (param === 37446) return 'Apple GPU';   // UNMASKED_RENDERER_WEBGL
                return getParameter.call(this, param);
            };
            """
        })
        self.driver.execute_cdp_cmd("Emulation.setTimezoneOverride", {"timezoneId": "America/New_York"})
        self.driver.execute_cdp_cmd("Emulation.setLocaleOverride", {"locale": "en-US"})

    def extract_comment_response_from_logs(self):
        """从 performance 日志中提取评论 API 的完整响应"""
        try:
            logs = self.driver.get_log("performance")
        except Exception as e:
            print(f"获取日志失败: {e}")
            return None

        request_id_to_url = {}
        finished_request_ids = set()

        for entry in logs:
            try:
                message = json.loads(entry["message"])
                method = message.get("message", {}).get("method")
                params = message.get("message", {}).get("params", {})

                if method == "Network.responseReceived":
                    url = params.get("response", {}).get("url", "")
                    request_id = params.get("requestId")
                    if request_id and re.search(r'comment.*list|comments.*aweme', url, re.I):
                        request_id_to_url[request_id] = url

                elif method == "Network.loadingFinished":
                    request_id = params.get("requestId")
                    if request_id:
                        finished_request_ids.add(request_id)
            except Exception:
                continue

        for req_id, url in request_id_to_url.items():
            if req_id in finished_request_ids:
                try:
                    body = self.driver.execute_cdp_cmd(
                        "Network.getResponseBody",
                        {"requestId": req_id}
                    )
                    raw = body.get("body", "{}")
                    if body.get("base64Encoded"):
                        raw = base64.b64decode(raw).decode("utf-8")
                    data = json.loads(raw)
                    if isinstance(data, dict) and ("comments" in data or "item_comments" in data):
                        print(f"✅ 捕获评论接口: {url}")
                        return data
                except Exception as e:
                    print(f"获取响应体失败 (req_id={req_id}): {e}")

        return None

    def scroll_comment_section(self):
        """在 .TUXTabBar-content 内部查找并滚动真正的评论列表容器"""
        script = """
            const tabContent = document.querySelector('.TUXTabBar-content');
            if (!tabContent) {
                console.log('❌ .TUXTabBar-content not found');
                return false;
            }

            // 获取所有子 div
            const candidates = Array.from(tabContent.querySelectorAll('div'));

            // 按 DOM 层级深度排序(优先选深层级的,通常是列表)
            candidates.sort((a, b) => {
                let depthA = 0, depthB = 0;
                let p = a; while (p && p !== tabContent) { depthA++; p = p.parentElement; }
                p = b; while (p && p !== tabContent) { depthB++; p = p.parentElement; }
                return depthB - depthA; // 深的优先
            });

            for (const el of candidates) {
                const style = window.getComputedStyle(el);
                const overflowY = style.overflowY;
                // 必须满足:可滚动 + 有溢出内容
                if ((overflowY === 'auto' || overflowY === 'scroll') &&
                    el.scrollHeight > el.clientHeight) {
                    el.scrollTop = el.scrollHeight+100;
                    console.log('✅ Scrolled real comment container');
                    return true;
                }
            }

            console.log('⚠️ No scrollable child found in .TUXTabBar-content');
            return false;
        """
        try:
            result = self.driver.execute_script(script)
            return result is True
        except Exception as e:
            print(f"滚动执行异常: {e}")
            return False

    def auto_play_and_load_more_comments(self, user_input):

        url = 'https://www.tiktok.com/' + user_input
        print(f"打开视频页面: {url}")
        self.driver.get(url)
        wait = WebDriverWait(self.driver, 20)
        # wait.until(EC.presence_of_element_located((By.TAG_NAME, "video")))
        # 等待评论tab加载完毕
        # wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "div.TUXTabBar-list")))
        wait.until(EC.presence_of_element_located((By.XPATH, "//span[@data-e2e='comment-icon']")))

        print("视频评论已加载")

        # 点击评论按钮
        try:
            comment_span = wait.until(
                EC.element_to_be_clickable((By.XPATH, '//span[@data-e2e="comment-icon"]'))
            )
            print("正在点击评论图标 (span[@data-e2e='comment-icon'])...")
            self.driver.execute_script("arguments[0].click();", comment_span)
        except Exception as e:
            print(f"无法点击评论按钮: {e}")
            return

#         time.sleep(2)
#         debug_prefix = extract_tiktok_filename(user_input)
#         try:
#             # 保存 HTML
#             with open(f"{debug_prefix}_after_click.html", "w", encoding="utf-8") as f:
#                 f.write(self.driver.page_source)
#             print(f"页面 HTML 已保存: {debug_prefix}_after_click.html")
#
#             # 保存截图
#             self.driver.save_screenshot(f"{debug_prefix}_after_click.png")
#             print(f"页面截图已保存: {debug_prefix}_after_click.png")
#         except Exception as e:
#             print(f"保存调试文件失败: {e}")

        # 加载第一页评论
        first_page_data = self.wait_for_comments(10)
        if not first_page_data:
            print("未捕获到第一页评论")
            return
        self.comments_data.append(first_page_data)

        # 模拟滚动加载更多评论
        # self.driver.execute_script("document.querySelector('.TUXTabBar-content').scrollTo(0, document.querySelector('.TUXTabBar-content').scrollHeight);")
        # 改为调用新方法
        time.sleep(1)
        if self.scroll_comment_section():
            print("已滚动加载更多评论...")
            time.sleep(1)  # 等待新评论加载
        else:
            print("无法滚动评论区,可能结构变化")

        # 加载第二页评论
        second_page_data = self.wait_for_comments(10)
        if second_page_data:
            # 假设每页返回的数据结构相似,合并 comments 字段
            merged_comments = merge_comments(first_page_data, second_page_data)
        else:
            merged_comments = first_page_data
            print("未捕获到第二页评论")


        filename = f"{extract_tiktok_filename(user_input)}.json"
        print(filename)
        with open(filename, "w", encoding="utf-8") as f:
            json.dump(merged_comments, f, ensure_ascii=False, indent=2)
        print(f"评论数据已保存到: {filename}")
        print(f"   共 {len(merged_comments.get('comments', []))} 条评论")

    def wait_for_comments(self, timeout_seconds=10):
        """等待并捕获评论API响应"""
        start_time = time.time()
        while time.time() - start_time < timeout_seconds:
            comment_data = self.extract_comment_response_from_logs()
            if comment_data:
                return comment_data
            time.sleep(0.5)
        return None

    def close(self):
        if hasattr(self, "driver"):
            self.driver.quit()


def main():
    parser = argparse.ArgumentParser(
        description="Scrape TikTok comments via /api/comment/list/ ")
    parser.add_argument(
        "video_input",
        help="TikTok video URL or video_id, e.g., '/@user/video/7318855966163275054' "
    )
    args = parser.parse_args()

    video_input = args.video_input.strip()
    print(video_input)

    if not video_input:
        print("Error: Video input cannot be empty")
        sys.exit(1)


    scraper = TiktokScraper()
    try:
        scraper.auto_play_and_load_more_comments(video_input)
        time.sleep(1)  # 保持窗口打开以便观察
    finally:
        scraper.close()

    sys.exit(0)

if __name__ == "__main__":
    main()

用户页面发布的视频

这里只实现啦只第一页接口的数据, /api/post/item_list/把这个接口的数据放到啦一个json文件中。
这个页面我做了根据cookie的登陆,其实不登陆应该也可以。cookie 文件是通过chrome扩展 Cookies.txt 生成。登陆TikTok后点击这个扩展下载文件下来就行。
image.png

python3.11 post_item_list.py @dlw2026
image.png

post_item_list.py 代码如下:

# scraper.py
import asyncio
import json
import sys
import argparse
from playwright.async_api import async_playwright
from cookies import load_cookies_safely


# 这是用来抓取用户主页的 /api/post/item_list/


async def scrape_tiktok_user(username):
    target_responses = []
    clean_username = username.lstrip("@")
    output_json = clean_username + "_posts.json"
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        context = await browser.new_context(
            viewport={"width": 1920, "height": 1080},
            user_agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"
        )

        cookies = load_cookies_safely()
        await context.add_cookies(cookies)
        page = await context.new_page()

         # 隐藏自动化特征
        await page.add_init_script("""
            // 隐藏 webdriver 标志
            delete navigator.__proto__.webdriver;
            window.chrome = { runtime: {} };
            // 伪造 platform 为 Mac
            Object.defineProperty(navigator, 'platform', {
                get: () => 'MacIntel'
            });
            // 伪造 userAgentData(高熵指纹)
            if (!navigator.userAgentData) {
                Object.defineProperty(navigator, 'userAgentData', {
                    value: {
                        brands: [
                            { brand: "Chromium", version: "120" },
                            { brand: "Google Chrome", version: "120" },
                            { brand: "Not:A-Brand", version: "99" }
                        ],
                        mobile: false,
                        platform: "macOS",
                        getHighEntropyValues: async (hints) => ({
                            architecture: "x86_64",
                            model: "",
                            platform: "macOS",
                            platformVersion: "13.5",
                            uaFullVersion: "120.0.6099.0"
                        })
                    },
                    writable: false,
                    configurable: false
                });
            }
        """)

        # ✅ 关键:宽松匹配 API(不再检查 content-type)
        def handle_response(response):
            url = response.url
            if (
                    "/api/post/item_list/" in url
                    and response.status == 200
                    and "tiktok.com" in url
            ):
                if not target_responses:
                    target_responses.append(response)
                    print(f"捕获 API: {url.split('?')[0]}")

        page.on("response", handle_response)

        url = f"https://www.tiktok.com/{username}"
        print(f"打开页面: {url}")
        await page.goto(url, wait_until="domcontentloaded", timeout=50000)

        # 等待用户信息出现
        try:
            await page.wait_for_selector('h1[data-e2e="user-title"]', timeout=15000)
            print("用户主页加载成功")
        except:
            print("用户信息未加载,继续尝试...")

        # 滚动一下,触发懒加载(重要!)
        await page.evaluate("window.scrollTo(0, document.body.scrollHeight / 3)")

        # 等待 API(最多 20 秒)
        for i in range(40):
            if target_responses:
                break
            if i % 10 == 0:
                print(f"⏳ 等待 API 中... ({i * 0.5}s)")
            await asyncio.sleep(0.5)

        api_data = None
        if target_responses:
            try:
                api_data = await target_responses[0].json()
                print("✅ 成功解析 JSON 数据")
            except Exception as e:
                # 如果 .json() 失败,可能是 text/plain,手动解析
                try:
                    text = await target_responses[0].text()
                    api_data = json.loads(text)
                    print("✅ 通过 .text() 成功解析 JSON")
                except:
                    print(f"❌ 完全无法解析响应: {e}")

        if api_data:
            with open(output_json, "w", encoding="utf-8") as f:
                json.dump(api_data, f, ensure_ascii=False, indent=2)
            items = api_data.get("itemList", [])
            print(f"抓取到 {len(items)} 个视频,已保存至 {output_json}")
        else:
            print("未捕获到任何 API 数据")
            # 调试:打印所有请求(可选)
#             await page.route("**/*", lambda route: print("REQ:", route.request.url) or route.continue_())
#         screenshot_path = f"{clean_username}_homepage.png"
#         await page.screenshot(path=screenshot_path, full_page=True)
#         print(f"已保存页面截图: {screenshot_path}")

        await page.wait_for_timeout(5000)
        await browser.close()
        if api_data:
            return True
        else:
            return False


def main():
    parser = argparse.ArgumentParser(description="Scrape TikTok user profile")
    parser.add_argument("username", help="TikTok username (with or without @), e.g., @dishilife or dishilife")
    args = parser.parse_args()

    username = args.username.strip()
    if not username:
        print("Error: Username cannot be empty")
        sys.exit(1)
    if not username.startswith('@'):
        username = '@' + username
    success = asyncio.run(scrape_tiktok_user(username))
    sys.exit(0 if success else 1)


if __name__ == "__main__":
    main()

cookies.py脚本:

import os
from datetime import datetime



COOKIES_FILE = "cookies.txt"

def load_cookies_safely():
    filepath = COOKIES_FILE
    if not os.path.exists(filepath):
        raise FileNotFoundError(f"❌ Cookie 文件不存在: {os.path.abspath(filepath)}")

    cookies = []
    current_ts = int(datetime.now().timestamp())
    tiktok_domains = {".tiktok.com", "www.tiktok.com"}

    with open(filepath, "r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if not line or line.startswith("#"):
                continue
            parts = line.split("\t")
            if len(parts) < 7:
                continue

            domain = parts[0]
            if domain.startswith("#HttpOnly_"):
                domain = domain[len("#HttpOnly_"):]
            if not domain.startswith("."):
                domain = "." + domain.lstrip(".")

            if not any(t in domain for t in tiktok_domains):
                continue

            cookie = {
                "name": parts[5],
                "value": parts[6],
                "domain": domain,
                "path": parts[2],
                "secure": parts[3].upper() == "TRUE",
            }

            expires_str = parts[4]
            if expires_str.isdigit():
                expires = int(expires_str)
                if expires > current_ts:
                    cookie["expires"] = expires

            cookies.append(cookie)

    if not cookies:
        raise ValueError("❌ 未加载有效 Cookie!请确认包含 sessionid。")
    return cookies

if __name__ == "__main__":
    print('不可以直接执行')

自签名有什么用,就不用我多说了吧,会玩的用处就多,分享一个简单的自签教程,废话不多说,下面是操作。
1.下载Sideloadly软件,下载地址:https://sideloadly.io

2.打开后连接手机入下图,会显示你的手机名称;

3.把你准备好的ipa软件拖入这个软件里面,举个例子:tiktok,下载连接:https://www.mediafire.com/file/7rtq772su5wsa57/tiktok.ipa/file

4.输入你的apple ID,点击确认后输入密码后最后点击下面的Star就开始签名

5.等到桌面出现软件图标的时候就算签名完成了,然后点击手机设置-通用-VPN与设备管理,信任以你签名的apple ID,

6.然后就可以打开软件愉快的玩耍啦,

很多玩法需要自己去解锁,比如安装低版本、或者好玩的版本,至于软件可以自己去找找,有很多的

ps:7天就掉签的,然后得再弄一次