0%

pc自动化调研

关于PC自动化

无论使用那种PC自动化工具,首先需要知道应用程序是win32 APIbackend「 win32 」,还是MS UI Automationbackend「 uia 」

WinAppDriver

  • 它支持 Appium,可以使用 Appium-Python-Client 依赖库完成对 Windows 桌面程序的自动化操作

  • 项目地址:https://github.com/Microsoft/WinAppDriver

  • 需要注意的是,要使用 WinAppDriver 服务框架完成 Windows 的自动化,需要满足 Windows10 或 Windows Server 2016 以上系统

    另外,它支持的应用程序包含:

    • UWP - Universal Windows Platform
    • WinForms - Windows Forms
    • WPF - Windows Presentation Foundation
    • Win32 - Classic Windows

应用检测

  • 打开Insights 下载定位工具,检测是否能定位到元素

image-20220927155045493

  • 正常定位到元素

image-20220927160349788

配置

  • 开启「 开发者模式 」

image-20220927160535821

下载WinAppDriver

  • 打开链接下载,截至到现在最新版本为:WindowsApplicationDriver-1.2.99-win-x86.exe
  • 运行WinAppDriver.exe
1
2
3
C:\Program Files (x86)\Windows Application Driver>WinAppDriver.exe 4727
Windows Application Driver listening for requests at: http://127.0.0.1:4727/
Press ENTER to exit.

代码测试

  • 安装selenium,不能用4的版本
1
pip install selenium==3.14.1
  • 测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

import unittest
from selenium import webdriver

class SimpleTests(unittest.TestCase):

@classmethod
def setUpClass(self):
desired_caps = {}
desired_caps['app'] = r"C:\Windows\System32\notepad.exe"
self.driver = webdriver.Remote(
command_executor='http://127.0.0.1:4727',
desired_capabilities=desired_caps)

@classmethod
def tearDownClass(self):
self.driver.quit()

def test_edit(self):
self.driver.find_element_by_name("文本编辑器").send_keys("helloword")


if __name__ == '__main__':
suite = unittest.TestLoader().loadTestsFromTestCase(SimpleTests)
unittest.TextTestRunner(verbosity=2).run(suite)

pywinauto

  • 主要使用到 Application 类,用于应用程序管理(打开与关闭应用等)、窗口管理(最小化、最大化、关闭窗口)

  • 同时支持控件操作和图像操作,支持Win32 APIMS UI Automation API

  • 安装

1
pip install pywinauto

应用程序

 确定应用程序的可访问技术,pywinauto的后端,支持控件的访问技术:

  • Win32 API(backend=”win32”)- 默认的backend,MFC,VB6,VCL。简单的WinForms控件和大多数旧的应用程序
  • MS UI Automation API ( backend = “ uia “ ):WinForms , WPF , Store apps , Qt5 , 浏览器

注:可以借助于GUI对象检查工具来确定程序到底适用于那种backend。eg:如果使用 inspect 的uia模式,可见的控件和属性更多的话,backend可选uia,反之,backend可选win32。

  • 常用的检查工具

    • Inspect(定位元素工具(uia))

    • Spy++ (定位元素工具(win32))

    • UI Spy (定位元素工具)

    • Swapy(可简单生成pywinauto代码)

定位元素

  • 下载Inspect后,定位桌面微信

image-20220930104310955

  • 用UISpy定位,需要下载.net 3.5

image-20220930105530130

image-20220930110003234

  • 用SPYXX.EXE定位,拖动到Finder Tool图标到微信上,识别到后点击OK

image-20220930110155030

  • 看到当期微信的句柄等信息

image-20220930110350697

  • 打开并启动app
1
2
3
from pywinauto.application import Application
app = Application(backend = "uia").start(r'C:\Program Files (x86)\Tencent\WeChat\WeChat.exe') # 启动微信

  • 连接app
1
2
3
4
5
app = Application(backend='uia').connect(process=8948)  # 进程号
app = Application().connect(handle=0x010f0c) # 句柄
app = Application().connect(path="D:\Office14\EXCEL.exe") # path
app = Application().connect(title_re=".*Notepad", class_name=“Notepad”) # 参数组合

注意:应用程序必须先准备就绪,才能使用connect(),当应用程序start()后没有超时和重连的机制。在pywinauto外再启动应用程序,需要sleep,等程序start。

常用方法

Application对象app的常用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
app.top_window() # 返回应用程序当前顶部窗口,是WindowSpecification对象,可以继续使用对象的方法往下继续查找控件
# eg:如:app.top_window().child_window(title='地址和搜索栏', control_type='Edit')

app.window(**kwargs) # 根据筛选条件,返回一个窗口, 是WindowSpecification对象,可以继续适用对象的方法往下继续查找控件
# eg: 微信主界面 app.window(class_name='WeChatMainWndForPC')

app.windows(**kwargs) # 根据筛选条件返回一个窗口列表,无条件默认全部,列表项为wrapped(装饰器)对象,可以使用wrapped对象的方法,注意不是WindowSpecification对象
# eg:[<uiawrapper.UIAWrapper - '李渝的早报 - Google Chrome', Pane, -2064264099699444098>]

app.kill(soft=False) # 强制关闭
app.cpu_usage() # 返回指定秒数期间的CPU使用率百分比
app.wait_cpu_usage_lower(threshold=2.5, timeout=None, usage_interval=None) # 等待进程CPU使用率百分比小于指定的阈值threshold
app.is64bit() # 如果操作的进程是64-bit,返回True

控件的定位和可用方法

层级查找控件的方法:定位控件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 通过层级查找控件相关方法
window(**kwargs) # 用于窗口的查找
child_window(**kwargs) # 可以无视层级的找后代中某个符合条件的元素===>【最常用】
parent() # 返回此元素的父元素,没有参数
children(**kwargs) # 返回符合条件的子元素列表,支持索引,是BaseWrapper对象(或子类)
iter_children(**kwargs) # 返回子元素的迭代器,是BaseWrapper对象(或子类)
descendants(**kwargs) # 返回符合条件的所有后代元素列表,是BaseWrapper对象(或子类)
iter_children(**kwargs) # 符合条件后代元素迭代器,是BaseWrapper对象(或子类)---> 存疑,是iter_descendants?

** kwargs的参数

# 常用的
class_name=None, # 类名
class_name_re=None, # 正则匹配类名
title=None, # 控件的标题文字,对应inspect中Name字段
title_re=None, # 正则匹配文字
control_type=None, # 控件类型,inspect界面LocalizedControlType字段的英文名
best_match=None, # 模糊匹配类似的title
auto_id=None, # inspect界面AutomationId字段,但是很多控件没有这个属性

# 不常用
parent=None,
process=None,# 这个基本不用,每次启动进程都会变化
top_level_only=True,
visible_only=True,
enabled_only=False,
handle=None,
ctrl_index=None,
found_index=None,
predicate_func=None,
active_only=False,
control_id=None,
framework_id=None,
backend=None,
  • 控件可用的方法属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# 以下几个只支持窗口模式的控件
dlg.close() # 关闭界面
dlg.minimize() # 最小化界面
dlg.maximize() # 最大化界面
dlg.restore() # 将窗口恢复为正常大小,比如最小化的让他正常显示在桌面
dlg.get_show_state() # 正常0,最大化1,最小化2
dlg.menu_select() # 菜单栏,eg:app.window.menu_select(Edit -> Replace)
dlg.exists(timeout=None, retry_interval=None) # 判断是否存在
#timeout:等待时间,一般默认5s
#retry_interval:timeout内重试时间
dlg.wait(wait_for, timeout=None, retry_interval=None) # 等待窗口处于特定状态
dlg.wait_not(wait_for_not, timeout=None, retry_interval=None) # 等待窗口不处于特定状态,即等待消失
# wait_for/wait_for_not:
# * 'exists' means that the window is a valid handle
# * 'visible' means that the window is not hidden
# * 'enabled' means that the window is not disabled
# * 'ready' means that the window is visible and enabled
# * 'active' means that the window is active
# timeout:等待多久
# retry_interval:timeout内重试时间
# eg: dlg.wait('ready')

# 鼠标键盘操作,只列举了常用形式,他们有很多默认参数但不常用,可以在源码中查看
ctrl.click_input() # 最常用的点击方法,一切点击操作的基本方法(底层调用只是参数不同),左键单击,使用时一般都使用默认不需要带参数
ctrl.right_click_input() # 鼠标右键单击
ctrl.type_keys(keys, pause = None, with_spaces = False,) # 键盘输入,底层还是调用keyboard.send_keys
# keys:要输入的文字内容
# pause:每输入一个字符后等待时间,默认0.01就行
# with_spaces:是否保留keys中的所有空格,默认去除0
ctrl.double_click_input(button ="left", coords = (None, None)) # 左键双击
ctrl.press_mouse_input(coords = (None, None)) # 指定坐标按下左键,不传坐标默认左上角
ctrl.release_mouse_input(coords = (None, None)) # 指定坐标释放左键,不传坐标默认左上角
ctrl.move_mouse_input(coords=(0, 0)) # 将鼠标移动到指定坐标,不传坐标默认左上角
ctrl.drag_mouse_input(dst=(0, 0)) # 将ctrl拖动到dst,是press-move-release操作集合

# 控件的常用属性
ctrl.children_texts() # 所有子控件的文字列表,对应inspect中Name字段
ctrl.window_text() # 控件的标题文字,对应inspect中Name字段
# ctrl.element_info.name
ctrl.class_name() # 控件的类名,对应inspect中ClassName字段,有些控件没有类名
# ctrl.element_info.class_name
ctrl.element_info.control_type # 控件类型,inspect界面LocalizedControlType字段的英文名
ctrl.is_child(parent) # ctrl是否是parent的子控件
ctrl.legacy_properties().get('Value') # 可以获取inspect界面LegacyIAccessible开头的一系列字段,在源码uiawraper.py中找到了这个方法,非常有用

# 控件常用操作
ctrl.draw_outline(colour='green') # 空间外围画框,便于查看,支持'red', 'green', 'blue'
ctrl.print_control_identifiers(depth=None, filename=None) # 以树形结构打印其包含的元素,详见打印元素
# depth:打印的深度,缺省时打印最大深度。
# filename:将返回的标识存成文件(生成的文件与当前运行的脚本在同一个路径下)
ctrl.scroll(direction, amount, count=1,) # 滚动
# direction :"up", "down", "left", "right"
# amount:"line" or "page"
# count:int 滚动次数
ctrl.capture_as_image() # 返回控件的 PIL image对象,可继续使用其方法如下:
# eg: ctrl.capture_as_image().save(img_path)
ret = ctrl.rectangle() # 控件上下左右坐标,(L430, T177, R1490, B941),可输出上下左右
# eg: ret.top=177
# ret.bottom=941
# ret.left=430
# ret.right=1490

实战

  • 用UISpy定位,记事本

不用微信演示,经过测试微信应该做了类似的屏蔽,很多内容获取不到,比如用打印当前窗口的所有controller(控件和属性)win_main_Dialog.print_control_identifiers(depth=None, filename=None),报错了

image-20220930153132566

  • 测试代码
1
2
3
4
5
6
from pywinauto.application import Application
# 启动记事本
app = Application(backend = "uia").start(r'notepad.exe')
# 找到记事本主窗口
win = app.window(title_re=".*记事本*.")
win.child_window(class_name="Edit").type_keys("hello")
  • 还有一种方法是可以打印主窗口控件列表,然后你可以通过控件操作这个对象
1
2
3
4
5
6
7
8
from pywinauto.application import Application
# 启动记事本
app = Application(backend = "uia").start(r'notepad.exe')
# 找到记事本主窗口
win = app.window(title_re=".*记事本*.")
print('-----------------')
# 打印主窗口控件列表,然后你可以通过控件id操作这个对象
win.print_control_identifiers()
  • 打印的值的格式大概如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Control Identifiers:

Dialog - '无标题 - 记事本' (L267, T123, R1707, B794)
['无标题 - 记事本Dialog', 'Dialog', '无标题 - 记事本']
child_window(title="无标题 - 记事本", control_type="Window")
|
| Edit - '文本编辑器' (L278, T198, R1696, B749)
| ['', 'Edit', '0', '1']
| child_window(title="文本编辑器", auto_id="15", control_type="Edit")
| |
| ...
|
| Menu - '应用程序' (L278, T168, R1696, B197)
| ['应用程序Menu', '应用程序', 'Menu2']
| child_window(title="应用程序", auto_id="MenuBar", control_type="MenuBar")
| |
| | MenuItem - '文件(F)' (L278, T168, R350, B197)
| | ['文件(F)', 'MenuItem2', '文件(F)MenuItem']
| | child_window(title="文件(F)", control_type="MenuItem")
| |
| | MenuItem - '编辑(E)' (L350, T168, R422, B197)
| | ['编辑(E)MenuItem', 'MenuItem3', '编辑(E)']
| | child_window(title="编辑(E)", control_type="MenuItem")
| |
| | MenuItem - '格式(O)' (L422, T168, R499, B197)
| | ['MenuItem4', '格式(O)MenuItem', '格式(O)']
| | child_window(title="格式(O)", control_type="MenuItem")
| |
| | MenuItem - '查看(V)' (L499, T168, R573, B197)
| | ['MenuItem5', '查看(V)MenuItem', '查看(V)']
| | child_window(title="查看(V)", control_type="MenuItem")
| |
| | MenuItem - '帮助(H)' (L573, T168, R649, B197)
| | ['帮助(H)', '帮助(H)MenuItem', 'MenuItem6']
| | child_window(title="帮助(H)", control_type="MenuItem")
  • 调整后的代码为
1
2
3
4
5
6
7
8
9
10
11
12
from pywinauto.application import Application
# 启动记事本
app = Application(backend = "uia").start(r'notepad.exe')
# 找到记事本主窗口
win = app.window(title_re=".*记事本*.")
print('-----------------')
# 打印主窗口控件列表
win.print_control_identifiers()
# win.child_window(class_name="Edit").type_keys("hello")
# 操作对应窗口下面的组件
win.Edit.type_keys('hello')
win['Edit'].type_keys('world')

image-20220930154435316

  • 如果打印全部控件内容太多,也可以打印部分
1
2
3
4
5
6
7
8
from pywinauto.application import Application
# 启动记事本
app = Application(backend = "uia").start(r'notepad.exe')
# 找到记事本主窗口
win = app.window(title_re=".*记事本*.")
print('-----------------')
dlg = win['Edit']
dlg.print_control_identifiers()
  • 对应打印的内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
dit - '文本编辑器'    (L781, T200, R2199, B751)
['', 'Edit']
child_window(title="文本编辑器", auto_id="15", control_type="Edit")
|
| ScrollBar - '垂直滚动条' (L2173, T200, R2199, B751)
| ['垂直滚动条', '垂直滚动条ScrollBar', 'ScrollBar']
| child_window(title="垂直滚动条", auto_id="NonClientVerticalScrollBar", control_type="ScrollBar")
| |
| | Button - '上一行' (L2173, T200, R2199, B226)
| | ['上一行Button', 'Button', '上一行', 'Button0', 'Button1']
| | child_window(title="上一行", auto_id="UpButton", control_type="Button")
| |
| | Button - '下一行' (L2173, T725, R2199, B751)
| | ['下一行', '下一行Button', 'Button2']
| | child_window(title="下一行", auto_id="DownButton", control_type="Button")
  • 两者结合使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import time

from pywinauto.application import Application
# 启动记事本
app = Application(backend = "uia").start(r'notepad.exe')
# 找到记事本主窗口
win = app.window(title_re=".*记事本*.")
print('-----------------')
# 打印主窗口控件列表
# win.print_control_identifiers()
# 记事本中输入内容
win.child_window(class_name="Edit").type_keys("hello")
# 选择菜单另存为
win.menu_select("文件->另存为")
time.sleep(2)
# 定位另存为窗口的输入框,并且输入内容;注意这里有三层,一层层往下面找
win.child_window(class_name='DUIViewWndClassName', control_type='Pane').\
child_window(class_name='AppControlHost',control_type='ComboBox', auto_id='FileNameControlHost').\
child_window(class_name='Edit').type_keys('hello')
# 点击保存
win[u"保存"].click_input()

image-20220930165315916

  • 优化下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import time

from pywinauto.application import Application

def driver():
app = Application(backend="uia").start(r"notepad.exe")
return app

def test():
# 启动记事本
app = driver()
# 找到记事本主窗口
win = app.window(title_re=".*记事本*.")
print('-----------------')
# 打印主窗口控件列表
# win.print_control_identifiers()
# 记事本中输入内容
win.Edit.type_keys("hello")
# 选择菜单另存为
win.menu_select("文件->另存为")
time.sleep(2)
# 定位另存为窗口的输入框,并且输入内容;注意这里有三层,一层层往下面找
win.child_window(class_name='DUIViewWndClassName', control_type='Pane').\
child_window(class_name='AppControlHost',control_type='ComboBox', auto_id='FileNameControlHost').\
child_window(class_name='Edit').type_keys('hello')
# 点击保存
win[u"保存"].click_input()
time.sleep(2)
# 关闭窗口
win.close()

test()

pywin32

  • pywin32 包含 win32gui、win32api、win32con 3个子模块,主要用于窗口管理(定位窗口、显示和关闭窗口、窗口前置、窗口聚焦、获取窗口位置等),通常用的较多的是 win32gui,因此本文仅对此子模块进行介绍
  • 安装
1
pip install pywin32
  • 常用的事件
1
2
3
4
5
6
7
8
9
10
11
12
#查找窗口句柄
win32gui.FindWindow()
#查找指定窗口的菜单
win32gui.GetMenu()
#查找某个菜单的子菜单
win32gui.GetSubMenu()
#获得子菜单的ID
win32gui.GetMenuItemID()
#获得某个子菜单的内容
win32gui.GetMenuItemInfo()
#给句柄发送通知(点击事件)
win32gui.PostMessage() 
  • 实例代码打开记事本,点击文件另存为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import win32gui, win32con, win32api
import time

# 打开记事本
win32api.ShellExecute(0,"open","notepad.exe","","",1)

# 等待记事本窗口打开
time.sleep(1)

# 寻找记事本窗口
notepad = win32gui.FindWindow("Notepad", None)

# 若为非 0 的值,代表有找到窗口
if (notepad != 0):

# 设置记事本标题
win32gui.SendMessage(notepad, win32con.WM_SETTEXT, None, "Hello")

# 寻找输入文字的地方
edit = win32gui.FindWindowEx(notepad, None, "Edit", None)

# 输入文字
win32gui.SendMessage(edit, win32con.WM_SETTEXT, None, "您好!Pythonn")
# 插入一个回车
win32gui.PostMessage(edit, win32con.WM_KEYDOWN, win32con.VK_RETURN, 0)

menu_handle = win32gui.GetMenu(notepad)
# 这里的代码分块逻辑,搞不明白
for index in [0,3]:
cmd_ID = win32gui.GetMenuItemID(menu_handle, 3)
menu_handle = win32gui.GetSubMenu(menu_handle, 0)

# 点击另存为
win32gui.PostMessage(notepad, win32con.WM_COMMAND, cmd_ID, 0)

放弃对这个模块的研究,写用例非常累

pyautogui

  • pyautogui 模块主要用于屏幕控制(获取屏幕尺寸、截屏等)、鼠标控制(移动鼠标、单击、双击、右击、拖拽、滚动等)、键盘控制(编辑、按键等)
  • 主要是图片识别

