资讯详情

资讯详情

Python+Playwright实现抖音/B站/小红书视频一键自动发布

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(headlessFalse) context browser.new_context() page context.new_page() page.goto(url) input(请在浏览器中完成登录然后回到终端按回车...) context.storage_state(pathstate_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, fstates/{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(headlessFalse) context browser.new_context() page context.new_page() page.goto(login_url) input(请手动完成登录完成后按回车键保存状态...) context.storage_state(pathstate_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(headlessFalse) self.context self.browser.new_context( storage_stateself.state_file, localezh-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_untilload) 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[typefile]就可以直接赋值。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_untilload) # 选择视频文件 self.page.set_input_files(input[typefile], task[video]) # 等待发布按钮出现表示页面上传流程已就绪 self.page.wait_for_selector(text发布, timeout120000) # 填写标题 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[typefile], task[cover], timeout10000) # 点击发布 publish_btn self.page.get_by_role(button, name发布) publish_btn.click() self.page.wait_for_timeout(5000)这里有几个细节点需要展开说说。第一选择视频文件后不要立刻填标题。页面在收到文件后会立即开始上传和转码此时页面可能还会出现视频处理中转码中之类的状态一些表单元素可能是禁用的。稳妥的做法是等待发布按钮出现并且确保页面没有正在处理的提示后再操作表单。上面代码里我用wait_for_selector(text发布, timeout120000)做了一道卡点实际上更严谨可以再判断是否有处理中上传中文案消失不同时间页面文案可能会变所以要按你实际操作时看到的界面去微调。第二关于封面。抖音网页端通常会在转码成功后自动从视频里抽几帧给你选。如果你不想用自动帧而是要用自己的本地封面图页面会有上传封面按钮点击后会出现一个新的文件选择入口。这个入口不一定是一个显眼的input[typefile]所以稳妥做法是先点击上传封面按钮让它弹出文件选择框再定位最新出现的input。示例里的写法是触发按钮后再对input[typefile]传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_untilload) # 上传视频文件 self.page.set_input_files(input[typefile], task[video]) # 等待保存按钮可点击表示上传与转码基础完成 self.page.wait_for_selector(text保存, timeout180000) # 填写标题 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[typefile], task[cover], no_wait_afterTrue) # 提交投稿 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_untilload) # 上传视频 self.page.set_input_files(input[typefile], task[video]) self.page.wait_for_selector(text发布, timeout120000) # 标题 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这时要改用contenteditabletrue的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_ms180000): 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做多平台自动发布本质上是把一套浏览器操作舞蹈变成了可复现的脚本。它适合个人创作者、小团队做日常分发但不适合把它当成高并发、强稳定性的商业化系统来搭因为浏览器自动化天然存在脆弱性平台随便调整一下页面结构都可能让你的脚本停摆。所以我个人建议把它定位成效率工具而不是核心业务系统。如果后续想在这个方向继续深挖有几个扩展角度接入任务队列。之前我用的是本地JSON文件正式一点可以用SQLite或Redis做任务队列支持从内容后台自动拉取待发布任务。增加发布结果回传。发布完成后把结果状态、链接、截图回传到团队协作系统方便复核。加入模板化内容库。不同平台对标题长度、话题数量的要求不一样可以针对每个平台维护一套文案模板运行时动态渲染。做可视化配置页面。写一个小后台把视频路径、标题、话题这些参数用表单录入避免每次改代码。如果你所在团队已经有内容管理系统也可以把这套脚本封装成一个发布组件嵌入到现有流程里这样比单独跑一个脚本更符合日常操作习惯。最后再说一条我自己踩过的坑脚本写完后先把三个平台各跑一遍测试任务确认每个环节都稳定再开始批量导入真实任务。不要一上来就几十条视频一起跑一旦某个平台的登录态过期后面的任务全部会卡在登录页上而且你还不容易察觉到。测试任务最好选那种短小的、不重要的视频避免误发造成的后续处理成本。这套方案我用了挺长时间了从最初的十几个脚本文件慢慢收敛成现在的结构。比起省下的时间更大的收获是让我重新思考了一类问题很多重复工作的解法不在一个更聪明的算法里而在于把人在网页上做的操作结构化、程序化。你只要愿意把流程拆细把每个步骤的等待条件想清楚这套思路也可以迁移到很多其他网页操作场景里。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →