1. 项目概述:为什么需要自动化发布?

如果你运营着一个或多个微信公众号,肯定对“日更”的压力深有体会。找选题、写稿、排版、配图、预览、发布……一套流程下来,少说也得一两个小时。更别提那些需要定时发布、或者内容源来自外部(如RSS订阅、其他平台同步)的场景了。手动操作不仅效率低下,还容易出错,比如忘记定时、图片上传失败、格式错乱等。

这时候,自动化就成了刚需。市面上有一些第三方工具号称能实现自动发布,但它们要么收费不菲,要么功能受限,要么存在数据安全风险。作为一名开发者,我们更倾向于将核心流程掌握在自己手里。用 Python 配合 Playwright 这个强大的浏览器自动化框架,自己动手搭建一套自动发布系统,就成了一个既经济又可靠的选择。

这个项目的核心目标,就是模拟一个真实用户在微信公众号后台的全部操作,从登录、进入素材管理、创建新图文、编辑内容(标题、正文、封面、摘要等),到最终点击“群发”或“定时群发”,实现全流程无人值守。它不仅能解放你的双手,更能作为内容工作流中的一个关键自动化节点,与其他系统(如内容管理系统、爬虫、AI写作工具)无缝衔接。

2. 技术选型:为什么是 Python + Playwright?

在自动化领域,Python 是当之无愧的王者,生态丰富,库多易上手。而浏览器自动化工具,我们有几个常见选择:Selenium、Puppeteer 和 Playwright。

  • Selenium :老牌工具,支持多语言和多浏览器,但速度相对较慢,对现代单页应用(SPA)的支持有时不够稳定,且需要额外安装浏览器驱动。
  • Puppeteer :由 Chrome 团队开发,对 Chromium 系浏览器支持极佳,速度快,但原生只支持 JavaScript/Node.js。
  • Playwright :由微软开发,可以看作是 Puppeteer 的“精神续作”和全面升级版。它原生支持 Python、JavaScript、.NET 和 Java,并且 同时支持 Chromium、Firefox 和 WebKit 三大浏览器引擎。这意味着你可以用同一套脚本在不同浏览器上测试,兼容性更有保障。

对于微信公众号后台这种复杂的、动态加载频繁的 Web 应用,Playwright 的优势非常明显:

  1. 自动等待机制 :Playwright 在执行操作(如点击、输入)前,会自动等待元素变得可交互(可见、启用、稳定),这大大减少了编写显式等待( time.sleep )代码的需要,脚本更健壮。
  2. 强大的选择器 :除了常规的 CSS 和 XPath,Playwright 提供了像 text= has= 这样更贴近用户视角的选择器,定位元素更直观。
  3. 网络拦截与模拟 :可以轻松拦截和修改网络请求,这对于处理图片上传、模拟特定环境(如移动端)非常有用。
  4. 无头模式与有头模式 :开发调试时可以用有头模式观察浏览器行为,部署时用无头模式节省资源。
  5. Python 原生支持 :对于 Python 开发者来说,无需像使用 Puppeteer 那样通过 pyppeteer 等第三方桥接库,直接使用官方 playwright 包即可,API 设计也很 Pythonic。

因此, Python + Playwright 的组合,在开发效率、运行稳定性和跨浏览器兼容性上,为我们实现微信公众号自动发布提供了最佳的技术基础。

3. 环境准备与核心依赖安装

工欲善其事,必先利其器。开始编码前,我们需要搭建好开发环境。

3.1 Python 环境配置

确保你的系统已经安装了 Python(建议版本 3.8 或以上)。你可以通过命令行输入 python --version python3 --version 来检查。如果没有安装,可以去 Python 官网下载安装包,安装时记得勾选“Add Python to PATH”。

我个人习惯使用虚拟环境来管理项目依赖,避免污染全局环境。这里以使用 venv 为例:

# 在你的项目目录下
python -m venv venv

# 激活虚拟环境
# Windows:
venv\Scripts\activate
# macOS/Linux:
source venv/bin/activate

