ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于Playwright的短视频矩阵自动发布系统实践

2026/9/11 21:41:38 拓冰建站 浏览量
基于Playwright的短视频矩阵自动发布系统实践 简介短视频矩阵内容分发系统设计资源包面向正在做课程设计、期末大作业或毕业设计的计算机相关专业学生也适合有一定编程基础、希望研究浏览器自动化与多平台内容分发的技术爱好者。资源瞄准视频多平台分发的典型痛点围绕Playwright实现了从登录、队列等待到自动上传的完整链路覆盖抖音、快手、视频号、小红书等平台源码内包含用户登录模块、视频发布队列、各平台上传器、公共函数封装、数据库表结构和防检测脚本并配有运行说明与演示视频可帮助读者快速理解整体项目架构与核心逻辑各平台上传器相互独立便于按需扩展新的视频渠道扫码登录、定时发布、文件预检等逻辑也可直接复用。压缩包共28个文件以19个Python脚本为主兼有JavaScript脚本、SQL文件、Markdown说明、图片和演示视频等整包仅5.09MB轻量且便于部署调试。已有187人学习下载对于需要完成课程项目或搭建自动发布原型的学习者是一份具有直接参考价值的完整实现。1. 短视频矩阵分发系统的自建切入点Playwright 队列化发布矩阵账号一多手动上传视频的时间成本就不再是线性增长而是指数增长四个平台、四套登录态、四份话题标签规则每天重复一遍就是半小时起步。市面上的 SaaS 分发工具按月收费还经常因为接口变动集体挂掉一旦平台改版你连什么时候恢复都不知道。这个项目展示了另一种做法——用 Playwright 驱动浏览器按队列把本地视频分发到抖音、快手、视频号、小红书四个平台登录态存本地、发布记录落 SQLite、失败自动重试。它不解决内容生产只解决“把已做好的视频按规则发布出去”这个环节。适合正在做课程设计、需要给 MCN 小团队搭内部分发管道、或者想搞懂浏览器自动化在真实平台应用边界的开发者。整个系统的核心不是抓取数据而是可控地执行 UI 操作因此 Playwright 在这里的角色从测试工具扩展成了内容分发引擎。2. Playwright 反检测与登录态复用user_queue_login.py 与 stealth 脚本2.1 风控检测做了什么stealth 脚本补了什么直接用 Playwright 打开抖音创作者平台时在 DevTools 里执行navigator.webdriver会返回true这是被判定为自动化操作最直接的特征。平台风控脚本通常还会检查chrome.runtime是否存在、HTMLCanvasElement.prototype.toDataURL是否有调用痕迹、以及浏览器启动参数里有没有 headless 相关标记。这个项目把 requireCool 的stealth.min.js以本地文件形式放进仓库注意文件名cdn.jsdelivr.net_gh_requireCool_stealth.min.js_stealth.min.js说明是从 jsdelivr 保存到本地目的就是在页面任何业务脚本执行之前先把这些 API 的返回值改掉减少被识别成自动化的概率。from playwright.async_api import async_playwright stealth_path ./cdn.jsdelivr.net_gh_requireCool_stealth.min.js_stealth.min.js async def create_authed_context(platform: str): p await async_playwright().start() browser await p.chromium.launch( headlessFalse, args[--disable-blink-featuresAutomationControlled], ) context await browser.new_context( viewport{width: 1280, height: 900}, localezh-CN, timezone_idAsia/Shanghai, ) await context.add_init_script(pathstealth_path) return browser, context这里的关键是add_init_script它会在每个 document 创建时、页面内联脚本执行之前插入 stealth 代码。--disable-blink-featuresAutomationControlled则是配合去掉 Chrome 自动控制提示的常见参数。项目里的user_queue_login.py就是基于这个 context 打开对应创作者平台把二维码展示出来等用户扫码。2.2 扫码登录与登录态持久化user_queue_login.py的执行流程是启动浏览器 → 打开创作者平台 → 等二维码元素出现 → 截图保存到qrcode11.jpg或1.png文件里同时存在两个图片对应不同平台的二维码截图命名 → 轮询登录状态 → 拿到登录后的 storage_state。需要注意 storage_state 不仅包含 cookie还包含 localStorage视频号这类平台的 token 往往放在 localStorage 里只存 cookie 是不够的。async def wait_login_and_save(context, platform: str, qrcode_file: str): page context.pages[0] if context.pages else await context.new_page() # 等待二维码元素可见后截图常见选择器是 img.qrcode 或带 qrcode class 的节点 qr page.locator(img.qrcode, div[class*qrcode]) await qr.first.wait_for(statevisible, timeout30000) await qr.first.screenshot(pathqrcode_file) # 轮询检查登录是否成功出现用户头像或“发布视频”入口即认为已登录 for _ in range(120): if await page.locator(text发布视频, [class*avatar]).first.count() 0: break await page.wait_for_timeout(1000) await context.storage_state(pathfstate_{platform}.json)参数说明二维码等待超时是 30 秒超时说明页面没打开或选择器失效轮询 120 次、每次间隔 1 秒相当于最多等 2 分钟让用户完成扫码。保存出的state_douyin.json这类文件之后会直接被发布队列加载这也是“下载即用”的关键——不用每次启动都重新扫码。2.3 缓存与时间工具的配合utils/cache.py维护一个视频是否已发布的集合键是视频文件的 md5。为什么用 md5 而不是文件名矩阵分发时同一个视频可能被复制出多个文件名但内容相同md5 能挡住重复发布。utils/files_times.py按文件修改时间排序扫描 video 目录时旧的先发符合“先做好的素材先分发”的运营习惯。文件职责关键参数user_queue_login.py扫码登录保存 storage_stateqrcode_file、轮询次数utils/cache.pymd5 去重防止重复发布cache_db 路径utils/files_times.py按文件修改时间排序orderascconstant.py平台主域名、选择器前缀、超时阈值各平台超时值这套组合解决的是发布前的“确定性”登录态确定存在、视频文件确定未被发过、发送顺序确定可预期。后面队列消费时就不需要再处理这些横切逻辑每个 uploader 只管最纯粹的上传动作。3. 发布队列与平台适配器publish_video_queue.py 驱动四个 uploader3.1 为什么用 SQLite 做任务队列这个项目的中枢是database/matrix.sql。单机分发场景下 Redis 不是必需品SQLite 一行记录一个视频加平台事务天然支持重启不丢任务。核心表结构可以简化为CREATE TABLE video_queue ( id INTEGER PRIMARY KEY AUTOINCREMENT, video_path TEXT NOT NULL, platform TEXT NOT NULL CHECK(platform IN (douyin,kuaishou,xhs,shipinhao)), status TEXT DEFAULT pending, retry_count INTEGER DEFAULT 0, published_at TEXT, created_at TEXT DEFAULT (datetime(now, localtime)) ); CREATE INDEX idx_video_queue_pending ON video_queue(status, platform);platform字段用 CHECK 约束成四个枚举值避免脏数据status由pending流转到published或failed。索引建在statusplatform组合上因为publish_video_queue.py每次取待发记录时都是WHERE statuspending再按平台分组这个索引能让扫描走 index seek 而不是全表扫。3.2 平台适配器的差异处理四个 uploader 目录结构一致每个都有main.py但差异点非常明显这也是这个项目最有复用价值的部分。douyin_uploader/main.py抖音创作者平台的上传是input typefile元素接收文件不需要走文件选择对话框。用set_input_files直接写入绝对路径即可。标题输入框和话题组件在同一个页面里异步加载重点动作之间建议加 500ms 等待。ks_uploader/main.py快手后台上传按钮点击后会先弹“选择作品”区域实际是隐藏 input 被 label 触发此时set_input_files要挂到 label 对应的 input 上而不是页面第一个 file input否则文件会传错入口。tencent_uploader/main.py视频号助手页面间跳转会切换 top-level window发布时要监听新窗口用context.expect_page捕获新页面再去操作。同时视频号对标题长度有限制提交前要做裁剪避免前端校验拦截。xhs_uploader/main.py小红书创作服务平台的上传区域是动态 iframe需要先frame_locator定位 iframe再在 frame 内查选择器。等待策略比其余三个平台更重先用wait_for_function等 iframe 内部 DOM 出现“上传”按钮再执行文件写入这段逻辑和用 scrapy 处理动态 iframe 的思路一致——先确认子文档加载完再操作子文档里的元素。# xhs_uploader/main.py 的上传思路 frame page.frame_locator(iframe[src*upload]) await frame.locator(input[typefile]).set_input_files(video_path) await page.wait_for_selector(text上传成功, timeout120000)frame_locator返回的FrameLocator可以直接链式调用locator查询与 page 的 locator API 一致。set_input_files对真实浏览器和 headless 模式都有效120 秒是为了给大文件上传留出转码前的余量测试用的video/1.mp4通常在几秒内就会进入转码阶段。3.3 主循环与重试边界publish_video_queue.py的主循环逻辑非常直白while True: tasks db.fetch_pending(limit5) for task in tasks: try: uploader get_uploader(task.platform) uploader.publish( video_pathtask.video_path, storage_statefstate_{task.platform}.json, ) db.mark(task.id, published) except Exception as e: db.mark(task.id, failed, retrytask.retry_count 1) time.sleep(interval)get_uploader按平台名映射到对应模块storage_state是第 2 章扫码登录得到的 JSON。失败时只把retry_count加一超过上限才置为failed这样单平台临时返回“请稍后再试”不会影响整个队列。间隔参数推荐 60 秒四个平台轮流发而不是并发发配合各平台的频率限制降低被限流的概率。utils/cache.py在这里再次起作用mark published之前先查视频 md5 是否已在其他平台成功发布过。已发布过但不是本任务的目标平台时状态直接置为skipped这样重跑队列时不会重复上传。4. xhs-api 辅助服务与配置中心utils 与 database 的协作方式4.1 xhs-api 提供什么接口xhs-api目录里的app.py是 FastAPI 服务js/app2024.py是它的辅助模块。小红书上传流程中的一部分预检参数需要单独计算这个服务被设计成独立进程避免把签名逻辑耦合进 uploader。对外暴露两个接口GET /api/health和POST /api/xhs/precheck。cd xhs-api pip install fastapi uvicorn uvicorn app:app --host 127.0.0.1 --port 8000启动后发布队列遇到 xhs 平台任务时会先调 precheck 拿参数再进上传流程。如果服务没启动xhs_uploader/main.py的 try/except 会降级为不带预检参数的原始流程其余三个平台照常工作服务边界划得很干净。4.2 utils/conf.py 的配置优先级utils/conf.py做的事情是读 config.json → 读环境变量 → 用默认值填充缺项环境变量优先级最高。这样可以不改代码就切换运行参数。# utils/conf.py 的配置合并逻辑 import os, json def load_config(pathconfig.json): cfg { headless: True, interval: 60, max_retry: 3, upload_timeout: 120, } if os.path.exists(path): cfg.update(json.load(open(path, encodingutf-8))) for key in cfg: if key.upper() in os.environ: cfg[key] os.environ[key] return cfg环境变量覆盖用的是key.upper()部署时可以直接INTERVAL30 python publish_video_queue.py临时把扫描间隔从 60 秒改成 30 秒。注意headless配置调试阶段建议设为false部分平台通过 User-Agent 或 WebGL 参数对 headless 环境有额外校验Playwright 的 chromium headless shell 与有头模式的浏览器指纹存在差异设为 false 能减少这类干扰。配置项默认值说明headlesstrue是否无头模式运行interval60队列扫描间隔单位秒max_retry3单个任务最大重试次数state_dir./statesstorage_state 存放目录upload_timeout120上传等待超时单位秒4.3 完整运行链路顺序上先初始化数据库再配置 conf.py然后跑user_queue_login.py逐个平台登录最后跑发布队列python -c import sqlite3; csqlite3.connect(matrix.db); c.executescript(open(database/matrix.sql, encodingutf-8).read()) python user_queue_login.py --platform douyin python user_queue_login.py --platform kuaishou python user_queue_login.py --platform xhs python user_queue_login.py --platform shipinhao python publish_video_queue.py每条命令建议独立终端运行或用进程守护工具拉起。user_queue_login.py每处理一个平台就保存一个 state JSON发布队列读取时按平台名自动匹配。整个链路里最容易出问题的环节是登录态过期所以“登录”和“发布”被拆成了两个独立命令发布队列里如果发现 storage_state 文件缺失会直接报错并跳过该平台而不是卡住整条队列。5. 用 Playwright tracing 与幂等标记定位发布失败点矩阵分发最怕的不是发布失败而是失败后重跑把视频发两遍。这里有两个可以稳定落地的技巧组合使用能同时解决“失败现场不好查”和“重跑重复发”两个问题。第一个技巧是把 tracing 开在 uploader 内部而不是等到失败后手动加。# douyin_uploader/main.py 的调试入口 async def publish_with_trace(context, video_path: str): trace_file ftrace_{platform}_{int(time.time())}.zip await context.tracing.start(screenshotsTrue, snapshotsTrue) try: await do_upload(context, video_path) finally: await context.tracing.stop(pathtrace_file)screenshotsTrue会在每个关键动作前截图snapshotsTrue记录 DOM 快照。失败后执行python -m playwright show-trace trace_xxx.zip在时间线里能看到点击上传按钮前后的页面状态与网络请求比看日志直观得多。重点看两个位置点击提交按钮后请求是否发出平台返回的提示框文案是什么。第二个技巧是幂等标记。utils/cache.py的 md5 去重针对的是单平台但同一个视频发出四个平台后下次扫描还要不要发做法是在video_queue表增加唯一索引应用层计算好 md5 再写入CREATE UNIQUE INDEX idx_video_unique ON video_queue(video_path, platform, video_md5);发布队列每次入队前先查这个索引已存在且statuspublished的直接跳过状态是failed的则允许重试。配合 tracing重跑队列时走到失败平台上直接用对应 trace 看现场不会再出现重复发布。验证发布成功的另一种可靠方式是断言式检查发布流程结束后在平台“作品管理”页统计作品卡片数量与发布前对比加一。这个方法对四个平台都适用比读接口返回值更贴近真实效果。本文还有配套的精品资源点击获取