几个特有功能

  • 移动鼠标并在其他应用程序的窗口中单击
  • 向应用程序发送击键(例如,填写表单)。
  • 截取屏幕截图,并给出一个图像(例如,按钮或复选框的图像),然后在屏幕上找到它。
  • 找到应用程序的窗口,然后移动、调整大小、最大化、最小化或关闭它(目前仅限 Windows)。
  • 显示警报和消息框。

前置参数

1
2
3
4
5
import pyautogui 
# 停顿功能
pyautogui.PAUSE = 1 # 调用在执行动作后暂停的秒数,只能在执行一些pyautogui动作后才能使用,建议用time.sleep
# 自动 防故障功能
pyautogui.FAILSAFE = True # 启用自动防故障功能,左上角的坐标为(0,0),将鼠标移到屏幕的左上角,来抛出failSafeException异常

鼠标操作

获取屏幕的宽度和高度

1
2
width, height = pyautogui.size() # 获取屏幕的宽度和高度
print(width, height)

获取鼠标当前位置

1
2
currentMouseX, currentMouseY = pyautogui.position() # 鼠标当前位置
print(currentMouseX, currentMouseY)

鼠标移动类操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# pyautogui.moveTo(x,y,持续时间) 在持续时间内 将光标移动到目标位置(x,y)
pyautogui.moveTo(100, 100, duration=0.25) # 移动到 (100,100)

#pyautogui.moveRel(xOffset,yxOffset,持续时间) 在持续时间内 将光标偏移 距离原始位置 xOffset,yxOffset 的位置
pyautogui.moveRel(50, 0, duration=0.25) # 从当前位置右移50像素

# 实现拖拽效果
pyautogui.mouseDown(740,73) #鼠标按下指定位置
pyautogui.moveRel(100,0,2) #移动/可以使用其他移动方法
pyautogui.mouseUp() # 鼠标抬起
#或者
pyautogui.dragTo(100,300,duration=1)
#或者
pyautogui.dragRel(100,300,duration=4)

鼠标滚动类操作

1
2
3
4
5
6
7
# scroll函数控制鼠标滚轮的滚动,amount_to_scroll参数表示滚动的格数。正数则页面向上滚动,负数则向下滚动
# pyautogui.scroll(clicks=amount_to_scroll, x=moveToX, y=moveToY)
# 默认从当前光标位置进行滑动 amount_to_scroll是个数字 数字太小效果可能不明显, 正数表示往上划 负数表示往下化
pyautogui.scroll(500, 20, 2)
pyautogui.scroll(100) # 向上滚动100格
pyautogui.scroll(-100) # 向下滚动100格
pyautogui.scroll(100, x=100, y=100) # 移动到(100, 100)位置再向上滚动100格

鼠标点击类操作

1
2
3
4
5
6
7
8
9
# pyautogui.click(x,y,clicks=点击次数,interval=每次点击间隔频率,button=可以是left表示左击 可以是right表示右击 可以是middle表示中击)
pyautogui.click(10, 20, 2, 0.25, button='left')
pyautogui.click(x=100, y=200, duration=2) # 先移动到(100, 200)再单击
pyautogui.click() # 鼠标当前位置点击一下
pyautogui.doubleClick() # 鼠标当前位置左击两下
pyautogui.doubleClick(x=100, y=150, button="left") # 鼠标在(100,150)位置左击两下
pyautogui.tripleClick() # 鼠标当前位置左击三下
pyautogui.rightClick(10,10) # 指定位置,双击右键
pyautogui.middleClick(10,10) # 指定位置,双击中键

键盘操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 相关操作
# pyautogui.typewrite(要输入的字符只能是英文,interval=输入每个字符的间隔频率)
pyautogui.typewrite('python', 1)
# typewrite 还可以传入单字母的列表
# 运行下面代码,编辑器里面就会输出 python 之后换行。
pyautogui.typewrite(['p','y','t','h','o','n','enter'])

# pyautogui.keyDown():模拟按键按下
# pyautogui.keyUP():模拟按键松开
# pyautogui.press(键盘按键字母) 模拟一次按键过程,即 keyDown 和 keyUP 的组合 按下指定的键盘按键
# pyautogui.hotkey("ctrl","a") 实现组合键功能

# 按住 shift 按键,然后再按住 1 按键,就可以了。用 pyautogui 控制就是
pyautogui.keyDown('shift')
pyautogui.press('1')
pyautogui.keyUp('shift')


# 输入中文字符的方法 借用 pyperclip模块
import pyperclip
pyperclip.copy("要书写的字符串") #复制字符串
time.sleep(2)
pyautogui.hotkey("ctrl","v") #实现复制