激活后,命令行提示符前会出现 (venv) 标识。

3.2 安装 Playwright

在激活的虚拟环境中,使用 pip 安装 Playwright 的 Python 包:

pip install playwright

安装完成后,还需要安装 Playwright 所需的浏览器内核。Playwright 提供了一个命令行工具来完成这件事:

playwright install

这个命令会下载 Chromium、Firefox 和 WebKit 的可用版本。如果你只想安装 Chromium(对于微信公众号后台,通常 Chromium 就够了),可以运行:

playwright install chromium

注意 playwright install 命令可能会因为网络问题下载缓慢或失败。如果遇到这种情况,可以尝试设置国内镜像源,或者手动下载浏览器驱动。一个实用的技巧是,先通过 playwright install --dry-run 查看需要下载的文件列表和URL,然后用下载工具手动下载并放置到 Playwright 的缓存目录中。

3.3 辅助库

除了 Playwright 本身,我们可能还需要一些辅助库来处理数据、定时任务等:

pip install python-dotenv  # 用于管理环境变量(如账号密码)
pip install schedule       # 用于简单的定时任务调度
pip install requests       # 用于可能的网络请求(如下载图片)

现在,基础环境就准备好了。我们可以创建一个新的 Python 文件,比如 wechat_auto_publish.py ,开始编写我们的自动化脚本。

4. 核心流程拆解与实现

微信公众号文章发布的完整流程,可以拆解为以下几个关键步骤,我们将逐一实现。

4.1 步骤一:模拟登录微信公众号后台

这是整个流程中最关键也最可能出问题的一步。微信公众号后台登录通常需要扫描二维码。Playwright 可以很好地处理这个流程。

核心思路

  1. 启动浏览器,打开登录页。
  2. 等待二维码图片出现。
  3. 人工扫描二维码 。这一步目前很难完全自动化绕过,因为涉及腾讯的安全验证。我们的脚本需要在这里暂停,等待用户手动扫码。
  4. 扫码成功后,检测页面跳转,确认登录成功。

代码实现与详解

import asyncio
from playwright.async_api import async_playwright
import os
from dotenv import load_dotenv

load_dotenv()  # 加载 .env 文件中的环境变量

async def login_to_wechat():
    """
    登录微信公众号后台
    返回一个已登录的浏览器上下文(context)
    """
    async with async_playwright() as p:
        # 启动浏览器,headless=False 表示显示浏览器界面,方便调试
        browser = await p.chromium.launch(headless=False, slow_mo=100) # slow_mo 让操作变慢,便于观察
        # 创建一个新的上下文,可以隔离cookie、缓存等
        context = await browser.new_context(
            viewport={'width': 1920, 'height': 1080},
            user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
        )
        page = await context.new_page()

        # 1. 打开登录页
        login_url = "https://mp.weixin.qq.com/"
        await page.goto(login_url)
        print("已打开登录页面,等待二维码...")

        # 2. 等待二维码元素出现
        # 微信公众号的二维码图片通常在一个特定的容器里
        qr_code_frame = page.frame_locator("#login_frame")
        qr_img = qr_code_frame.locator("img#js_qrcode")

        await qr_img.wait_for(state="visible")
        print("二维码已显示,请使用微信扫描二维码登录...")

        # 3. 等待登录成功(页面跳转或出现特定元素)
        # 扫码成功后,页面通常会跳转到后台首页,或者出现“进入公众号”按钮
        try:
            # 等待页面URL变成后台首页,或者出现“首页”相关的元素
            await page.wait_for_url("**/cgi-bin/home?t=home/index**", timeout=120000) # 设置2分钟超时
            # 也可以等待一个登录后才会出现的元素,比如用户头像
            # await page.wait_for_selector('img.avatar', timeout=120000)
            print("登录成功!")
        except Exception as e:
            print(f"登录等待超时或失败: {e}")
            await browser.close()
            return None

        # 登录成功,返回 context 和 browser,以便后续操作
        # 注意:需要保持 browser 和 context 不被关闭
        return browser, context, page

