☰
Python+Playwright实现抖音/B站/小红书视频一键自动发布
2026/10/20 8:42:37 网站建设 项目流程

1. 项目背景与需求拆解

很多做内容的朋友应该都有过这种崩溃瞬间:一条视频明明已经剪好了,但还要分别打开抖音、B站、小红书的创作后台,挨个上传、重新填标题、重新选封面,运气好十分钟搞完,运气不好遇到某边网络抽风,半小时就没了。我做这个Social-Auto-Upload项目的目标很简单——用Python加上Playwright,把"上传素材→填标题描述→加话题标签→选封面→点发布"这条重复链路统一编排,实现一条视频在三平台一键联动分发。这套方案适合每天要发多条内容、希望省下重复劳动的独立创作者、小团队运营者,也适合希望用脚本把固定流程"托管"掉的技术同学。

1.1 为什么内容分发需要自动化

先算一笔时间账。手动发布一条视频到单个平台,平均要经历:打开后台(10秒)、拖拽视频或选择文件(10秒)、等上传和转码(视网速和视频体积,通常30秒到2分钟)、填标题和描述(30秒)、加话题(10秒)、选封面或裁剪封面(30秒)、勾选各种声明(10秒)、点击发布(5秒)。顺畅的情况下单平台至少2分钟,碰到要剪辑封面或者平台审核排队,拖到5分钟也不奇怪。三个平台下来,一条视频纯"搬运"就要花掉十分钟以上。

这个重复过程有个特点:操作高度固定,几乎没有需要临场判断的环节。这正是脚本化最舒服的场景。你不需要写复杂的算法,只需要把一个"人工用户"要做的动作按步骤复现,并且每步都等待页面真正完成后再做下一步。用自动化的方式把三边流程串起来之后,一条视频的发布成本从"三平台乘以5分钟"降到了"三平台共3到4分钟",而且过程中基本不用守着屏幕。

1.2 技术选型:为什么不是接口、不是Selenium,而是Playwright

做自动化方案的时候,我对比过三条路。

第一条路是逆向每个平台的网页或App接口,直接构造上传请求。这条路最难。各平台都有签名参数、风控校验,今天调通明天失效,成本极高,也不适合非逆向方向的开发者。普通内容创作者要的是能稳定用起来的工具,而不是天天跟加密算法较劲。

第二条路是用Selenium控制浏览器。Selenium本身成熟,但安装驱动、等待逻辑、处理弹窗易碎标签这些体验比较差,近年维护节奏也不如人意。它的问题不在于不能用,而在于"用起来疼":写脚本的时间可能比手动发布还长。

第三条路是用Playwright控制真实浏览器。Playwright由浏览器团队自己维护,支持自动等待、截图、录制脚本、多上下文,处理文件上传这类场景干净利落。我最终选了Playwright。原因有几个:第一,它支持多浏览器上下文,这意味着每个平台可以用独立的Cookie环境互不干扰;第二,它的定位器会做自动等待,写脚本时可以少写大量sleep;第三,调试体验好,出现异常可以保留现场截图和页面快照;第四,Python API非常接近人的直觉,学习成本低。对这些"网页自动化"需求来说,Playwright几乎是当下比较省心的选择。

2. 核心功能设计与思路

2.1 整体架构

整个项目按"调度层-浏览器层-执行层"三层切分。调度层读取一个视频任务列表(比如一个JSON或者CSV,每条记录包含视频路径、标题、描述、话题、封面路径等字段),逐个分发到各平台的执行器。浏览器层统一管理一个Playwright浏览器实例,每个平台跑在独立的上下文里,让登录状态互相隔离。执行层是三个平台各自的Uploader类,对外暴露同一个接口,比如prepare、fill_detail、publish。

这样做的好处很明显:加新平台时只写一个新的Uploader,不影响已有逻辑;任务列表从文件来,以后接入定时任务或者内容后台都很方便;每个平台的登录态单独保存,一个掉线不会牵连其他两个。

2.2 登录态统一管理

平台登录是自动化发布绕不开的门槛。如果每次执行都扫码登录,那自动化就没有意义。Playwright提供了一个很实用的能力:storage_state。第一次运行登录脚本时,我可以打开浏览器窗口,手动扫码完成登录,然后把当前上下文的Cookies和LocalStorage一起导出成一个JSON文件;之后脚本启动时加载这份状态,多数情况下可以保持登录一段时间。