# pyautogui.KEYBOARD_KEYS数组中就是press(),keyDown(),keyUp()和hotkey()函数可以输入的按键名称
pyautogui.KEYBOARD_KEYS = ['\t', '\n', '\r', ' ', '!', '"', '#', '$', '%', '&', "'", '(', ')', '*', '+', ',', '-', '.',
'/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '@',
'[', '\\', ']', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',
'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~',
'accept', 'add', 'alt', 'altleft', 'altright', 'apps', 'backspace', 'browserback',
'browserfavorites', 'browserforward', 'browserhome', 'browserrefresh', 'browsersearch',
'browserstop', 'capslock', 'clear', 'convert', 'ctrl', 'ctrlleft', 'ctrlright', 'decimal',
'del', 'delete', 'divide', 'down', 'end', 'enter', 'esc', 'escape', 'execute', 'f1', 'f10',
'f11', 'f12', 'f13', 'f14', 'f15', 'f16', 'f17', 'f18', 'f19', 'f2', 'f20', 'f21', 'f22',
'f23', 'f24', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'final', 'fn', 'hanguel', 'hangul',
'hanja', 'help', 'home', 'insert', 'junja', 'kana', 'kanji', 'launchapp1', 'launchapp2',
'launchmail', 'launchmediaselect', 'left', 'modechange', 'multiply', 'nexttrack',
'nonconvert', 'num0', 'num1', 'num2', 'num3', 'num4', 'num5', 'num6', 'num7', 'num8', 'num9',
'numlock', 'pagedown', 'pageup', 'pause', 'pgdn', 'pgup', 'playpause', 'prevtrack', 'print',
'printscreen', 'prntscrn', 'prtsc', 'prtscr', 'return', 'right', 'scrolllock', 'select',
'separator', 'shift', 'shiftleft', 'shiftright', 'sleep', 'space', 'stop', 'subtract', 'tab',
'up', 'volumedown', 'volumemute', 'volumeup', 'win', 'winleft', 'winright', 'yen', 'command',
'option', 'optionleft', 'optionright']

弹窗操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import pyautogui

# 显示一个简单的带文字和OK按钮的消息弹窗。用户点击后返回button的文字。
pyautogui.alert(text='', title='', button='OK')
b = pyautogui.alert(text='要开始程序么?', title='请求框', button='OK')
print(b) # 输出结果为OK

# 显示一个简单的带文字、OK和Cancel按钮的消息弹窗,用户点击后返回被点击button的文字,支持自定义数字、文字的列表。
pyautogui.confirm(text='', title='', buttons=['OK', 'Cancel']) # OK和Cancel按钮的消息弹窗
pyautogui.confirm(text='', title='', buttons=range(10)) # 10个按键0-9的消息弹窗
a = pyautogui.confirm(text='', title='', buttons=range(10))
print(a) # 输出结果为你选的数字

# 可以输入的消息弹窗,带OK和Cancel按钮。用户点击OK按钮返回输入的文字,点击Cancel按钮返回None。
pyautogui.prompt(text='', title='', default='')

# 样式同prompt(),用于输入密码,消息用*表示。带OK和Cancel按钮。用户点击OK按钮返回输入的文字,点击Cancel按钮返回None。
pyautogui.password(text='', title='', default='', mask='*')

图像操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import pyautogui
im = pyautogui.screenshot() # 返回屏幕的截图,是一个Pillow的image对象
im.save('屏幕截图.png') #保存图片
# 或者
im = pyautogui.screenshot('屏幕截图.png') # 截全屏并设置保存图片的位置和名称
print(im) # 打印图片的属性

# 不截全屏,截取区域图片。截取区域region参数为:左上角XY坐标值、宽度和高度
pyautogui.screenshot('屏幕截图.png', region=(0, 0, 300, 400))


# 获得文件图片在现在的屏幕上面的坐标,返回的是一个元组(top, left, width, height)
# 如果截图没找到,pyautogui.locateOnScreen()函数返回None
a = pyautogui.locateOnScreen(r'目标图片路径')
print(a) # 打印结果为Box(left=0, top=0, width=300, height=400)
x, y = pyautogui.center(a) # 获得文件图片在现在的屏幕上面的中心坐标
print(x, y) # 打印结果为150 200
# 或者
x, y = pyautogui.locateCenterOnScreen(r'目标图片路径') # 这步与上面的四行代码作用一样
print(x, y) # 打印结果为150 200

# 匹配屏幕所有与目标图片的对象,可以用for循环和list()输出
for pos in pyautogui.locateAllOnScreen(r'C:\Users\ZDH\Desktop\PY\region_screenshot.png'):
print(pos)
# 打印结果为Box(left=0, top=0, width=300, height=400)
a = list(pyautogui.locateAllOnScreen(r'C:\Users\ZDH\Desktop\PY\region_screenshot.png'))
print(a) # 打印结果为[Box(left=0, top=0, width=300, height=400)]

分辨率适配

注意:pyautogui的图像识别是模板匹配算法 无法跨分辨率识别(图片放大缩小就无法识别) 提供以下图像识别算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# -*- coding: utf-8 -*-
"""
使用需求:
需要安装 airtest pip install airtest -i https://mirrors.aliyun.com/pypi/simple/
运行时如果出现以下错误:
import win32api
ImportError: DLL load failed: 找不到指定的程序。
重新安装win32api版本
pip install pywin32==227 # 安装 227版本
不行的话再试试
pip install pywin32==223 # 安装 223版本

"""