# 由于登录需要人工干预,我们通常单独运行一次登录,然后保存登录状态(Cookies)
async def save_login_state():
    browser, context, page = await login_to_wechat()
    if context:
        # 将登录状态的 cookies 和 localStorage 保存到文件
        storage_state = await context.storage_state()
        import json
        with open("wechat_state.json", "w") as f:
            json.dump(storage_state, f)
        print("登录状态已保存到 wechat_state.json")
        await browser.close()

if __name__ == "__main__":
    # 第一次运行,执行登录并保存状态
    asyncio.run(save_login_state())

实操心得

  • 状态持久化 :每次运行都扫码登录是不现实的。上述代码在登录成功后,将浏览器的 storage_state (包含 Cookies、LocalStorage 等)保存到了本地文件 wechat_state.json 。下次启动时,可以直接加载这个状态文件来恢复登录会话,无需再次扫码,除非登录过期(通常可以维持数天到数周)。
  • 超时处理 wait_for_url wait_for_selector 都设置了较长的超时时间(2分钟),给用户足够的扫码时间。同时要做好异常捕获,避免脚本卡死。
  • User-Agent :设置一个常见的桌面版 Chrome User-Agent,可以减少被识别为自动化脚本的风险。

4.2 步骤二:导航到素材管理并创建新图文

登录成功后,我们需要进入发布文章的功能入口。

核心思路

  1. 从首页导航到“创作管理” -> “图文素材”。
  2. 点击“新建图文”按钮。

代码实现

async def create_new_article(context, page):
    """
    从已登录的页面,创建一篇新的图文素材
    """
    # 方法1:直接跳转到素材管理页面(更稳定)
    material_url = "https://mp.weixin.qq.com/cgi-bin/appmsg?t=media/appmsg_edit_v2&action=edit&isNew=1&type=10&lang=zh_CN&token=xxxx"
    # 注意:上面的 token 是动态的,通常从首页链接中可以获取。
    # 更可靠的方法是点击页面上的链接。

    # 方法2:通过点击页面元素导航(更模拟真人操作)
    print("正在进入素材管理页面...")
    
    # 等待首页加载完成,找到“创作管理”菜单
    # 注意:公众号后台的UI可能改版,选择器需要根据实际情况调整
    await page.wait_for_selector('a:has-text("创作管理")')
    await page.click('a:has-text("创作管理")')
    
    # 等待子菜单出现并点击“图文素材”
    await page.wait_for_selector('a:has-text("图文素材")')
    await page.click('a:has-text("图文素材")')
    
    # 等待素材页面加载
    await page.wait_for_selector('div.weui-desktop-btn >> text=新建图文')
    
    print("已进入图文素材页面,点击新建图文...")
    # 点击“新建图文”按钮
    new_article_btn = page.locator('div.weui-desktop-btn:has-text("新建图文")')
    await new_article_btn.click()
    
    # 等待编辑器页面加载。编辑器的容器通常有一个特定的ID或Class
    await page.wait_for_selector('#editor', timeout=30000) # 假设编辑器容器id是#editor
    print("已成功进入图文编辑器。")
    
    return page

注意事项

  • 选择器的脆弱性 :微信公众号后台的前端结构并非一成不变,腾讯可能会进行UI改版。上面代码中的 :has-text() 选择器非常实用,它通过元素的文本来定位,比依赖固定的 CSS 类名或 ID 要稍微健壮一些,但也不是绝对可靠。你需要根据实际页面结构进行调整。Playwright 的录制工具( playwright codegen )可以帮助你快速获取元素选择器。
  • 等待策略 :在点击一个可能触发页面跳转或动态加载的按钮后,一定要使用 wait_for_selector wait_for_url 等待下一个关键页面元素出现,确保页面状态稳定后再进行后续操作。

4.3 步骤三:填充文章内容(标题、正文、封面等)

这是自动化的核心,我们需要将准备好的文章标题、正文(HTML格式)、封面图等填充到编辑器的各个输入框中。

核心思路

  1. 定位标题输入框并输入标题。
  2. 定位正文编辑器(通常是一个富文本编辑器 iframe),并注入 HTML 内容。
  3. 上传封面图片。
  4. 填写摘要、作者等信息(可选)。

代码实现

async def fill_article_content(page, article_data):
    """
    在编辑器中填充文章内容
    :param page: 当前页面对象
    :param article_data: 字典,包含 title, content_html, cover_path, digest, author 等
    """
    print("开始填充文章内容...")
    
    # 1. 输入标题
    title_input = page.locator('input[id*="title"], input[placeholder*="标题"]').first
    await title_input.fill(article_data['title'])
    print(f"已输入标题: {article_data['title']}")
    await asyncio.sleep(0.5) # 短暂等待,让UI反应
    
    # 2. 输入正文(最复杂的部分)
    # 微信公众号编辑器通常是一个 iframe 包裹的富文本编辑器(如 quill)
    editor_frame = page.frame_locator('#editor iframe').first # 定位到编辑器iframe
    if editor_frame:
        # 定位到 iframe 内的可编辑正文区域
        # 可能需要尝试不同的选择器,如 .ql-editor, [contenteditable="true"] 等
        editable_div = editor_frame.locator('[contenteditable="true"]').first
        if await editable_div.count() > 0:
            # 方法A:直接设置 innerHTML(可能不触发编辑器的事件)
            # await editable_div.evaluate('(element, html) => element.innerHTML = html', article_data['content_html'])
            
            # 方法B:更可靠的方式 - 先点击聚焦,然后模拟键盘输入(对于纯文本)或使用 execCommand(对于HTML)
            # 但对于复杂HTML,最稳妥的方式是使用 Playwright 的 set_input_files 模拟图片上传,再组合文本。
            # 这里提供一个简化思路:清空后粘贴HTML(需要编辑器支持)
            await editable_div.click()
            await page.keyboard.press('Control+A') # 全选 (Mac: Command+A)
            await page.keyboard.press('Delete') # 删除
            # 注意:直接粘贴HTML可能被编辑器过滤。更常见的做法是分解操作:
            # a. 处理文本和段落
            # b. 单独处理图片上传
            
        else:
            print("未找到可编辑的正文区域,尝试备用选择器...")
            # 尝试其他选择器...
    else:
        print("未找到编辑器iframe,正文填充可能失败。")
    
    # 3. 上传封面图片
    if article_data.get('cover_path'):
        print("正在上传封面图...")
        # 找到封面图上传的 input[type="file"] 元素
        # 通常点击“设置封面”按钮后会触发文件选择
        cover_btn = page.locator('div:has-text("设置封面"), button:has-text("封面")').first
        await cover_btn.click()
        await asyncio.sleep(1)
        
        # 等待文件选择对话框?不,Playwright可以直接设置文件input的值
        # 需要先定位到隐藏的 file input 元素
        file_input = page.locator('input[type="file"][accept*="image"]').first
        if await file_input.count() > 0:
            await file_input.set_input_files(article_data['cover_path'])
            print("封面图文件已选择。")
            # 等待上传完成和预览出现
            await page.wait_for_selector('img.preview_cover', timeout=10000)
            # 点击“确定”或“完成”按钮
            confirm_btn = page.locator('button:has-text("确定"), button:has-text("完成")').first
            await confirm_btn.click()
        else:
            print("未找到封面图上传输入框。")
    
    # 4. 填写摘要和作者(可选)
    if article_data.get('digest'):
        # 摘要通常是一个textarea
        digest_area = page.locator('textarea[placeholder*="摘要"]').first
        if await digest_area.count() > 0:
            await digest_area.fill(article_data['digest'])
    
    if article_data.get('author'):
        author_input = page.locator('input[placeholder*="作者"]').first
        if await author_input.count() > 0:
            await author_input.fill(article_data['author'])
    
    print("文章内容填充完成。")
    await asyncio.sleep(2) # 等待所有操作完成

