出于兴趣,想要获取 “QQ 群”成员资料,于是乎找到了一个自动化的 Python 仓库:Python-UIAutomation-for-Windows。
目录
1. 简介
1.1 实际环境
1.2 安装/源码
1.2.1 pip安装
1.2.2 源码
2. 代码
2.1 全部代码
2.2 class QQGroupMembers
2.2.1 def countdown
2.2.2 更多资料
2.2.3 def guess
2.2.4 def config
2.2.5 保存
3. 演示
1. 简介
引用仓库作者 Yinkai Sheng 的话,来介绍:
uiautomation 封装了微软 UIAutomation API,支持自动化 Win32,MFC,WPF,Modern UI(Metro UI),Qt,IE,Firefox(version<=56 or >=60,Firefox57是第一个 Rust 开发版本,前几个 Rust 开发版本个人测试发现不支持), Chrome 和基于 Electron 开发的应用程序(Chrome 浏览器和 Electron 应用需要加启动参数 --force-renderer-accessibility 才能支持 UIAutomation).
uiautomation is shared under the Apache Licence 2.0.
1.1 实际环境
个人对操作系统并不是很了解,以下给出我的实际环境。
- 操作系统:Windows 11 教育版
- 系统类型:64 位操作系统, 基于 x64 的处理器
- Python:3.8.19
1.2 安装/源码
对于 Python 包的使用,可以直接通过 pip 安装,或者使用源码(个人使用源码)。
1.2.1 pip安装
使用 pip 安装包即可。
pip install uiautomation
1.2.2 源码
使用源码也较为简单,因为代码作者已经将全部类和方法放置于一个 .py 文件:yinkaisheng/Python-UIAutomation-for-Windows/uiautomation/uiautomation.py。
必要的只有 yinkaisheng/Python-UIAutomation-for-Windows/uiautomation 这一个文件夹即可。
当然,作者提供了很多使用的 demos,也可以下载下来参考。本文即参考 demos/get_qq_group_members.py 进行的修改与实践。
注意,uiautomation/bin 中文件作用暂时不清楚,我看源码有对该文件的访问。但是我的测试,在以下代码执行过程中,bin中的文件不是强制被需要的。
2. 代码
2.1 全部代码
先放全部的代码,代码解析在后续。
# coding=utf-8
# @Author: Fulai Cui (cuifulai@mail.hfut.edu.cn)
# @Time: 2024/9/23 11:34
import argparse
import re
from datetime import datetime
import json
import time
from typing import Optional
from tqdm import trange
import uiautomation as auto
class QQGroupMembers:
def __init__(
self,
file_path: Optional[str] = None,
):
self.file_path = file_path
def main(self):
auto.Logger.WriteLine('【提示】请把鼠标放在QQ群聊天窗口中右下角群成员列表中的一个成员上面,3秒后获取。',
auto.ConsoleColor.Cyan, writeToFile=False)
self.countdown(3)
control = auto.ControlFromCursor()
if control.ControlType != auto.ControlType.ListItemControl:
auto.Logger.WriteLine('【警告】没有放在群成员上面,程序退出!',
auto.ConsoleColor.Red, writeToFile=False)
return
window = auto.GetConsoleWindow()
if window:
window.SetActive()
group = control.GetParentControl()
members = group.GetChildren()
for member in members:
auto.Logger.WriteLine(member.Name,
auto.ConsoleColor.Green, writeToFile=False)
pass
auto.Logger.WriteLine('【提示】是否获取成员详细信息?按F9继续,F10退出。',
auto.ConsoleColor.Cyan, writeToFile=False)
self.waite()
auto.Logger.WriteLine('【提示】3秒后开始获取QQ群成员详细资料,您可以一直按住F10键暂停脚本。',
auto.ConsoleColor.Cyan, writeToFile=False)
self.countdown(3)
# 确保群里第一个成员可见在最上面
group.Click()
group.SendKeys('{Home}', waitTime=0.5)
for member in members:
if member.ControlType == auto.ControlType.ListItemControl:
if auto.IsKeyPressed(auto.Keys.VK_F10):
if window:
window.SetActive()
auto.Logger.WriteLine('【提示】您暂停了脚本,按F9继续。',
auto.ConsoleColor.Cyan, writeToFile=False)
while True:
if auto.IsKeyPressed(auto.Keys.VK_F9):
break
time.sleep(0.05)
member.RightClick(waitTime=0.5)
menu = auto.MenuControl(searchDepth=1, ClassName='TXGuiFoundation')
menu_items = menu.GetChildren()
for menu_item in menu_items:
if menu_item.Name == '查看资料':
menu_item.Click(40)
break
auto.Logger.WriteLine(json.dumps(self.get_person_detail(), ensure_ascii=False),
auto.ConsoleColor.Green, logFile=self.file_path)
member.Click()
auto.SendKeys('{Down}')
def get_person_detail(self):
detail_window = auto.WindowControl(searchDepth=1, ClassName='TXGuiFoundation', SubName='的资料')
for control, _ in auto.WalkControl(detail_window):
if isinstance(control, auto.ButtonControl):
if control.Name == '更多资料':
control.Click()
break
details = {}
for control, depth in auto.WalkControl(detail_window):
key, value = self.guess(control, depth)
if key is None or key == '':
continue
if key not in details:
details[key] = value
else:
details[key] += "-|-" + value
detail_window.Click(-10, 10)
return details
@staticmethod
def guess(control: auto.Control, depth: int):
key = None
value = None
if isinstance(control, auto.ButtonControl):
value = control.Name
if value != '':
if depth == 8 and re.match(r'^\d+$', value) and control.ControlType == auto.ControlType.ButtonControl:
key = '点赞记录'
elif isinstance(control, auto.TextControl):
value = control.Name
if value != '':
if '天' in value:
key = '连续登陆天数'
value = value
elif isinstance(control, auto.PaneControl):
value = control.Name
if value != '':
if '等级' in value:
key = '等级'
value = re.findall(r'(\d+)', value)[0]
elif isinstance(control, auto.EditControl):
key = control.Name
value = control.GetValuePattern().Value
if key == '' and value != '':
if re.match(r'^\d{5,}$', value):
key = 'QQ号'
elif '月' in value:
key = '生日'
else:
key = '网名/备注'
return key, value
@staticmethod
def countdown(seconds: int):
for _ in trange(seconds, desc='倒计时'):
time.sleep(1)
@staticmethod
def waite():
while True:
if auto.IsKeyPressed(auto.Keys.VK_F9):
break
elif auto.IsKeyPressed(auto.Keys.VK_F10):
return
time.sleep(0.05)
def config(args):
auto.SEARCH_INTERVAL = args.SEARCH_INTERVAL if hasattr(args, 'SEARCH_INTERVAL') else auto.SEARCH_INTERVAL
auto.MAX_MOVE_SECOND = args.MAX_MOVE_SECOND if hasattr(args, 'MAX_MOVE_SECOND') else auto.MAX_MOVE_SECOND
auto.TIME_OUT_SECOND = args.TIME_OUT_SECOND if hasattr(args, 'TIME_OUT_SECOND') else auto.TIME_OUT_SECOND
auto.OPERATION_WAIT_TIME = args.OPERATION_WAIT_TIME if hasattr(args, 'OPERATION_WAIT_TIME') else auto.OPERATION_WAIT_TIME
auto.MAX_PATH = args.MAX_PATH if hasattr(args, 'MAX_PATH') else auto.MAX_PATH
auto.DEBUG_SEARCH_TIME = args.DEBUG_SEARCH_TIME if hasattr(args, 'DEBUG_SEARCH_TIME') else auto.DEBUG_SEARCH_TIME
auto.DEBUG_EXIST_DISAPPEAR = args.DEBUG_EXIST_DISAPPEAR if hasattr(args, 'DEBUG_EXIST_DISAPPEAR') else auto.DEBUG_EXIST_DISAPPEAR
auto.S_OK = args.S_OK if hasattr(args, 'S_OK') else auto.S_OK
def parse_args():
parser = argparse.ArgumentParser(description='QQ Group Members')
parser.add_argument('--dir_path', type=str, default='../logs',
help='日志文件路径')
parser.add_argument('--group_name', type=str, default='XXX群',
help='QQ群名称')
parser.add_argument('--MAX_MOVE_SECOND', type=int, default=0.5,
help='最大移动秒数')
return parser.parse_args()
def main():
args = parse_args()
config(args)
automation = QQGroupMembers(file_path=f"{args.dir_path}/{args.group_name}_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.log")
automation.main()
if __name__ == '__main__':
main()
2.2 class QQGroupMembers
以下详述一些 demos 中没有的内容。
2.2.1 def countdown
设置倒计时。
@staticmethod
def countdown(seconds: int):
for _ in trange(seconds, desc='倒计时'):
time.sleep(1)
2.2.2 更多资料
在打开“查看资料”面板之后,先找到“更多资料”,然后点击 .Click() 打开,目的是获取全面的资料信息。
def get_person_detail(self):
detail_window = auto.WindowControl(searchDepth=1, ClassName='TXGuiFoundation', SubName='的资料')
for control, _ in auto.WalkControl(detail_window):
if isinstance(control, auto.ButtonControl):
if control.Name == '更多资料':
control.Click()
break
...more code
2.2.3 def guess
猜测控件中的内容是什么,这个需要根据实践做出适当的调整。
@staticmethod
def guess(control: auto.Control, depth: int):
key = None
value = None
if isinstance(control, auto.ButtonControl):
value = control.Name
if value != '':
if depth == 8 and re.match(r'^\d+$', value) and control.ControlType == auto.ControlType.ButtonControl:
key = '点赞记录'
elif isinstance(control, auto.TextControl):
value = control.Name
if value != '':
if '天' in value:
key = '连续登陆天数'
value = value
elif isinstance(control, auto.PaneControl):
value = control.Name
if value != '':
if '等级' in value:
key = '等级'
value = re.findall(r'(\d+)', value)[0]
elif isinstance(control, auto.EditControl):
key = control.Name
value = control.GetValuePattern().Value
if key == '' and value != '':
if re.match(r'^\d{5,}$', value):
key = 'QQ号'
elif '月' in value:
key = '生日'
else:
key = '网名/备注'
return key, value
这里,猜测了:
- ButtonControl.Name,在第 8 层,如果完全是数字,则更可能是“点赞记录”
- TextControl.Name,如果包含“天”,则更可能是“连续登陆天数”
- PaneControl.Name,如果包含“等级”,则更可能是“等级”
- EditControl,有 Name 和 GetValuePattern().Value,如果 Value 完全是数字,则更可能是“QQ号”;如果 Value 包含“月”,则更可能是“生日”;否则的话是“网名/备注”。
2.2.4 def config
为了修改 uiautomation 的默认配置参数,这里只修改了 uiautomation.MAX_MOVE_SECOND(默认为 1,感觉有点久)。别的参数也可自行修改。
def config(args):
auto.SEARCH_INTERVAL = args.SEARCH_INTERVAL if hasattr(args, 'SEARCH_INTERVAL') else auto.SEARCH_INTERVAL
auto.MAX_MOVE_SECOND = args.MAX_MOVE_SECOND if hasattr(args, 'MAX_MOVE_SECOND') else auto.MAX_MOVE_SECOND
auto.TIME_OUT_SECOND = args.TIME_OUT_SECOND if hasattr(args, 'TIME_OUT_SECOND') else auto.TIME_OUT_SECOND
auto.OPERATION_WAIT_TIME = args.OPERATION_WAIT_TIME if hasattr(args, 'OPERATION_WAIT_TIME') else auto.OPERATION_WAIT_TIME
auto.MAX_PATH = args.MAX_PATH if hasattr(args, 'MAX_PATH') else auto.MAX_PATH
auto.DEBUG_SEARCH_TIME = args.DEBUG_SEARCH_TIME if hasattr(args, 'DEBUG_SEARCH_TIME') else auto.DEBUG_SEARCH_TIME
auto.DEBUG_EXIST_DISAPPEAR = args.DEBUG_EXIST_DISAPPEAR if hasattr(args, 'DEBUG_EXIST_DISAPPEAR') else auto.DEBUG_EXIST_DISAPPEAR
auto.S_OK = args.S_OK if hasattr(args, 'S_OK') else auto.S_OK
2.2.5 保存
使用 uiautomation.Logger.WriteLine() 方法来写入内容。注意,需要在定义 QQGroupMembers 对象时,设置参数 file_path。
3. 演示
此演示无别的意图。