这里要提醒的是,平台的登录态有效期不固定,有的可能一周,有的可能一个月,到期后脚本首次访问就会跳回登录页。所以项目里我加了一个轻量检测逻辑:访问创作后台首页后,判断URL里是否出现login或passport特征,如果出现就提示需要重新登录并重新保存state文件。

from playwright.sync_api import sync_playwright def auto_login(url: str, state_path: str): with sync_playwright() as p: browser = p.chromium.launch(headless=False) context = browser.new_context() page = context.new_page() page.goto(url) input("请在浏览器中完成登录,然后回到终端按回车...") context.storage_state(path=state_path) browser.close()

2.3 并发还是串行

刚做第一版时,我一度想让三个平台同时上传,觉得这样更快。实测后很快就放弃了:一是三个平台的上传和转码都吃带宽,并发会让各自速度明显变慢;二是同时操作多个页面,如果其中一个抛异常,处理起来很混乱;三是短时间内在多个平台几乎同时发布同一条内容,还顶着同一套IP,容易触发不必要的风控。最终我改成了串行执行,每个平台发布完成后休息几秒到十几秒,再用随机间隔进入下一个平台。整体耗时基本没增加多少,稳定性却上了一个台阶。

这里我整理了一套任务调度伪代码,后面实现也可以照着这个思路来扩展自己的任务源:

import json import random import time TASKS = [ { "video": "videos/ep01.mp4", "title": "自动发布初体验", "desc": "这是用脚本完成的分发测试", "tags": ["自动化", "内容创作"], "cover": "covers/ep01.jpg", } ] def create_uploader(platform: str, state_path: str): # 根据平台名返回对应的上传器实例 if platform == "douyin": return DouyinUploader(state_path) if platform == "bilibili": return BilibiliUploader(state_path) if platform == "xiaohongshu": return XiaohongshuUploader(state_path) raise ValueError(f"未知平台: {platform}") for task in TASKS: for platform in ["douyin", "bilibili", "xiaohongshu"]: uploader = create_uploader(platform, f"states/{platform}_state.json") uploader.run(task) time.sleep(random.uniform(8, 20))

3. 实操过程与关键代码实现

先说环境准备。项目依赖很干净:一台电脑,Python 3.9以上版本,安装Playwright库,并安装好它内置的Chromium浏览器。

pip install playwright playwright install chromium

下面按"登录态准备→三个平台分别实现→跑通完整链路"的顺序拆解。

3.1 登录态准备脚本

创建一个save_state.py,把每个平台各自的登录凭证存成独立文件。这里我用一个通用函数,传入平台后台地址和保存路径。

from playwright.sync_api import sync_playwright def save_login_state(login_url: str, state_file: str): with sync_playwright() as p: browser = p.chromium.launch(headless=False) context = browser.new_context() page = context.new_page() page.goto(login_url) input("请手动完成登录,完成后按回车键保存状态...") context.storage_state(path=state_file) browser.close() if __name__ == "__main__": # 示例:分别为三个平台保存登录态 save_login_state("https://creator.douyin.com/", "states/douyin_state.json") save_login_state("https://member.bilibili.com/platform/upload/text", "states/bilibili_state.json") # 小红书创作平台入口 save_login_state("https://creator.xiaohongshu.com/", "states/xiaohongshu_state.json")

说明一下:运行这条脚本时,浏览器会弹出来,你像平时一样扫码或账号密码登录,登录成功后回到终端按回车,脚本就会把当前Cookie状态写到对应文件。这个过程只需要偶发做一次,直到状态失效。

3.2 一个通用上传器基类

为了让三个平台尽量复用代码,我会定义一个BaseUploader类,把打开浏览器、加载登录态、最终截图留证等公共逻辑放进去。子类只负责实现平台特有的步骤。

from abc import ABC, abstractmethod from playwright.sync_api import sync_playwright class BaseUploader(ABC): def __init__(self, state_file: str): self.state_file = state_file self.browser = None self.context = None self.page = None def start(self): self._playwright = sync_playwright().start() self.browser = self._playwright.chromium.launch(headless=False) self.context = self.browser.new_context( storage_state=self.state_file, locale="zh-CN", viewport={"width": 1366, "height": 900}, ) self.page = self.context.new_page() def stop(self): self.browser.close() self._playwright.stop() def ensure_login(self, homepage_url: str): self.page.goto(homepage_url, wait_until="load") if "login" in self.page.url or "passport" in self.page.url: raise RuntimeError("登录态已失效,请重新跑 save_state 脚本") @abstractmethod def run(self, task: dict): pass

这里比较核心的是ensure_login:每次发布前先访问后台首页,如果地址里出现登录特征就主动报错,避免后面所有操作都打在登录页上导致白等。这个判断可能因为不同平台首页路径不同而需要几行特判,但思路是通用的。

3.3 抖音创作服务平台发布流程

抖音后台的上传流程大致是:进入发布页、选择视频文件、填写作品标题和简介、添加话题标签、选择封面(可以选择自动截取或上传本地封面)、勾选原创声明,最后点击发布。

在Playwright里,最关键的一步是调用set_input_files。这个方法的原理是把本地文件路径直接塞给页面上的文件输入框,触发浏览器原生的文件选择逻辑。即使页面上的输入框是隐藏的,只要选择器能定位到input[type=file],就可以直接赋值。

class DouyinUploader(BaseUploader): UPLOAD_URL = "https://creator.douyin.com/creator-micro/content/upload" def run(self, task: dict): self.page.goto(self.UPLOAD_URL, wait_until="load") # 选择视频文件 self.page.set_input_files("input[type=file]", task["video"]) # 等待发布按钮出现,表示页面上传流程已就绪 self.page.wait_for_selector("text=发布", timeout=120000) # 填写标题 self.page.locator("input[placeholder*='填写作品标题']").fill(task["title"]) # 填写简介 self.page.locator("textarea[placeholder*='填写简介']").fill(task["desc"]) # 添加话题标签 for tag in task.get("tags", []): self.page.locator("div[class*='tag'] input, input[placeholder*='话题']").click() self.page.keyboard.type(tag) self.page.keyboard.press("Enter") # 选择封面:这里演示先触发上传封面按钮,再传本地文件 if task.get("cover"): cover_btn = self.page.get_by_text("上传封面") if cover_btn.count(): cover_btn.click() self.page.set_input_files("input[type=file]", task["cover"], timeout=10000) # 点击发布 publish_btn = self.page.get_by_role("button", name="发布") publish_btn.click() self.page.wait_for_timeout(5000)

这里有几个细节点需要展开说说。

第一,选择视频文件后不要立刻填标题。页面在收到文件后会立即开始上传和转码,此时页面可能还会出现"视频处理中""转码中"之类的状态,一些表单元素可能是禁用的。稳妥的做法是等待发布按钮出现,并且确保页面没有正在处理的提示后再操作表单。上面代码里我用wait_for_selector("text=发布", timeout=120000)做了一道卡点,实际上更严谨可以再判断是否有"处理中""上传中"文案消失,不同时间页面文案可能会变,所以要按你实际操作时看到的界面去微调。

第二,关于封面。抖音网页端通常会在转码成功后自动从视频里抽几帧给你选。如果你不想用自动帧,而是要用自己的本地封面图,页面会有"上传封面"按钮,点击后会出现一个新的文件选择入口。这个入口不一定是一个显眼的input[type=file],所以稳妥做法是先点击"上传封面"按钮,让它弹出文件选择框,再定位最新出现的input。示例里的写法是触发按钮后再对input[type=file]传cover文件,你实际调试时如果发现封面上传不生效,要考虑先触发按钮。

第三,发布按钮的定位。不同时期后台文案可能不同,可能是"发布",也可能是"发布作品"或"完成"。用get_by_role("button", name="发布")时尽量用包含匹配,避免按钮文字加了空格或个别字导致失败。

我实际跑下来的一个体会:抖音后台是最容易出状况的,因为它的页面交互比较多,上传完会有各种弹层和提示。建议在调试阶段把headless设为False,肉眼看一遍每一步,确认选择器稳定后再交给脚本跑。

3.4 B站投稿页发布流程

B站的投稿发布页相对规范很多,主路径比较稳定。进入投稿页后,同样用文件输入框上传视频,之后填写标题、简介、标签,选择分区,设置封面,最后点提交。

class BilibiliUploader(BaseUploader): UPLOAD_URL = "https://member.bilibili.com/platform/upload/video" def run(self, task: dict): self.page.goto(self.UPLOAD_URL, wait_until="load") # 上传视频文件 self.page.set_input_files("input[type=file]", task["video"]) # 等待"保存"按钮可点击,表示上传与转码基础完成 self.page.wait_for_selector("text=保存", timeout=180000) # 填写标题 self.page.get_by_placeholder("标题").fill(task["title"]) # 填写简介 self.page.get_by_placeholder("简介").fill(task["desc"]) # 添加标签 tag_input = self.page.locator("input[placeholder='标签']") for tag in task.get("tags", []): tag_input.click() tag_input.fill(tag) self.page.keyboard.press("Enter") # 设置封面 if task.get("cover"): cover_btn = self.page.get_by_text("上传封面") if cover_btn.count(): cover_btn.click() self.page.set_input_files("input[type=file]", task["cover"], no_wait_after=True) # 提交投稿 self.page.get_by_role("button", name="投稿").click() self.page.wait_for_timeout(5000)