import sys
import types
from copy import deepcopy
from airtest import aircv
from airtest.aircv import cv2
from airtest.aircv.template_matching import TemplateMatching
from airtest.core.cv import MATCHING_METHODS, Predictor
from airtest.core.error import InvalidMatchingMethodError
from airtest.core.helper import logwrap, G
from airtest.core.win.screen import screenshot
from airtest.utils.transform import TargetPos
from six import PY3
from airtest.core.settings import Settings as ST # noqa

# # -*- encoding=utf8 -*-
import logging
logger = logging.getLogger("airtest")
logger.setLevel(logging.ERROR)
# 日志级别有[DEBUG]、[INFO]、[WARNING] 和 [ERROR]

class Template(object):
"""
picture as touch/swipe/wait/exists target and extra info for cv match
filename: pic filename
target_pos: ret which pos in the pic
record_pos: pos in screen when recording
resolution: screen resolution when recording
rgb: 识别结果是否使用rgb三通道进行校验.
scale_max: 多尺度模板匹配最大范围.
scale_step: 多尺度模板匹配搜索步长.
"""

def __init__(self, filename, threshold=None, target_pos=TargetPos.MID, record_pos=None, resolution=(), rgb=False, scale_max=800, scale_step=0.005):
self.filename = filename
# self.filename =os.path.join(Settings.Picture_Path,filename)
self._filepath = None
self.threshold = threshold or ST.THRESHOLD
self.target_pos = target_pos
self.record_pos = record_pos
self.resolution = resolution
self.rgb = rgb
self.scale_max = scale_max
self.scale_step = scale_step

@property
def filepath(self):

return self.filename

def __repr__(self):
filepath = self.filepath if PY3 else self.filepath.encode(sys.getfilesystemencoding())
return "Template(%s)" % filepath

def match_in(self, screen):
match_result = self._cv_match(screen)
G.LOGGING.debug("match result: %s", match_result)
if not match_result:
return None
focus_pos = TargetPos().getXY(match_result, self.target_pos)
return focus_pos

def match_all_in(self, screen):
image = self._imread()
image = self._resize_image(image, screen, ST.RESIZE_METHOD)
return self._find_all_template(image, screen)

@logwrap
def _cv_match(self, screen):
# in case image file not exist in current directory:
ori_image = self._imread()
image = self._resize_image(ori_image, screen, ST.RESIZE_METHOD)
ret = None
for method in ST.CVSTRATEGY:
# get function definition and execute:
func = MATCHING_METHODS.get(method, None)
if func is None:
raise InvalidMatchingMethodError("Undefined method in CVSTRATEGY: '%s', try 'kaze'/'brisk'/'akaze'/'orb'/'surf'/'sift'/'brief' instead." % method)
else:
if method in ["mstpl", "gmstpl"]:
ret = self._try_match(func, ori_image, screen, threshold=self.threshold, rgb=self.rgb, record_pos=self.record_pos,
resolution=self.resolution, scale_max=self.scale_max, scale_step=self.scale_step)
else:
ret = self._try_match(func, image, screen, threshold=self.threshold, rgb=self.rgb)
if ret:
break
return ret

@staticmethod
def _try_match(func, *args, **kwargs):
G.LOGGING.debug("try match with %s" % func.__name__)
try:
ret = func(*args, **kwargs).find_best_result()
except aircv.NoModuleError as err:
G.LOGGING.warning("'surf'/'sift'/'brief' is in opencv-contrib module. You can use 'tpl'/'kaze'/'brisk'/'akaze'/'orb' in CVSTRATEGY, or reinstall opencv with the contrib module.")
return None
except aircv.BaseError as err:
G.LOGGING.debug(repr(err))
return None
else:
return ret

def _imread(self):
return aircv.imread(self.filepath)

def _find_all_template(self, image, screen):
return TemplateMatching(image, screen, threshold=self.threshold, rgb=self.rgb).find_all_results()

def _find_keypoint_result_in_predict_area(self, func, image, screen):
if not self.record_pos:
return None
# calc predict area in screen
image_wh, screen_resolution = aircv.get_resolution(image), aircv.get_resolution(screen)
xmin, ymin, xmax, ymax = Predictor.get_predict_area(self.record_pos, image_wh, self.resolution, screen_resolution)
# crop predict image from screen
predict_area = aircv.crop_image(screen, (xmin, ymin, xmax, ymax))
if not predict_area.any():
return None
# keypoint matching in predicted area:
ret_in_area = func(image, predict_area, threshold=self.threshold, rgb=self.rgb)
# calc cv ret if found
if not ret_in_area:
return None
ret = deepcopy(ret_in_area)
if "rectangle" in ret:
for idx, item in enumerate(ret["rectangle"]):
ret["rectangle"][idx] = (item[0] + xmin, item[1] + ymin)
ret["result"] = (ret_in_area["result"][0] + xmin, ret_in_area["result"][1] + ymin)
return ret