避坑技巧

  • 正文填充的挑战 :这是整个自动发布流程中 最棘手 的部分。微信公众号的富文本编辑器会过滤或转换粘贴进去的 HTML,直接设置 innerHTML 可能无效或丢失样式。一个更稳健但更复杂的方案是:
    1. 将你的 HTML 内容解析为一系列“操作指令”(插入文本、插入图片、设置样式等)。
    2. 通过 Playwright 模拟键盘输入文本。
    3. 对于图片,找到编辑器内的图片上传按钮,使用 set_input_files 逐一上传,而不是直接插入 img 标签。
    4. 对于加粗、斜体等简单样式,可以尝试通过 page.keyboard 模拟快捷键(如 Ctrl+B)或点击编辑器工具栏按钮来实现。
    5. 终极方案 :考虑使用微信公众号提供的 草稿箱API 。这是官方推荐的接口方式,可以稳定地以JSON格式创建草稿,完全绕过前端编辑器。但这需要申请公众号的开发者权限,配置服务器,复杂度更高。对于追求稳定性的生产环境,强烈建议研究此方案。
  • 文件上传 :Playwright 的 set_input_files 方法非常强大,可以直接将本地文件路径赋给 <input type="file"> 元素,无需模拟打开文件选择对话框。
  • 等待与稳定性 :在每一个可能引起 UI 变化(如弹窗、图片上传预览)的操作后,都要添加适当的等待( wait_for_selector asyncio.sleep ),确保页面状态稳定后再进行下一步。

4.4 步骤四:设置发布选项并群发

内容填充完毕后,最后一步就是设置发布参数并点击发送。

核心思路

  1. 选择发布范围(通常为“全部用户”或按标签选择)。
  2. 选择发布形式(立即发布、定时发布)。
  3. 点击“群发”按钮,并确认二次弹窗。

代码实现

async def publish_article(page, schedule_time=None):
    """
    发布或定时发布文章
    :param page: 当前页面对象
    :param schedule_time: 可选,datetime对象,指定定时发布时间。如果为None,则立即发布。
    """
    print("准备发布文章...")
    
    # 滚动到页面底部,确保发布按钮在视图中
    await page.evaluate('window.scrollTo(0, document.body.scrollHeight)')
    await asyncio.sleep(1)
    
    # 1. 点击“群发”按钮
    # 注意:在编辑页面,可能是“发布”或“群发”
    publish_btn = page.locator('button:has-text("群发"), button:has-text("发布")').first
    await publish_btn.click()
    
    # 等待发布设置面板/弹窗出现
    await page.wait_for_selector('div.weui-desktop-dialog', timeout=10000)
    
    # 2. 选择发布范围(默认通常是“全部用户”)
    # 如果需要按标签选择,需要点击相应的单选按钮或下拉框
    # await page.click('label:has-text("按标签选择")')
    # ... 后续选择标签逻辑
    
    # 3. 选择发布形式
    if schedule_time:
        print(f"设置为定时发布: {schedule_time}")
        # 点击“定时群发”选项
        await page.click('label:has-text("定时群发")')
        await asyncio.sleep(0.5)
        # 定位日期时间输入框并填充时间
        # 这里的选择器需要根据实际弹窗的HTML结构来调整,可能非常复杂
        # 可能需要通过 page.locator('input[type="datetime-local"]') 来定位
        # 由于时间输入控件可能是一个自定义组件,直接设置值可能比较困难。
        # 一个替代思路:如果公众号后台支持,可以先用“保存为草稿”功能,然后通过“草稿箱”列表页的定时功能来设置,可能更容易定位元素。
        # 此处代码省略,因为UI操作细节多变。
        raise NotImplementedError("定时发布的具体UI操作需要根据实际页面结构实现")
    else:
        print("设置为立即发布。")
        # 确保“立即群发”选项被选中(通常是默认)
        # await page.click('label:has-text("立即群发")')
    
    # 4. 点击最终确认按钮(可能是“确定”、“群发”或“发送”)
    # 注意:这里可能有多个步骤和确认弹窗
    final_confirm_btn = page.locator('div.weui-desktop-dialog button:has-text("群发"), button:has-text("确定")').last
    await final_confirm_btn.click()
    
    # 5. 处理可能的二次确认弹窗(例如,输入验证码或管理员扫码确认)
    # 公众号对于群发有安全验证,可能需要管理员在手机上确认。
    # 脚本可以在这里等待,并提示用户手动确认。
    print("请留意微信手机客户端,可能需要管理员确认发送。")
    # 可以等待一个表示发布成功的元素出现,或者等待页面跳转
    try:
        await page.wait_for_selector('text=发布成功', timeout=180000) # 等待3分钟
        print("文章发布流程已成功提交!")
    except Exception as e:
        print(f"发布结果确认超时或未检测到成功状态: {e}")
        # 可能是需要手机确认,脚本无法自动处理

