新建Python文件:wx_file.py,代码如下
# -*- coding: utf-8 -*-
# @Author : CxiuM
# @Time : 2023-07-06 10:12
# @Name : wx_operation.py
"""微信群发消息"""
import os
import time
import subprocess
import requests
import uiautomation as auto
from copy import deepcopy
from typing import Iterable
class WxOperation:
"""
微信群发消息的类。
...
Attributes:
----------
wx_window: auto.WindowControl
微信控制窗口
input_edit: wx_window.EditControl
聊天界面输入框编辑控制窗口
search_edit: wx_window.EditControl
搜索输入框编辑控制窗口
Methods:
-------
__goto_chat_box(name):
跳转到 指定好友窗口
__send_text(*msgs):
发送文本。
__send_file(*filepath):
发送文件
get_friend_list(tag, num):
可指定tag,获取好友num页的好友数量
send_msg(*names, msgs, file_paths)
单个或批量发送文本和文件
"""
def __init__(self):
auto.SendKeys(text='{Alt}{Ctrl}z') # 快捷键唤醒微信
self.wx_window = auto.WindowControl(Name='微信', ClassName='WeChatMainWndForPC')
assert self.wx_window.Exists(), "窗口不存在"
self.input_edit = self.wx_window.EditControl(Name='输入')
self.search_edit = self.wx_window.EditControl(Name='搜索')
def __goto_chat_box(self, name: str) -> None:
"""
跳转到指定 name好友的聊天窗口。
Args:
name(str): 必选参数,好友名称
Returns:
None
"""
assert name, "无法跳转到名字为空的聊天窗口"
self.wx_window.SendKeys(text='{Ctrl}f', waitTime=0.2)
self.wx_window.SendKeys(text='{Ctrl}a', waitTime=0.1)
self.wx_window.SendKey(key=auto.SpecialKeyNames['DELETE'])
auto.SetClipboardText(text=name)
self.wx_window.SendKeys(text='{Ctrl}v', waitTime=0.1)
self.wx_window.SendKey(key=auto.SpecialKeyNames['ENTER'], waitTime=0.2)
def __send_text(self, *msgs) -> None:
"""
发送文本.
Args:
*msgs(Iterable or str): 必选参数,为发送的文本
Returns:
None
"""
for msg in msgs:
assert msg, "发送的文本内容为空"
self.input_edit.SendKeys(text='{Ctrl}a', waitTime=0.1)
self.input_edit.SendKey(key=auto.SpecialKeyNames['DELETE'])
# self.input_edit.SendKeys(text=msg, waitTime=0.1) # 一个个字符插入,不建议使用该方法
# 设置到剪切板再黏贴到输入框
auto.SetClipboardText(text=msg)
self.input_edit.SendKeys(text='{Ctrl}v', waitTime=0.1)
self.wx_window.SendKey(key=auto.SpecialKeyNames['ENTER'], waitTime=0.2)
def __send_file(self, *file_paths) -> None:
"""
发送文件.
Args:
*file_paths(Iterable or str): 必选参数,为文件的路径
Returns:
None
"""
all_path = str()
for path in file_paths:
full_path = os.path.abspath(path=path)
assert os.path.exists(full_path), f"{full_path} 文件路径有误"
all_path += "'" + full_path + "',"
args = ['powershell', f'Get-Item {all_path[:-1]} | Set-Clipboard']
# 去除console 弹窗
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags = subprocess.CREATE_NEW_CONSOLE | subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
subprocess.Popen(args=args, startupinfo=startupinfo)
time.sleep(0.5)
self.input_edit.SendKeys(text='{Ctrl}v', waitTime=0.2)
self.wx_window.SendKey(key=auto.SpecialKeyNames['ENTER'], waitTime=0.2)
def get_friend_list(self, tag: str = None, num: int = 10) -> list:
"""
获取微信好友名称.
Args:
tag(str): 可选参数,如不指定,则获取所有好友
num(int): 可选参数,如不指定,只获取10页好友
Returns:
list
"""
def click_tag():
"""点击标签"""
contacts_management_window.ButtonControl(Name="标签").Click()
# 点击 通讯录管理
self.wx_window.ButtonControl(Name="通讯录").Click()
self.wx_window.ListControl(Name="联系人").ButtonControl(Name="通讯录管理").Click()
contacts_management_window = auto.GetForegroundControl() # 切换到通讯录管理,相当于切换到弹出来的页面
# contacts_management_window.ButtonControl(Name='最大化').Click()
if tag:
click_tag() # 点击标签
contacts_management_window.PaneControl(Name=tag).Click()
time.sleep(0.3)
click_tag() # 关闭标签
# 获取滑动模式
scroll = contacts_management_window.ListControl().GetScrollPattern()
# assert scroll, "没有可滑动对象"
name_list = list()
if not scroll:
for name_node in contacts_management_window.ListControl().GetChildren(): # 获取当前页面的 列表 -> 子节点
nick_name = name_node.TextControl().Name # 用户名
remark_name = name_node.ButtonControl(foundIndex=2).Name # 用户备注名,索引1会错位,索引2是备注名,索引3是标签名
name_list.append(remark_name if remark_name else nick_name)
else:
rate: int = int(float(102000 / num)) # 根据输入的num计算滑动的步长
for pct in range(0, 102000, rate): # range不支持float,不导入numpy库,采取迂回这的方式
# 每次滑动一点点,-1代表不用滑动
scroll.SetScrollPercent(horizontalPercent=-1, verticalPercent=pct / 100000)
for name_node in contacts_management_window.ListControl().GetChildren(): # 获取当前页面的 列表 -> 子节点
nick_name = name_node.TextControl().Name # 用户名
remark_name = name_node.ButtonControl(foundIndex=2).Name # 用户备注名,索引1会错位,索引2是备注名,索引3是标签名
name_list.append(remark_name if remark_name else nick_name)
contacts_management_window.SendKey(auto.SpecialKeyNames['ESC']) # 结束时候关闭 "通讯录管理" 窗口
return list(set(name_list)) # 简单去重,但是存在误判(如果存在同名的好友
def get_group_chat_list(self) -> list:
"""获取群聊通讯录中的用户名称"""
name_list = list()
auto.ButtonControl(Name='聊天信息').Click()
time.sleep(0.5)
chat_members_win = self.wx_window.ListControl(Name='聊天成员')
if not chat_members_win.Exists():
return list()
self.wx_window.ButtonControl(Name='查看更多').Click()
for item in chat_members_win.GetChildren():
name_list.append(item.ButtonControl().Name)
return name_list
def get_chat_records(self, page: int = 1) -> list:
"""
获取聊天列表的聊天记录.
Args:
page(int): 可选参数,如不指定,只获取1页聊天记录
Returns:
list
"""
chat_records = list()
def extract_msg() -> None:
all_msgs = self.wx_window.ListControl(Name="消息").GetChildren()
for msg_node in all_msgs:
msg = msg_node.Name
if not msg:
continue
if msg_node.PaneControl().Name:
chat_records.append({'type': 'Time', 'name': 'System', 'msg': msg_node.PaneControl().Name})
continue
if msg in ['以下为新消息', '查看更多消息', '该类型文件可能存在安全风险,建议先检查文件安全性后再打开。', '已撤回']:
chat_records.append({'type': 'System', 'name': 'System', 'msg': msg})
continue
if '撤回了一条消息' in msg or '尝试撤回上一条消息' in msg:
chat_records.append(
{'type': 'Other', 'name': ''.join(msg.split(' ')[:-1]), 'msg': msg.split(' ')[-1]})
continue
if msg in ['发出红包,请在手机上查看', '收到红包,请在手机上查看', '你发送了一次转账收款提醒,请在手机上查看', '你收到了一次转账收款提醒,请在手机上查看']:
chat_records.append({'type': 'RedEnvelope', 'name': 'System', 'msg': msg})
continue
if '领取了你的红包' in msg:
_ = msg.split('领取了你的红包')
chat_records.append({'type': 'RedEnvelope', 'name': _[0], 'msg': _[1]})
continue
name = msg_node.ButtonControl(foundIndex=1).Name
if msg == '[文件]':
file_name = msg_node.PaneControl().TextControl(foundIndex=1).Name
size = msg_node.PaneControl().TextControl(foundIndex=2).Name
chat_records.append(
{'type': 'File', 'name': name, 'msg': f'size: {size} --- file_name: {file_name}'})
continue
if msg == '微信转账':
operation = msg_node.PaneControl().TextControl(foundIndex=2).Name
amount = msg_node.PaneControl().TextControl(foundIndex=3).Name
chat_records.append(
{'type': 'RedEnvelope', 'name': name, 'msg': msg + f' {operation} ' + amount})
continue
if '引用' in msg and '的消息' in msg:
chat_records.append({'type': 'Cited', 'name': name, 'msg': msg})
continue
if msg == '[聊天记录]':
if not name:
name = msg_node.ButtonControl(foundIndex=2).Name
chat_records.append({'type': 'Content', 'name': name, 'msg': msg})
for _ in range(page):
self.wx_window.WheelUp(wheelTimes=15)
extract_msg()
return chat_records
def send_msg(self, *names, msgs, file_paths, add_remark_name=False) -> None:
"""
发送消息,可同时发送文本和文件(至少选一项
Args:
*names (str or Iterable):必选参数,接收消息的好友名称,可以群发,也可以单发
msgs (list): 可选参数,发送的文本消息
file_paths (Iterable):可选参数,发送的文件路径
add_remark_name(bool): 可选参数,是否添加备注名称发送
Returns:
None
"""
assert names, "用户名列表为空"
assert any([msgs, file_paths]), "没有发送任何消息"
assert not isinstance(msgs, str), "文本必须为可迭代且非字符串类型"
assert not isinstance(file_paths, str), "文件路径必须为可迭代且非字符串类型"
for name in names:
self.__goto_chat_box(name=name)
if msgs:
if add_remark_name:
new_msgs = deepcopy(msgs)
new_msgs.insert(0, name)
self.__send_text(*new_msgs)
else:
self.__send_text(*msgs)
if file_paths:
self.__send_file(*file_paths)
再建一个Python文件为:mytest.py,代码如下:
from wx_file import WxOperation
wx = WxOperation()
# 发送文本和文件
wx.send_msg('文件传输助手', msgs=["Hello"], file_paths=["F:\Test.txt"])
wx.send_msg('文件传输助手', msgs=["world"], file_paths=["F:\Test.txt"])
但是有时第一次发送不成功,第二次成功。
注意:微信最新版本3.9.5以上代码会发送文件不成功,原因是找不到控件。把电脑版本微信降到上一个版本3.9.2发送成功。
仅用于学习交流。