我在运营一个今日头条的情感类账号。这个活儿的特点是:操作极简单,但频次高、必须每天做,而且全在网页后台点鼠标。
每天要做的事大概这几件:
这些动作加起来一天二十分钟,但有个致命问题:它必须人肉盯着。 头条号后台没有给普通创作者的开放接口,抓包拿到的接口要带一堆动态签名和 cookie,为这点事去逆向不值得。
我最初的想法是让 WorkBuddy 直接帮我操作浏览器。试了之后发现两条路都不通:
最后走通的方案是第三条:在已经登录好的真实 Chrome 里,用物理鼠标键盘去点。 由 WorkBuddy 负责「看」和「判断」,一段几十行的 Python 负责「动手」。
这样做的最大好处是:在平台眼里,这就是我本人在操作。 没有新设备、没有新环境、没有异常请求特征。
方案 | 登录态 | 风控风险 | 维护成本 | 适合场景 |
|---|---|---|---|---|
官方 API / 开放平台 | 需申请 | 无 | 低 | 平台开放了接口 |
requests 逆向接口 | 需自己维护 | 中高 | 高 | 接口简单稳定 |
Playwright / Selenium | 需自己存 cookie | 中(自动化指纹明显) | 中 | 批量、可无人值守 |
GUI 自动化(本方案) | 复用本人已登录窗口 | 低 | 低 | 低频、高价值、老后台 |
关键判断标准只有一条:这件事一天只做几次,但必须做对。 一天几十次的任务,GUI 自动化的速度是瓶颈;一天三五次的任务,它几乎是最优解。
pyautogui、pyperclip、pillowpip install pyautogui pyperclip pillow我在项目根目录定死了一套约定,并且写进了 WorkBuddy 的工作区记忆里,让它每次执行都遵守:
D:\头条号运营\
├── 01_头像素材\ # 图片素材
├── 02_内容文案\ # 选题、成稿
├── 03_数据复盘\ # 数据导出、复盘表
├── 04_投稿\ # 对外发布的稿子
├── _tools\ # 自动化脚本(wb.py 等)
└── _cache\ # 截图临时目录,用完即删这一步看着无关紧要,其实是整套东西能稳定跑下去的基础:脚本产出的截图、日志、记录,必须落在固定位置,否则 WorkBuddy 下次执行时找不到上下文,就会自己乱猜。
我没有每次都把操作步骤重新讲一遍,而是在工作区记忆里写成了硬规则,例如:
AI 声明不在「预览并发布」弹窗里,而在编辑器底部「共 N 字 | 发文设置 ⌄」→ 展开面板最下方「作品声明」→ 勾选「引用AI」 浏览器窗口不够宽时,底栏左侧控件会被右侧面板挤到窗外,按 F11 全屏 不要用缩放后的截图目测坐标,改用像素颜色扫描算中心点 发布后列表不刷新,必须重新导航到列表页才会出现新条目
这几条规则的价值,比代码本身大。 因为它们是我踩坑踩出来的,写进去之后,WorkBuddy 下一次执行就不会在同一个地方再摔一次。
整件事的核心不是「让 AI 会点鼠标」,而是给 AI 一套稳定的感官和手。这套东西我抽成了 wb.py:
"""GUI helpers: 截图 + 鼠标键盘控制。所有截图落在 _cache/"""
import ctypes
import os
import time
# 关键:声明 DPI 感知,否则 Windows 会做缩放,坐标全错
try:
ctypes.windll.shcore.SetProcessDpiAwareness(2)
except Exception:
pass
import pyautogui
import pyperclip
from PIL import ImageGrab
pyautogui.FAILSAFE = True # 鼠标甩到左上角可紧急中止
pyautogui.PAUSE = 0.35
CACHE = r"D:\头条号运营\_cache"
os.makedirs(CACHE, exist_ok=True)
def shot(name="s.png", box=None, scale=1.0):
"""截全屏;box 传裁剪区域;scale 放大便于读数"""
img = ImageGrab.grab(all_screens=True)
if box:
img = img.crop(box)
if scale != 1.0:
w, h = img.size
img = img.resize((int(w * scale), int(h * scale)))
path = os.path.join(CACHE, name)
img.save(path)
print(path, img.size)
return path
def click(x, y, pause=0.6):
pyautogui.click(int(x), int(y))
time.sleep(pause)
def paste(text):
"""中文必须走剪贴板,pyautogui.typewrite 输不了中文"""
pyperclip.copy(text)
time.sleep(0.2)
pyautogui.hotkey("ctrl", "v")
time.sleep(0.5)
def hot(key):
pyautogui.hotkey(*key.split("+"))
time.sleep(0.5)
def focus(title_kw="Chrome"):
"""按标题关键字把窗口提到前台(比 pyautogui 的窗口操作可靠)"""
u = ctypes.windll.user32
found = []
def cb(h, l):
if u.IsWindowVisible(h):
n = u.GetWindowTextLengthW(h)
if n:
b = ctypes.create_unicode_buffer(n + 1)
u.GetWindowTextW(h, b, n + 1)
if title_kw in b.value:
found.append(h)
return True
cb_t = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_void_p)
u.EnumWindows(cb_t(cb), 0)
if not found:
print("not found")
return False
h = found[0]
u.ShowWindow(h, 3) # 3 = 最大化
try:
u.SetForegroundWindow(h)
except Exception:
pass
time.sleep(1.2)
return True三个要点:
SetProcessDpiAwareness(2) 必须写在 pyautogui 导入之前。不写,Windows 会把坐标按 150% 缩放再交给你,你会发现自己点的位置永远比看到的位置偏一点。pyautogui.typewrite() 只认 ASCII,直接输入中文会丢字或者乱码。focus() 用 win32 的 EnumWindows 按窗口标题找,比 pygetwindow 稳,因为它能处理标题被截断、窗口被最小化的情况。这是最容易出错的一件事,因为AI 声明不在你以为的地方。
第一版我让 WorkBuddy 去「预览并发布」弹窗里找,它找了半天没找到,最后在编辑器底部发现入口:
编辑器底部 →「共 1234 字 | 发文设置 ⌄」→ 展开 → 最下方「作品声明」→ 勾选「引用AI」
定位这个控件的代码:
import wb
wb.focus("Chrome")
wb.hot("f11") # 全屏,否则底栏被右侧面板挤到窗外
wb.hot("ctrl+l")
wb.paste("https://mp.toutiao.com/profile_v4/graphic/publish")
wb.hot("Return")注意右侧那个 头条创作助手 面板——它是隐形杀手。窗口不够宽时,它会把编辑器的底栏往左挤,导致「发文设置」「封面」「作品声明」全都跑到可视区外面。你在截图里看不到它们,会以为页面没有这些功能。按 F11 全屏之后它们才会出现。
微头条列表里,每条作品下面有一行字:
展现 4 · 阅读 0 · 点赞 0 · 评论 0
这行字在 4K 屏上只有 30 像素高,字宽不到 10 像素。直接截图给模型看,它会认错。正确做法是裁剪 + 放大:
import wb
wb.focus("Chrome")
wb.hot("ctrl+l")
wb.paste("https://mp.toutiao.com/profile_v4/weitoutiao")
wb.hot("Return")
import time; time.sleep(6)
wb.hot("f5") # 刷新一次,避免读到缓存
time.sleep(5)
# 只裁那一行,放大 6 倍
wb.shot("read.png", box=(1010, 468, 1420, 498), scale=6.0)放大 6 倍之后,4 / 0 / 0 / 0 就清清楚楚了。这一步的小技巧是:先截全屏,看整体布局;再按物理像素坐标裁局部;最后按需放大读数。 三张图,成本最低。
删除路径是「更多」→「删除作品」→ 弹窗确认。这里有个必须做对的判断:
from PIL import ImageGrab
im = ImageGrab.grab(all_screens=True).convert("RGB")
px = im.load()
xs, ys = [], []
for y in range(520, 730):
for x in range(1450, 2010):
r, g, b = px[x, y]
if r > 200 and g < 110 and b < 110: # 玫瑰红 #F04141
xs.append(x); ys.append(y)
if xs:
cx, cy = (min(xs) + max(xs)) // 2, (min(ys) + max(ys)) // 2
wb.click(cx, cy)不要用「截图里目测的位置乘以缩放比」去算坐标。 我在这上面浪费了大概两个小时。原因见下一节第一个坑。
这是最坑的一个,没有之一。
我的屏幕是 3440×1440。shot() 截出来的 PNG 就是 3440×1440 物理像素。但你在对话里预览这张图时,它被渲染成 1080 宽——缩放比 3.16 倍。
于是:你在预览里看到一个按钮在 (400, 200),你以为真实坐标是 (400, 200),实际是 (1266, 633)。点下去,什么都没发生,然后你开始怀疑人生。
正确做法只有两个:
wb.shot(box=(1010, 468, 1420, 498), scale=6.0),然后只看裁剪结果,不再换算。同一个按钮,在不同状态下位置会变(列表少一条,下面所有按钮上移 116 像素)。
稳定的定位方式是扫像素:
def find_rect(box, pred):
"""在 box 里找出满足 pred 的像素包围盒,返回中心点"""
from PIL import ImageGrab
im = ImageGrab.grab(all_screens=True).convert("RGB")
px = im.load()
xs, ys = [], []
for y in range(box[1], box[3]):
for x in range(box[0], box[2]):
if pred(*px[x, y]):
xs.append(x); ys.append(y)
if not xs:
return None
return ((min(xs) + max(xs)) // 2, (min(ys) + max(ys)) // 2)
# 玫瑰红的「确定」按钮
pos = find_rect((1450, 520, 2010, 730), lambda r, g, b: r > 200 and g < 110 and b < 110)但要注意:蓝色链接文字也会被「蓝色按钮」的判定误伤。我判过一次「打开 WorkBuddy」的按钮,结果定位到了旁边那句蓝色 URL 上,白点了一下。所以谓词要收紧,或者先聚簇再取最大连通块。
在网页上点「去完成」,如果它要唤起本地客户端,Chrome 会弹一个拦截条:
要打开 WorkBuddy 吗? https://www.workbuddy.cn 想打开此应用
这个弹窗是模态的,会挡掉整页的点击。 后续所有 click() 都失效,而且你不会收到任何报错——脚本照跑,只是什么都没发生。
解决:勾上「始终允许」,然后点「打开」。一次搞定,以后不再弹。
点完「确认发布」,页面回到列表,列表还是旧的——看不到新文章,草稿箱计数已经清零了。
我第一次以为发布失败,又发了一遍,然后账号里出现了两篇一样的稿子。
正确判断是:重新导航到列表页 URL,新条目才会出现。所以判定发布成功的条件是:
草稿消失 + 刷新后列表出现新条目 = 发布成功
全屏是为了让底栏控件进可视区。但脚本跑完要退出全屏,否则用户回来发现浏览器莫名全屏了,会以为出故障。养成配对习惯。
pyautogui.typewrite("测试") 会静默失败。pyperclip.copy() + ctrl+v 才是正解。另外粘贴前要先 click() 把焦点落进输入框,并且截图确认光标真的在里面——我遇到过一次点击落空,粘贴的内容进了页面搜索框。
在 Windows 上写 .ps1/.bat 带中文路径,编码会把文件名搞乱。要么全部用 execute_command 直接调用,要么路径里避不开中文时用 Python 处理(Python 3 默认 UTF-8,稳)。
跑通之后,这套东西的实际收益是:
事项 | 之前 | 现在 |
|---|---|---|
发一篇图文(含勾 AI 声明) | 8—10 分钟 | 2 分钟,我只需要审稿 |
每日读 4 个数据指标 | 3 分钟 | 40 秒,且自动写进复盘表 |
删一条作品 | 1 分钟 | 30 秒 |
判断「账号有没有被限流」 | 靠猜 | 有数据,有记录,能对比 |
更有价值的是它变成了一个可复用的判据。举个真实例子:
新号发了三篇图文,全部「展现 0 · 阅读 0」。我第一反应是内容有问题,准备改稿。但让脚本去读一条几十字的微头条探针,40 分钟后拿到 展现 4 / 阅读 0——说明推荐通道是通的,问题在图文本身,不在账号。如果没有这套自动读数,我会误判方向,然后在错误的地方改一整周。
说说我踩过的反例,免得你走弯路:
这套东西能跑起来,靠的不是「AI 会点鼠标」这件事本身——那部分代码只有 70 行,谁都能写。
真正难的是:AI 不知道「引用AI」的勾选框藏在编辑器底部,不知道发布后列表不会刷新,不知道 4K 屏上截图会大 3.16 倍。 这些全都要靠人踩一遍,然后写进它的记忆里。
所以我现在的用法是:我负责踩坑和定规则,它负责每天重复执行。 踩坑只能踩一次,后面的活儿都交给它。
如果你也有一个「天天要点、但没有 API」的老后台,这套思路可以直接搬。真正要改的只有两处:wb.py 不用动,动的是你踩出来的那几条规则。
本文所有代码均来自实际运行中的脚本,非示意代码。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。