重要提醒

  • 安全验证 :微信公众号对群发操作有严格的安全限制。除了扫码登录,在最终发布时,通常还需要 管理员在手机微信上确认 。这是无法通过自动化脚本绕过的。因此,我们的脚本本质上是“半自动”的,它完成了所有前置的繁琐操作,将最终确认权留给人。你可以将脚本部署在一台长期开机的电脑或服务器上,当它运行到等待确认这一步时,你的手机会收到通知,你点一下确认即可。
  • 定时发布 :通过 UI 操作实现精准的定时发布比较困难,因为时间选择器控件可能很复杂。如果定时发布是核心需求,有两个更好的方向:1) 研究微信公众号的 草稿箱API ,它支持创建定时群发任务。2) 使用脚本在设定的时间点执行“立即发布”操作,这需要借助操作系统的定时任务(如 cron Task Scheduler )来调度你的 Python 脚本。

5. 整合与优化:构建健壮的发布脚本

将上述所有步骤整合起来,并加入错误处理、状态恢复和日志记录,形成一个完整的脚本。

5.1 主函数与流程整合

import asyncio
import json
from datetime import datetime
from playwright.async_api import async_playwright

async def auto_publish_article(article_data, schedule_time=None, use_saved_state=True):
    """
    全自动文章发布主函数
    """
    browser = None
    context = None
    page = None
    try:
        async with async_playwright() as p:
            launch_options = {"headless": False} # 调试阶段建议有头模式
            if use_saved_state:
                # 尝试加载已保存的登录状态
                try:
                    with open("wechat_state.json", "r") as f:
                        storage_state = json.load(f)
                    launch_options["storage_state"] = storage_state
                    print("检测到已保存的登录状态,尝试恢复...")
                except FileNotFoundError:
                    print("未找到保存的登录状态文件,将启动全新会话。")
                    # 这里可以调用 login_to_wechat() 并保存状态
                    # 为了流程完整,我们先假设需要登录
                    pass
            
            browser = await p.chromium.launch(**launch_options)
            context = await browser.new_context(viewport={'width': 1920, 'height': 1080})
            page = await context.new_page()
            
            # 如果加载了状态,直接打开后台首页
            if use_saved_state and launch_options.get("storage_state"):
                await page.goto("https://mp.weixin.qq.com/")
                # 检查是否仍在登录状态
                try:
                    await page.wait_for_selector('text=首页', timeout=10000)
                    print("状态恢复成功,已登录。")
                except:
                    print("保存的状态可能已过期,需要重新登录。")
                    # 状态失效,需要重新登录
                    await page.close()
                    await context.close()
                    await browser.close()
                    # 递归调用,但不使用保存的状态
                    return await auto_publish_article(article_data, schedule_time, use_saved_state=False)
            else:
                # 执行登录流程
                print("需要重新登录...")
                # 这里应嵌入 login_to_wechat 的逻辑,并保存新状态
                # 为简化示例,我们假设登录成功并保存了状态
                raise NotImplementedError("完整的登录流程请参考前面章节的 login_to_wechat 函数")
            
            # 步骤二:创建新图文
            page = await create_new_article(context, page)
            
            # 步骤三:填充内容
            await fill_article_content(page, article_data)
            
            # 步骤四:发布
            await publish_article(page, schedule_time)
            
            print("自动化发布流程执行完毕。")
            
    except Exception as e:
        print(f"自动化流程执行过程中出现严重错误: {e}")
        import traceback
        traceback.print_exc()
    finally:
        # 可选:关闭浏览器,或者保持打开用于调试
        # if browser:
        #     await browser.close()
        pass