B站这边比抖音省心的是,它投稿页本身就是为高频创作者设计的,元素结构相对清晰。但要注意几点:

第一,B站上传视频后会有非常长的转码等待。尤其自媒体视频体积不小,如果带宽不够,等待时间可能超过3分钟。所以我在这里把超时时间拉长到180秒,而不是用默认的30秒。如果等待超时,脚本会抛出Playwright的TimeoutError,你可以捕获后重试,也可以考虑用wait_for_function轮询"是否出现投稿成功字样"。

第二,B站的标签输入框有时候位置不固定,在页面上滚动才能看到。如果直接定位不到,可以用page.mouse.wheel向下滚动一点再操作。标签输入的逻辑我建议先click再fill,否则部分页面监听不到输入。

第三,投稿按钮可能叫"投稿"也可能是"立即投稿",同样用包含匹配或者精确匹配都行。实际执行时建议你在现场打开开发者工具,把要用的选择器重新确认一遍,因为B站前端偶尔会改版。

3.5 小红书创作服务平台发布流程

小红书的发布入口是通过创作服务平台,进入后选择"发布笔记"再上传视频。流程上比前两个更简洁:传视频、填标题、写正文、加话题、选择是否原创,然后发布。

class XiaohongshuUploader(BaseUploader): UPLOAD_URL = "https://creator.xiaohongshu.com/publish/publish" def run(self, task: dict): self.page.goto(self.UPLOAD_URL, wait_until="load") # 上传视频 self.page.set_input_files("input[type=file]", task["video"]) self.page.wait_for_selector("text=发布", timeout=120000) # 标题 self.page.get_by_placeholder("标题").fill(task["title"]) # 正文内容 self.page.get_by_placeholder("正文").fill(task["desc"]) # 添加话题 for tag in task.get("tags", []): self.page.get_by_text("添加话题").click() self.page.keyboard.type(tag) self.page.keyboard.press("Enter") # 勾选原创声明 original_checkbox = self.page.locator("text=原创") if original_checkbox.count(): original_checkbox.click() # 发布 self.page.get_by_role("button", name="发布").click() self.page.wait_for_timeout(5000)

小红书这边有个常见的坑:正文和标题如果页面按组件渲染,get_by_placeholder未必都能命中。比如正文输入框在某些版本的页面上是可编辑的div,这时要改用contenteditable=true的div做定位,然后用fill或evaluate去设置文本。我遇到过一种情况,fill能设置内容但页面不识别,必须用键盘逐字输入才能触发它的输入事件。如果你的脚本在某个平台出现"填了表单但发布后是空的",优先考虑是不是这类富文本组件的问题,把fill换成click加上keyboard.type逐字输入即可。

另外小红书对同设备、同IP的频繁发布比较敏感,脚本执行时建议在两次任务之间加随机间隔,不要在短时间内把一周的量全部发完。时机的选择也重要,尽量模拟真人活跃时间段。

4. 常见问题与排查技巧实录

下面这些问题都是我调试过程中真正遇到过的,整理成速查表,方便大家对号入座。

现象可能原因解决办法
点击发布按钮无反应页面存在弹层遮挡,或按钮处于disabled先关闭所有弹层,再判断按钮属性;用is_enabled()做前置检查
选择文件后上传进度一直不动网络慢或页面入参不稳定增大wait超时;检查视频文件路径是否为绝对路径
标题填不进去富文本组件没有触发input事件改用click加keyboard.type逐字输入
登录态失效,脚本跑到登录页Cookie过期重新执行save_state脚本,更新state文件
某步骤元素定位不到平台前端改版或弹层吸附优先用text定位;必要时加滚动或强制等待
同一个视频三平台都发布失败视频文件本身有异常或平台不兼容先用人工方式在其中一个平台上传,确认文件格式合规
发布后封面不正确转码未完成或封面选择逻辑错了上传后等待"封面可用"状态,再触发封面逻辑
脚本偶尔抛TimeoutError等待条件写窄了改用wait_for_function或增加轮询

