我 60 岁,退休了,电脑水平一般,属于「会用不会写」那一档。代码是让 AI 写的,我只负责把需求说清楚,然后把跑不通的地方骂回去让它改。
去年开始用 WorkBuddy,用着用着上了瘾,开始在腾讯云开发者社区写文章。写得多了就发现一件很烦的事:写文章一小时,发文章十分钟,这十分钟全是机械劳动。
有多机械?我数过,手动发一篇要点的位置十几处:切到 Markdown 编辑器、填标题、粘正文、一个一个敲标签、填关键词、往下拉到底、关掉那个「自动给关键词加超链接」的开关、最后点确认发布。中间漏一步,发出来就是残次品。
我这人手慢眼也花,连着干几天就烦了。跟 AI 说了一句:这活能不能让机器干。
然后就掉坑里了。这篇是完整踩坑记录,代码都是我本机真跑过的。
动手之前,AI 给我列了三条路。我挨个试,前两条都有硬伤,最后走通的是第三条。
坐标点击:算出按钮在屏幕上的像素位置,直接点过去。问题是窗口一挪位置就全废。我这台笔记本外接显示器,窗口位置天天变,这条路第一个排除。
截图识别(OCR):先截一张图,再认字找按钮。能扛住窗口移动,但得额外装识别模型,跑起来也慢。更要命的是它只告诉你「这儿有这几个字」,不知道那个开关现在是开着还是关着。
UI Automation(简称 UIA):直接读 Windows 的控件树。这套接口本来是给读屏软件用的——屏幕阅读器要把界面念给视障用户听,就得知道每个控件是什么。既然它能把界面「讲」出来,那它肯定知道每个控件叫什么、在哪、现在什么状态。
关键的发现在这儿:浏览器会把网页内容也映射成控件树。 也就是说,网页上的按钮和输入框,在系统眼里跟桌面软件的按钮是一回事,一样有名字、有类型、有坐标、有状态。既然是网页,那就是浏览器说了算,跟网页用什么框架打包没关系。
三种方案摆一起看:
方案 | 靠什么定位 | 窗口挪位置 | 页面改版 | 能不能读到状态 |
|---|---|---|---|---|
UIA 控件树 | 控件的 Name / 类型 / 坐标 | 不受影响 | 按文字找,改版影响小 | 能,开关的开合、输入框的值都能读 |
坐标点击 | 写死的像素坐标 | 一挪就废 | 一改就废 | 不能 |
OCR 截图识别 | 截图里认字 | 不受影响 | 影响小 | 不能,还要额外装模型 |
一句话:能用 UIA 就别用坐标点击。 坐标点击是「我猜那儿有个按钮」,UIA 是「它告诉我它叫什么、在哪、现在什么状态」。
就一个库,Python 自带的 pip 装上就行:
pip install uiautomation我本机装的是 2.0.29,Windows 10/11 都能跑,不需要管理员权限。打开你要自动化的页面(我用的是 Edge),保持窗口可见,别最小化——UIA 要拿真实控件,最小化的窗口控件是不渲染的。
所有脚本开头都是同一个动作:遍历桌面根控件,按窗口标题找到目标窗口。
import uiautomation as auto
def find_edge():
for w in auto.GetRootControl().GetChildren():
try:
nm = w.Name or ''
if ('写文章' in nm or '编辑文章' in nm) and 'Edge' in nm:
return w
except Exception:
pass
return None
win = find_edge()
win.SetActive() # 提到前台注意 'Edge' in nm:Edge 的窗口标题末尾会带「- Microsoft Edge」,用它把 Edge 和其他浏览器区分开。找到窗口后一定要 SetActive(),否则后面的点击可能落在别的窗口上。
抓到窗口,按理说该遍历控件树找按钮了。我一开始就是这么干的,然后撞墙:
这个页面的控件树太大,WalkControl 遍历会卡死或者超时,EditControl(foundIndex=i) 按序号取也很不稳定,同一个输入框今天排第 3 明天排第 7。页面上那些按钮还经常被包在 GroupControl 里,ButtonControl(Name='确认发布') 直接搜搜不到。
我卡了半天,最后换个思路:不去遍历树,改成拿坐标反查控件。
spots = [("发布按钮区", 1638, 110), ("标题框中心", 960, 225), ("预览区", 1300, 400)]
for label, x, y in spots:
c = auto.ControlFromPoint(x, y)
if c:
print(f"{label} -> {c.ControlTypeName} {(c.Name or '')[:40]!r}")再配合一个粗网格(横向 150 像素、纵向 80 像素扫一遍),把每格是什么类型打出来,一屏就把页面结构摸清了:
for y in range(120, 1000, 80):
row = []
for x in range(150, 1850, 150):
c = auto.ControlFromPoint(x, y)
mark = {'EditControl': 'E', 'ButtonControl': 'B',
'DocumentControl': 'D', 'TextControl': 'T'}.get(
c.ControlTypeName if c else '', '.')
row.append(f" {mark} ")
print(f"y={y:4d} |" + "".join(row))E 是输入框、B 是按钮、D 是正文区、T 是文字。跑一遍,哪块是标题框、哪块是正文、标签框在哪个角落,全出来了。这一步是整件事的地基,别省。
(此处配图:网格探测脚本的输出截图,能看出页面控件的分布)
定位到标题框(EditControl),先试 SetValue,失败再退回 SendKeys 打字:
try:
box.GetValuePattern().SetValue(TITLE)
print("SetValue 写入")
except Exception as e:
box.Click(simulateMove=False)
box.SendKeys(TITLE)
print("SendKeys 兜底")坑在这儿:写完之后,控件自己的 Name 很可能还是空的,你从 UIA 属性读不出刚写进去的字。 所以必须回读校验:
box2 = <重新用坐标定位标题框>
cur = (box2.Name or '').strip()
print("回读:", repr(cur[:60]), "一致:", cur == TITLE)这一步救过我。有次标题里带全角引号,粘进去变成乱码,就是回读对不上才发现的。凡是写进去的东西,都要读回来对一遍,别信「写入成功」这四个字。
标签是最容易出错的地方。社区标签框是输入后弹下拉,下拉里可能有一堆长得像的选项。我给自己定的规矩是:没有完全一致的一项,就按 Esc 取消,绝不点「看着像」的。
box.Click(simulateMove=False)
box.SendKeys(tag)
time.sleep(2.2) # 等下拉弹出来
items = [] # 扫输入框下方的选项
for y in range(b.bottom + 5, min(b.bottom + 260, 1040), 12):
for x in range(b.left, b.right + 30, 30):
c = auto.ControlFromPoint(x, y)
nm = (c.Name or '').strip() if c else ''
if nm and nm not in [i[0] for i in items] and len(nm) < 40:
items.append((nm, c, x, y))
exact = [i for i in items if i[0] == tag] # 严格相等
if exact:
_, _, x, y = exact[0]
auto.Click(x, y)
else:
box.SendKeys('{Escape}') # 宁可不填,也不填错我第一版偷懒用了「包含就算匹配」,结果「Python」匹配到了「Python 进阶」这种标签,发出去的文章标签全歪。改成 == 之后一次都没错过。
正文是 Markdown 纯文本,用剪贴板搬运。别用 pyperclip 之类的库,ctypes 直接调 Windows API 更省事,也不怕没权限装包:
import ctypes, time
CF_UNICODETEXT = 13
GMEM_MOVEABLE = 0x0002
def set_clipboard(text):
k, u = ctypes.windll.kernel32, ctypes.windll.user32
for _ in range(5):
if u.OpenClipboard(0):
break
time.sleep(0.2)
u.EmptyClipboard()
h = k.GlobalAlloc(GMEM_MOVEABLE, (len(text) + 1) * 2)
ptr = k.GlobalLock(h)
(ctypes.c_wchar * (len(text) + 1)).from_address(ptr).value = text
k.GlobalUnlock(h)
u.SetClipboardData(CF_UNICODETEXT, h)
u.CloseClipboard()OpenClipboard 我加了 5 次重试,因为剪贴板是全局资源,被别的程序占着就会失败。
坑一:粘贴位置不确定。 编辑器分左右两栏(源码区、预览区),点错栏就粘到空气里。我的办法是准备几个候选坐标,挨个试,粘完读页面字数判断成没成功:
for (x, y) in [(500, 500), (400, 420), (600, 600), (300, 350)]:
set_clipboard(body)
auto.Click(x, y); time.sleep(1)
auto.SendKeys('{Ctrl}a'); time.sleep(0.3)
auto.SendKeys('{Ctrl}v'); time.sleep(3.5)
wc = get_wordcount(page_text(win)) # 从页面文本解析「字数: N」
if wc and wc > 5000:
print("正文已写入"); break
print("未写入,换下一个位置")坑二:这个坑跟股票软件有关,挺离谱的。 我电脑上装着同花顺,它在后台实时监控剪贴板——你一复制,它就弹个「股票复制识别」的窗口抢焦点,粘贴动作当场被打断。我头两回粘贴失败,查了半天才反应过来是这个东西在捣乱。
解决办法有两个:一是自动化跑之前先把同花顺退出;二是去同花顺设置里关掉「复制识别」。如果你也在跑自动化,先检查一遍电脑上有没有这类监控剪贴板的软件(一些翻译软件、截图工具也会干这事)。
发布按钮在页面很下面,不在可视区里的控件,点了是没反应的。所以要先滚,边滚边看按钮的位置,滚进屏幕再点:
for i in range(8):
b = btn.BoundingRectangle
cy = (b.top + b.bottom) // 2
if cy <= 1030: # 1030 是我屏幕的可视下边界
break
auto.SetCursorPos(1693, 600)
auto.WheelDown(wheelTimes=3)
time.sleep(0.9)
btn = find_btn(win, '确认发布') # 滚动后要重新找,句柄会失效点发布之前,我还会复查一遍那个「自动给关键词加超链接」的开关。这开关默认可能是开的,一开它就把正文里的某些词自动变成广告链接,很招人烦。用 UIA 读它的状态最靠谱:
sw = win.CheckBoxControl(Name='开启产品关键词自动添加超链接', searchDepth=40)
state = sw.GetTogglePattern().ToggleState # 0=关 1=开
if state == 1:
sw.Click(simulateMove=False)这就是 UIA 比坐标点击强的地方:它能告诉你开关现在是开着还是关着。 坐标点击只能闭着眼点一下,天知道点成了什么。
点完之后别急着走,把页面文本读回来验证结果:
doc = get_doc(win)
t = doc.GetTextPattern().DocumentRange.GetText(-1)
for kw in ['发布成功', '审核中', '提交成功', '驳回', '格式有误']:
if kw in t:
print("页面含:", kw)注意:不要用控件的 Name 属性判断页面内容,Name 经常是空的老数据。 只有 TextPattern.DocumentRange.GetText(-1) 拿到的才是页面上的真实文本。我在这上面吃过亏,明明内容已经变了,Name 还显示旧的。
内容发到公网就收不回来了,所以我给自己加了一道岗:发布前用脚本扫一遍正文,查四件事。
import re
t = open(正文路径, encoding='utf-8').read()
# 1. 字数够不够
print("中文字数:", len(re.findall(r'[\u4e00-\u9fff]', t)))
# 2. 代码块围栏配对、语言标识对不对
fences = re.findall(r'^```(\w*)\s*$', t, re.M)
open_f = [f for i, f in enumerate(fences) if i % 2 == 0]
print("围栏总数:", len(fences), "(应为偶数)")
print("开启围栏:", set(open_f), "里有空/错的就要修")
# 3. 有没有泄露隐私
for pat in [r'[A-Za-z]:\\+Users\\+([^\\\s"\']+)', # 系统用户名路径
r'(?<!\d)1[3-9]\d{9}(?!\d)', # 手机号
r'[\w.+-]+@[\w-]+\.[\w.]+', # 邮箱
r'eyJ[A-Za-z0-9_\-]{20,}']: # JWT
for m in re.finditer(pat, t):
print("⚠ 疑似敏感:", m.group()[:8] + "...")
# 4. 结尾格式对不对
last = [l for l in t.splitlines() if l.strip()][-1]
print("结尾行:", repr(last))第 3 条不是形式主义。我有一次贴代码,差点把本机的访问令牌带出去——那东西等同于账号密码,被人拿到就能花你的积分、看你的会话。从那以后,凡是要发到公网的东西,我一律先脚本扫一遍,不靠肉眼。
顺带一句:代码块里的旧实现也要查。我有回正文改干净了,但代码块里还留着一个「只露前 15 后 8」的旧函数,等于在教读者一个错误的脱敏方法。正则扫不到这种,只能人工把代码块通读一遍。
坑 | 现象 | 解法 |
|---|---|---|
控件树遍历失效 |
| 改用 |
按钮被 Group 包住 | 按 Name 直接搜搜不到 | 用 |
控件不在可视区 | 点了没反应,也不报错 | 先 |
写进去读不出来 | 控件 Name 是空的 | 写入后重新定位并回读;页面文本必须用 |
下拉选项选错 | 「Python」选成了「Python 进阶」 | 严格 |
剪贴板被抢 | 粘贴无效果 |
|
窗口不在前台 | 点击落到别的窗口 | 操作前 |
开关状态未知 | 不知道自动超链接开没开 | 用 |
敏感信息外泄 | 令牌、手机号、系统用户名进正文 | 发布前脚本扫 + 人工通读代码块 |
说点实在的。
手动发一篇,我数过要点的地方十几处,中间还要反复核对标签有没有选对、开关有没有关、正文粘全了没有。我这手速,一遍下来不快,还容易漏。
拆成脚本之后,每个环节单独跑,跑完立刻打印结果:标题写进去了没有、标签选中了几个、正文字数多少、开关现在是开是关。出错了能一秒定位到是哪一步,不用从头再来一遍。 这个价值比省下来的那几分钟大得多。
更省事的是可复用。填标签、粘正文、点发布这几段,换个文章换个标题直接跑,代码不用改。AI 帮我把这些拆成独立脚本之后,我发一篇基本就是看着它跑完,然后瞄一眼最后的验证输出。
回过头看,这件事真正值的不是那几段代码,是两个判断:
第一,先摸清底细再动手。 整个过程中最花时间的不是写代码,是那一步网格探测——把页面上哪块是标题框、哪块是正文、标签框藏在哪搞清楚。我一开始跳过这步,闷头写定位代码,白折腾了两个多小时。磨刀不误砍柴工,这话在自动化上特别灵。
第二,自动化脚本的价值不在「替你点」,在「告诉你点成了没有」。 填完回读、粘完查字数、点之前读开关状态、点之后验证结果——这些校验代码比点击代码重要得多。没有校验的自动化,等于闭着眼睛开车。
我 60 岁,代码一行不会写。但我会提需求、会验证、会骂人。有 AI 帮我写,有脚本替我查,这活就干得下来了。
如果你也要自动化某个网页,我的建议是:先花十分钟做网格探测,把页面结构摸清楚,再动手写。 磨刀不误砍柴工,我在这上面省了至少两个小时的瞎试。
#WorkBuddy#
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。