# 示例文章数据
sample_article = {
    'title': '【自动化测试】用Playwright解放你的双手',
    'content_html': '<p>这是一篇由Python脚本自动发布的测试文章。</p><p>正文内容在这里...</p>',
    'cover_path': '/path/to/your/cover_image.jpg',
    'digest': '本文介绍了如何使用Playwright实现微信公众号自动化发布。',
    'author': '你的名字'
}

# 运行脚本
if __name__ == "__main__":
    # 立即发布
    asyncio.run(auto_publish_article(sample_article))
    
    # 定时发布(需要完善UI操作)
    # schedule_time = datetime(2024, 12, 25, 20, 0, 0) # 2024年圣诞节晚上8点
    # asyncio.run(auto_publish_article(sample_article, schedule_time))

5.2 错误处理与重试机制

网络波动、页面加载慢、元素定位失败都可能导致脚本中断。一个健壮的脚本必须有良好的错误处理和重试机制。

from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type

# 使用 tenacity 库实现重试
@retry(
    stop=stop_after_attempt(3), # 最多重试3次
    wait=wait_exponential(multiplier=1, min=2, max=10), # 指数退避等待
    retry=retry_if_exception_type((TimeoutError, )) # 仅对超时错误重试
)
async def robust_click(page, selector, timeout=30000):
    """一个带重试的点击函数"""
    try:
        element = page.locator(selector)
        await element.wait_for(state='visible', timeout=timeout)
        await element.click()
        return True
    except Exception as e:
        print(f"点击元素 {selector} 失败: {e}")
        raise  # 抛出异常以便 tenacity 捕获并重试

# 在主流程中,对关键操作使用 robust_click 代替普通的 page.click()

5.3 日志记录

详细的日志对于调试和监控脚本运行状态至关重要。可以使用 Python 内置的 logging 模块。