4.1 选择器稳定性优化

我见过不少自动化脚本,第一版跑得很顺,一周后全废了,原因基本都是前端改了类名。减少这类问题有几点经验:

  • 优先用语义化角色,比如get_by_role("button", name="发布"),而不是一长串class。类名是前端频繁改的,按钮文字相对稳定。
  • 善用text定位。页面上的"上传封面""添加话题"是面向用户的关键词,比嵌套的div结构可靠。
  • 如果某个选择器必须用class,宁可取单一特征class,而不要拼接一长串祖先路径。祖先路径一变就全断。
  • 用locator.count()先判断元素是否存在,再决定要不要操作,避免一出错就白屏。

4.2 上传与转码等待的正确姿势

Playwright的自动等待已经比Selenium省心,但它不知道"视频转码完成"是什么状态。所以需要人工给它定义节点。我的做法是:上传文件之后,用wait_for_selector去等一个只有转码完成后才会出现的按钮或状态文本。这个做法的问题是文案可能变,所以更推荐做轮询:

import time def wait_until_text_settled(page, timeout_ms=180000): deadline = time.time() + timeout_ms / 1000 while time.time() < deadline: # 如果页面出现处理后状态文案,则返回 if page.locator("text=上传成功").count() or page.locator("text=封面设置").count(): return True time.sleep(2) raise TimeoutError("等待转码完成超时")

轮询虽然不如自动等待优雅,但在"等一个业务状态"这件事上确实更可靠。我后续项目里把这类判断统一收口成工具函数,三个平台共用。

4.3 风控与执行频控

这个话题我必须多说一句:自动化脚本操作的是你真实账号能干的动作,但"真人操作"和"脚本操作"的节奏差异是客观存在的。想要长期稳定运行,几条建议供参考:

  • 控制发布频率,不要连续批量发内容,单平台每天发布量控制在正常运营范围内。
  • 两次上传之间保留随机休息时间,不要精确到秒地定时触发。
  • 不要对平台的反造假机制动脑筋,比如伪造点击、刷量、绕过审核这类事坚决不能做。
  • 如果平台要求人工验证,就让脚本停下来等待人工处理,不要尝试用不正当手段绕过验证逻辑。

守住正常的发布节奏,这套方案在合规层面才站得住脚。

5. 方案边界与后续扩展

看到这里你应该能感受到,Python加Playwright做多平台自动发布,本质上是把一套"浏览器操作舞蹈"变成了可复现的脚本。它适合个人创作者、小团队做日常分发,但不适合把它当成高并发、强稳定性的商业化系统来搭,因为浏览器自动化天然存在脆弱性,平台随便调整一下页面结构都可能让你的脚本停摆。所以我个人建议,把它定位成"效率工具",而不是"核心业务系统"。

如果后续想在这个方向继续深挖,有几个扩展角度:

  1. 接入任务队列。之前我用的是本地JSON文件,正式一点可以用SQLite或Redis做任务队列,支持从内容后台自动拉取待发布任务。
  2. 增加发布结果回传。发布完成后把结果状态、链接、截图回传到团队协作系统,方便复核。
  3. 加入模板化内容库。不同平台对标题长度、话题数量的要求不一样,可以针对每个平台维护一套文案模板,运行时动态渲染。
  4. 做可视化配置页面。写一个小后台,把视频路径、标题、话题这些参数用表单录入,避免每次改代码。

如果你所在团队已经有内容管理系统,也可以把这套脚本封装成一个发布组件,嵌入到现有流程里,这样比单独跑一个脚本更符合日常操作习惯。

最后再说一条我自己踩过的坑:脚本写完后,先把三个平台各跑一遍测试任务,确认每个环节都稳定,再开始批量导入真实任务。不要一上来就几十条视频一起跑,一旦某个平台的登录态过期,后面的任务全部会卡在登录页上,而且你还不容易察觉到。测试任务最好选那种短小的、不重要的视频,避免误发造成的后续处理成本。

这套方案我用了挺长时间了,从最初的十几个脚本文件慢慢收敛成现在的结构。比起省下的时间,更大的收获是让我重新思考了一类问题:很多重复工作的解法不在一个更聪明的算法里,而在于把"人在网页上做的操作"结构化、程序化。你只要愿意把流程拆细,把每个步骤的等待条件想清楚,这套思路也可以迁移到很多其他网页操作场景里。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询