def _resize_image(self, image, screen, resize_method):
"""模板匹配中,将输入的截图适配成 等待模板匹配的截图."""
# 未记录录制分辨率,跳过
if not self.resolution:
return image
screen_resolution = aircv.get_resolution(screen)
# 如果分辨率一致,则不需要进行im_search的适配:
if tuple(self.resolution) == tuple(screen_resolution) or resize_method is None:
return image
if isinstance(resize_method, types.MethodType):
resize_method = resize_method.__func__
# 分辨率不一致则进行适配,默认使用cocos_min_strategy:
h, w = image.shape[:2]
w_re, h_re = resize_method(w, h, self.resolution, screen_resolution)
# 确保w_re和h_re > 0, 至少有1个像素:
w_re, h_re = max(1, w_re), max(1, h_re)
# 调试代码: 输出调试信息.
G.LOGGING.debug("resize: (%s, %s)->(%s, %s), resolution: %s=>%s" % (
w, h, w_re, h_re, self.resolution, screen_resolution))
# 进行图片缩放:
image = cv2.resize(image, (w_re, h_re))
return image

if __name__ == '__main__':
"""
用法:
res = Template(目标图片路径,threshold=匹配阈值,target_pos=可以是123456789 分别对应图片的九个点).match_in(screenshot(None))
"""
res = Template("pppp.png",threshold=0.8,target_pos=5).match_in(screenshot(None))
print(res)

实战

  • 安装
1
pip install PyAutoGUI
  • 测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import time
import pyautogui
import os

# 打开记事本
os.popen("notepad.exe")
time.sleep(2)
# 查找图片
coords = pyautogui.locateOnScreen('1.png')
# 得到图片的中心坐标
pos = pyautogui.center(coords)
# 移动鼠标
pyautogui.moveTo(pos, duration=0.5)
# 点击
pyautogui.click()
# 输入内容
pyautogui.typewrite('I like Python.')
  • 查找的1.png图片如下

image-20221008161852609

  • 测试结果

image-20221008161814486

sikulix

  • 基于图像识别的开源工具,支持:

    • Windows XP, 7, 8 and 10 (development on Windows 10)

    • Mac OSX 10.10 and later (development on macOS 10.15)

    • Linux/Unix systems depending on the availability of the prerequisites

  • 有自己的IDE

环境搭建

  • java的环境
1
2
3
4
C:\Users\Administrator>java --version
java 17.0.2 2022-01-18 LTS
Java(TM) SE Runtime Environment (build 17.0.2+8-LTS-86)
Java HotSpot(TM) 64-Bit Server VM (build 17.0.2+8-LTS-86, mixed mode, sharing)
  • 打开官网地址下载sikuli的jar包,现在最新版本为sikulixide-2.0.5-win.jar

  • 运行jar,打开ide

1
2
3
java -jar d:\sikulixide-2.0.5-win.jar

java -jar d:\sikulixide-2.0.5-win.jar -v -c

实战

  • 打开ide后,写入测试的代码,运行正常,发现一个问题左侧并没有显示常用的api(包括使用快捷键ctrl+t都无反应)

image-20221008184205087

基于python使用sikuli图像识别

  • 由于Python不能直接调用Java的方法,需要借助一些第三方的库比如Jython、Jpype、Pyjnius等
  • 安装依赖包
1
2
3
4
5
6
7
C:\Users\Administrator>pip install JPype1
Collecting JPype1
Downloading JPype1-1.4.0-cp37-cp37m-win_amd64.whl (343 kB)
---------------------------------------- 343.9/343.9 kB 484.9 kB/s eta 0:00:00
Requirement already satisfied: typing-extensions in d:\app\python37\lib\site-packages (from JPype1) (3.7.4.3)
Installing collected packages: JPype1
Successfully installed JPype1-1.4.0
  • 测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# coding=utf-8
import time

from jpype import *
import jpype
# 启动java虚拟机,调用sikulixide
if not jpype.isJVMStarted():
jpype.startJVM(jpype.getDefaultJVMPath(),'-ea',r'-Djava.class.path=D:\sikulixide-2.0.5-win.jar')
# 调用java的方法
java.lang.System.out.println("hello world")
# 获取对应的类,对应api:https://sikulix-2014.readthedocs.io/en/latest/appclass.html#App
app =JClass('org.sikuli.script.App')
app.open(r"c:\windows\System32\notepad.exe")
# 对应api https://sikulix-2014.readthedocs.io/en/latest/screen.html#Screen
Screen = JClass('org.sikuli.script.Screen')
screen=Screen()
time.sleep(2)
screen.click(r"D:\project\pc-auto\1.png")
screen.type("hello")

jpype.shutdownJVM()
  • Python调用jar包需要借助于jvm虚拟机,然后需要指定sikulixapi的安装路径,然后将jvm的路径添加到系统的path中(否则会提示jvm无法启动),然后在下方声明一个screen,app类,都是api中的类,然后就可以根据上方的一些方法进行调用

总结

  • 如果使用sikuli和PyAutoGUI,最好配合cv2(opencv)进行不同分辨率适配(这里没有测试)

  • 也有人推荐使用Ranorex ,不过这个是收费的

  • 当然还有一些其他场景没有测试,比如嵌入浏览器的场景