import logging

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('wechat_auto_publish.log'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

# 在代码中用 logger.info() 代替 print()
logger.info("已打开登录页面,等待二维码...")

6. 部署与进阶思考

6.1 如何部署到服务器?

脚本开发完成后,可以部署到云服务器(如 Linux 系统)上长期运行。

  1. 安装依赖 :在服务器上安装 Python、Playwright 及浏览器。
  2. 解决无头模式问题 :服务器通常没有图形界面,需要以 headless=True 模式运行。确保所有操作在无头模式下也能正常工作(例如,二维码登录不可用,必须依赖已保存的登录状态 storage_state )。
  3. 状态文件管理 :首次部署需要在有图形界面的环境中运行一次登录脚本,生成 wechat_state.json 文件,然后将其上传到服务器。需要定期检查该状态是否过期。
  4. 使用定时任务 :使用 cron (Linux)或 Task Scheduler (Windows)来定时执行你的 Python 脚本。例如,每天上午9点运行:
    # Linux crontab
    0 9 * * * cd /path/to/your/script && /usr/bin/python3 wechat_auto_publish.py
    
  5. 进程管理 :使用 systemd supervisor 来管理脚本进程,确保脚本意外退出后能自动重启。

6.2 更优方案:结合官方草稿箱API

如前所述,通过 UI 自动化始终存在不稳定因素(前端改版)。对于追求极高稳定性的生产环境, 强烈建议使用微信公众号的官方 API

  • 优势 :稳定、高效、无需模拟浏览器、支持定时发布。
  • 步骤
    1. 成为公众号开发者,配置服务器地址、Token等。
    2. 获取 access_token
    3. 调用 新增草稿 接口,以 JSON 格式上传图文素材。
    4. 调用 发布接口 将草稿发布出去(支持定时)。
  • 挑战 :需要一台有公网 IP 的服务器来接收微信服务器的消息,配置流程比本地脚本复杂。

你可以设计一个混合方案:日常发布使用稳定的 API,而当 API 调用出现问题时,或者需要处理一些 API 不支持的边缘操作时,再用 Playwright 脚本作为备用方案。

6.3 扩展可能性

这个自动化脚本可以作为一个核心模块,嵌入到更大的内容工作流中:

  • 与内容管理系统(CMS)集成 :从你的 WordPress、Ghost 或其他 CMS 中自动拉取最新文章并发布。
  • 与爬虫结合 :定时爬取特定网站(如新闻、博客)的内容,经过简单处理后自动发布到公众号。
  • 与AI结合 :接入 AI 写作工具(如 ChatGPT API),根据关键词自动生成文章初稿,再由人工审核或直接发布。
  • 多账号管理 :同时管理多个公众号的发布任务。

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

在实际操作中,你肯定会遇到各种各样的问题。下面是我踩过的一些坑和解决办法。

问题1:Playwright 提示 “Target closed” 或 “Execution context was destroyed”。

  • 原因 :通常是因为页面在操作完成前被意外导航或关闭了,或者浏览器上下文(context)被提前清理。
  • 解决 :检查你的代码逻辑,确保在操作一个页面元素时,该页面处于稳定状态(没有正在进行的跳转)。避免在 async with 块外部使用 page 对象。确保 browser context 的生命周期覆盖了整个操作流程。

问题2:元素定位失败,即使选择器在开发者工具里看起来是对的。

  • 原因
    • 页面有iframe :目标元素在 iframe 内,你需要先用 page.frame_locator() 定位到 iframe。
    • 动态加载 :元素是 JavaScript 动态生成的,你的代码执行太快,元素还没出现。必须使用 wait_for_selector
    • 选择器不唯一 :可能有多个元素匹配你的选择器,使用 .first .last .nth(index) 来指定。
    • 页面结构已更新 :微信公众号后台UI改版了。
  • 解决
    • 使用 Playwright 的录制工具 ( playwright codegen ) 重新生成选择器。
    • 使用更稳健的选择器,如 :has-text() 结合其他属性。
    • 增加等待时间,或使用 wait_for_selector state 参数(如 'visible' , 'attached' )。

问题3:登录状态(storage_state)很快失效。

  • 原因 :微信公众号的登录会话有有效期,也可能因为异地登录、安全策略等原因提前失效。
  • 解决
    • 定期(如每周)手动更新一次 wechat_state.json 文件。
    • 编写一个“状态检测”函数,在每次执行主任务前先访问首页,检查是否仍在登录状态,如果失效则触发重新登录流程(可能需要人工扫码)。

问题4:上传封面图或正文图片失败。

  • 原因 :文件路径错误,或者文件输入框没有正确定位到。
  • 解决
    • 使用绝对路径。
    • 确保 set_input_files 定位到的是真正的 <input type="file"> 元素。有时上传组件是自定义的,需要先点击一个按钮触发隐藏的 input 元素出现。
    • 可以先用 page.screenshot() 在操作前后截图,帮助调试。

问题5:脚本在服务器无头模式下运行失败,但在本地有头模式成功。

  • 原因 :无头模式下的某些行为(如视口大小、资源加载)可能与有头模式不同。
  • 解决
    • 在无头模式下也设置一个合理的视口大小 viewport
    • 增加 slow_mo 参数(虽然会变慢,但能减少时序问题)。
    • 在关键步骤后添加 page.wait_for_load_state('networkidle') 等待网络空闲。
    • 查看无头模式下的日志和截图( playwright 启动时设置 dumpio=True 可以输出浏览器日志)。

最后,记住自动化微信公众号发布是一个与官方前端界面“博弈”的过程。它的稳定性是相对的。保持脚本的简洁,对关键步骤做好异常捕获和日志记录,并准备一个手动发布的备用方案,是保证你内容运营不断更的关键。这个项目最大的价值不在于实现完全无人值守,而在于将你从重复、繁琐的页面操作中解放出来,让你能更专注于内容创作本身。

更多推荐