大家好,我是小小明。
来自杨秀璋老师的灵魂拷问:弱弱问句,这个库爬取数据,微信每日定时发送新闻也可以的吧?
杨老师表示之前用Itchat自动群发消息,但是现在网页版微信不能登录了,需要找个替代品,不然手工发送太浪费时间。
不知道有没有还不认识杨老师的,这是他的CSDN链接:https://blog.csdn.net/Eastmount
对于这个问题,UI自动化测试工具uiautomation,表示实在是太适合啦。
对uiautomation相关基础还不了解的童鞋,请查看下面这两篇文章:
《⭐UI自动化工具轻松实现微信消息收发⚡朋友圈爬取⁉️》
《️❤️对比PyWinAuto和uiautomation实现微信联系人自动采集❤️》
!!!
注意:本文所测试的PC微信版本是当前最新的3.3.5.42:
今天我们的目标是自动群发消息,即批量向多个会话发送消,向单个会话发消息已经在第一篇文章中演示过。
查看对应UI对象:
搜索并获取微信窗口的控制器,激活窗口后,获取会话列表对象:
import uiautomation as auto
wechatWindow = auto.WindowControl(
searchDepth=1, Name="微信", ClassName='WeChatMainWndForPC')
wechatWindow.SetActive()
sessionListControl = wechatWindow.ListControl(Name='会话')
我们先实现对前N个置顶的会话批量发送消息。这种非常适合每天都需要向固定几个群批量发消息的需求。
在发消息前,需要先保证会话列表已经移动到顶部,避免发错了对象:
wechatWindow.SetActive()
session = sessionListControl.GetFirstChildControl()
last = session.Name
session.GetNextSiblingControl().Click()
while 1:
sessionListControl.SendKeys("{UP 10}")
session = sessionListControl.GetFirstChildControl()
if last == session.Name:
break
last = session.Name
上述代码表示先单击第二个可见会话(第一个可见会话可能点击不到),然后按10次向上方向键之后比较当前可视列表的首个元素名称是否不再发生变化,发生变化则再按10次向上方向键,直到名称不再发生变化为止。
看看效果:
假设我们的爬虫爬取到的新闻文本内容如下:
news = """8月25日
中国石油大庆油田有限责任公司宣布
大庆油田古龙页岩油勘探
取得重大战略性突破
新增石油预测地质储量12.68亿吨
标志着我国页岩油勘探开发
取得重大突破
----来自于UI自动化程序测试(请忽略)
"""
经测试,直接发送以上文本无法进行换行,于是编写以下方法将所有的换行符替换成回车按键标记:
def chars2keys(chars):
# 将所有的换行符替换为回车键
return chars.replace("\n", "{ENTER}")
chars2keys(news)
'8月25日{ENTER}中国石油大庆油田有限责任公司宣布{ENTER}大庆油田古龙页岩油勘探{ENTER}取得重大战略性突破{ENTER}新增石油预测地质储量12.68亿吨{ENTER}标志着我国页岩油勘探开发{ENTER}取得重大突破{ENTER}{ENTER}----来自于UI自动化程序测试(请忽略){ENTER}'
测试输入:
edit = wechatWindow.EditControl(Name='输入')
edit.SendKeys(chars2keys(news))
可以看到已经顺利的可以输入换行符。
关于特殊按键可以参考:https://docs.microsoft.com/zh-cn/previous-versions/dotnet/netframework-1.1/k3w7761b(v=vs.80)
测试过程为了减少对被测用户的打扰,我加入发完测试消息自动撤回的功能。
发送并撤回的实现代码:
# 发送
sendButton = wechatWindow.ButtonControl(Name='发送(S)')
sendButton.Click()
# 撤回消息
messages = wechatWindow.ListControl(Name='消息')
message = messages.GetLastChildControl()
message.Click()
message.RightClick()
menu = wechatWindow.MenuControl()
menu_items = menu.GetLastChildControl().GetFirstChildControl().GetChildren()
for menu_item in menu_items:
if menu_item.ControlTypeName != "MenuItemControl":
continue
if menu_item.Name == "撤回":
menu_item.Click()
这时我们就可以向N个置顶会话批量挨个发送消息了。
批量发送的的完整代码(含撤回,正式使用时可以注释掉):
edit = wechatWindow.EditControl(Name='输入')
sendButton = wechatWindow.ButtonControl(Name='发送(S)')
messages = wechatWindow.ListControl(Name='消息')
n = 5
childs = sessionListControl.GetChildren()[:-1]
print("当前视图最大可发送会话个数为", len(childs))
sessions = childs[:n]
for session in sessions:
print(session.Name)
session.Click()
edit.SendKeys(chars2keys(news))
# 发送
sendButton.Click()
# 撤回消息
message = messages.GetLastChildControl()
# message.Click()
message.RightClick()
menu = wechatWindow.MenuControl()
menu_items = menu.GetLastChildControl().GetFirstChildControl().GetChildren()
for menu_item in menu_items:
if menu_item.ControlTypeName != "MenuItemControl":
continue
if menu_item.Name == "撤回":
menu_item.Click()
目前最大只支持向可视范围内的会话发送消息,未实现翻页。能向多少个置顶会话发送消息,取决于你的屏幕大小。
测试向前5个置顶会话发送消息:
上面的发送方式有点像打字机的效果,但实际上我们采用复制粘贴的方式会快很多。下面我们整理一下,最终完整代码如下:
import uiautomation as auto
def sessionList2Top():
session = sessionListControl.GetFirstChildControl()
last = session.Name
session.GetNextSiblingControl().Click()
while 1:
sessionListControl.SendKeys("{UP 10}")
session = sessionListControl.GetFirstChildControl()
if last == session.Name:
break
last = session.Name
def recall_lastmessage():
message = messages.GetLastChildControl()
message.Click()
message.RightClick()
menu = wechatWindow.MenuControl()
menu_items = menu.GetLastChildControl().GetFirstChildControl().GetChildren()
for menu_item in menu_items:
if menu_item.ControlTypeName != "MenuItemControl":
continue
if menu_item.Name == "撤回":
menu_item.Click()
news = """8月25日
中国石油大庆油田有限责任公司宣布
大庆油田古龙页岩油勘探
取得重大战略性突破
新增石油预测地质储量12.68亿吨
标志着我国页岩油勘探开发
取得重大突破
----来自自动化测试程序,如有打扰请见谅~
"""
wechatWindow = auto.WindowControl(
searchDepth=1, Name="微信", ClassName='WeChatMainWndForPC')
wechatWindow.SetActive()
sessionListControl = wechatWindow.ListControl(Name='会话')
sessionList2Top()
edit = wechatWindow.EditControl(Name='输入')
sendButton = wechatWindow.ButtonControl(Name='发送(S)')
messages = wechatWindow.ListControl(Name='消息')
n = 5
childs = sessionListControl.GetChildren()[:-1]
print("当前视图最大可发送会话个数为", len(childs))
sessions = childs[:n]
for session in sessions:
print(session.Name)
session.Click()
# 发送消息
auto.SetClipboardText(news)
edit.SendKeys('{Ctrl}v')
sendButton.Click()
# 撤回消息
# recall_lastmessage()
最终测试一下,已移除重复元素:
实际使用中,发现上述处理逻辑存在较大的BUG,如果在对置顶群挨个发消息的过程,某个群有其他人发消息,就会导致会话列表顺序发生变化,从而出现漏发或错发。
针对这种情况,我们比较精准的发消息的方式是基于搜索进行发送。
首先分别获取搜索框、输入框、消息列表和发送按钮4个UI组件:
import uiautomation as auto
wechatWindow = auto.WindowControl(
searchDepth=1, Name="微信", ClassName='WeChatMainWndForPC')
wechatWindow.SetActive()
search = wechatWindow.EditControl(Name='搜索')
edit = wechatWindow.EditControl(Name='输入')
messages = wechatWindow.ListControl(Name='消息')
sendButton = wechatWindow.ButtonControl(Name='发送(S)')
然后编写以下方法:
import time
def send2name(name, txt, wait_time=0.1):
search.Click()
auto.SetClipboardText(name)
edit.SendKeys('{Ctrl}v')
# 等待微信索引搜索跟上
time.sleep(wait_time)
search.SendKeys("{Enter}")
auto.SetClipboardText(txt)
edit.SendKeys('{Ctrl}v')
sendButton.Click()
测试一下:
name = "小小明"
txt = "小小明你好,收到这条消息说明你的程序已经成功---来自自动化测试程序"
send2name(name, txt)
可以看到能够精准的发送指定用户:
再搜索指定群名试一下(加入自动撤回):
def recall_lastmessage():
message = messages.GetLastChildControl()
message.Click()
message.RightClick()
menu = wechatWindow.MenuControl()
menu_items = menu.GetLastChildControl().GetFirstChildControl().GetChildren()
for menu_item in menu_items:
if menu_item.ControlTypeName != "MenuItemControl":
continue
if menu_item.Name == "撤回":
menu_item.Click()
name = "三人行"
txt = f"{
name}收到这条消息,说明搜索发送无误"
send2name(name, txt)
recall_lastmessage()
有了上述方法,想给任何人发消息都可以精准都搜索发送了。
需要批量发送,只需要指定名称列表循环处理即可。
大多数时候我们都是要发送完全相同的消息到多个群,使用微信的转发功能会更快,可以先将消息发送给文件传输助手后,然后再进行转发,下面演示如何实现转发。
先获取微信的四大组件:
import uiautomation as auto
wechatWindow = auto.WindowControl(
searchDepth=1, Name="微信", ClassName='WeChatMainWndForPC')
wechatWindow.SetActive()
search = wechatWindow.EditControl(Name='搜索')
edit = wechatWindow.EditControl(Name='输入')
messages = wechatWindow.ListControl(Name='消息')
sendButton = wechatWindow.ButtonControl(Name='发送(S)')
然后选中当前界面的最后一条消息打开转发界面:
message = messages.GetLastChildControl()
message.RightClick()
menu = wechatWindow.MenuControl()
menu_items = menu.GetLastChildControl().GetFirstChildControl().GetChildren()
for menu_item in menu_items:
if menu_item.ControlTypeName != "MenuItemControl":
continue
if menu_item.Name == "转发":
menu_item.Click()
break
获取转发窗口中几个重要的UI组件:
send2ps = wechatWindow.WindowControl(Name="转发给", ClassName='SelectContactWnd')
receiver = send2ps.EditControl(Name="搜索")
sendB2 = send2ps.ButtonControl(Name='发送')
items = send2ps.ListControl()
下面随便选了一些需要转发的好友或群:
import time
users = ["文件传输助手", "小小明", "云朵君", "道财", "黄同学", "三人行"]
for user in users:
receiver.Click()
auto.SetClipboardText(user)
receiver.SendKeys('{Ctrl}v')
# 等待UI组件的渲染变化
time.sleep(0.1)
items.ButtonControl().Click()
receiver.GetParentControl().GetNextSiblingControl().Click()
测试一下:
接下来只需要调用一下以下代码就完成发送了:
sendB2.Click()
上述代码还未考虑微信一次最大只支持对9个用户转发的限制,那么我们可以做个分组后,多次转发。最终完整代码:
import time
import uiautomation as auto
wechatWindow = auto.WindowControl(
searchDepth=1, Name="微信", ClassName='WeChatMainWndForPC')
wechatWindow.SetActive()
search = wechatWindow.EditControl(Name='搜索')
edit = wechatWindow.EditControl(Name='输入')
messages = wechatWindow.ListControl(Name='消息')
sendButton = wechatWindow.ButtonControl(Name='发送(S)')
message = messages.GetLastChildControl()
while message.Name == '':
message = message.GetPreviousSiblingControl()
message.EditControl().RightClick()
menu = wechatWindow.MenuControl()
menu_items = menu.GetLastChildControl().GetFirstChildControl().GetChildren()
for menu_item in menu_items:
if menu_item.ControlTypeName != "MenuItemControl":
continue
if menu_item.Name == "转发":
menu_item.Click()
break
send2ps = wechatWindow.WindowControl(Name="转发给", ClassName='SelectContactWnd')
receiver = send2ps.EditControl(Name="搜索")
sendB2 = send2ps.ButtonControl(Name='发送')
items = send2ps.ListControl()
def relay2users(users, max_n=9):
for i in range(0, len(users), max_n):
users_split = users[i:i+max_n]
for user in users_split:
receiver.Click()
auto.SetClipboardText(user)
receiver.SendKeys('{Ctrl}v')
# 等待UI组件的渲染变化
time.sleep(0.1)
items.ButtonControl().Click()
receiver.GetParentControl().GetNextSiblingControl().Click()
sendB2.Click()
users = ["文件传输助手", "小小明", "云朵君", "道财", "黄同学", "三人行"]
relay2users(users)
另一种更快的操作方案就是先用前一节的方法将新闻发送到文件传输助手,然后将当前界面的消息一口气转发到指定的列表中。