现在chrome快一统天下了(firefox/safari/others蛇蛇发抖),所以本文主要关注chrome~
UI自动化
UI自动化是一门高深的技术个屁,其实就是怎么用代码控制用户界面,在功能上分为三个方面,状态感知,行为决策,行为执行。
- 状态感知:识别当前是什么状态,它分为两部分:获取状态数据和识别状态。状态数据可能是当前应用/屏幕的截图,或者是UI元素树,这一部分需要依赖软硬件提供的能力,如操作系统提供的截图API,或是UI树访问API(辅助功能);而识别状态就是找特征,如找子图,OCR识别关键文字,UI树选择器匹配(xpath/css selector等)
- 行为决策:就是根据当前和历史的状态决定接下来应该干嘛,对于UI,正常用户能执行的原子行为是有限的(鼠标,键盘,触控等),所以这里的行为类别是有限的,怎么生成接下来的行为可以用硬编码的代码去做,也可用硬编码的数据驱动,或进一步可以由AI生成
- 行为执行:生成行为后需要去执行,执行的结果可能会导致状态转移,但如何让软件生成的行为也依赖于软硬件提供的接口,例如系统级的辅助能力、事件生成器,应用级的调试、事件生成等都能导致动作被执行,甚至直接在目标里插桩也能实现动作执行
浏览器自动化介绍
常见工具
在浏览器自动化时,通常会使用Selenium、playwright、Puppeteer这三种工具,下面简单说明:
Selenium
最古老的框架,所以它生态成熟,社区资源丰富,支持多浏览器(Chrome/Firefox/Safari等)及多语言(Java/Python/C#/JS等),不过它本身不支持并发,自身的代码也不是很优雅,运行起来速度慢,需要独立安装webdriver,而且默认会有较多自动化的特征。
Puppeteer
这是chrome团队搞的,只支持JS开发去驱动Chrome,如果只是控制Chrome且喜欢JS语言写着还行,因为它的速度快,API简洁,否则直接Pass。
Playwright
虽然最新,但由大金主微软开发,它也支持多浏览器(Chrome/Firefox/Safari)和多语言(JS/TS/Python/Java/.Net),原生支持多浏览器上下文隔离,超多高级API可用,简直是居家旅行杀人越货必备(不过本文不会使用它)...
注:Chrome代表所有基于chromium的浏览器/组件,Safari代表所有基于Webkit的浏览器/组件。
除了纯浏览器,还经常遇到混合应用或模拟移动端访问,所以再列出两个常见的移动端自动化工具airtest和appium:
Airtest
网易出的自动化框架,支持Android/IOS/Windows的自动化测试,提供多种元素定位方式,如图像识别/OCR(要给钱,三行代码即可自己实现)/xpath等,为了截图方便还提供了自己的ide,反正上手难度极低,而且简单用起来效果还不错,但场景复杂了需要做很多适配。(不过那个源码就像玩具,有代码洁癖的不要去看)
注:对于控制ios需要装WebDriverAgent,编译方式可参考ios安装ios-target流程
Appium
它是更流行的移动应用自动化框架,同样支持Android/IOS/Windows,不过它是插件化开发的,使用WebDriver协议,所以理论上可以支持任何类型,也确实存在多种插件去支持各种客户端,甚至同个目标也存在多种实现的插件可供选择。它支持原生、混合及移动 Web 应用,支持多种语言开发(Java/Python/Ruby/C#/JavaScript/PHP等)...反正就是上限很高就是喽!
它采用CS架构,需要先运行其服务端,服务端会加载相关插件,如Android的UiAutomator2或IOS的XCUITest(WebDriverAgent),客户端请求服务端并提供控制目标,服务端使用对应插件处理(代理)操作。
注:WebDriverAgent最初是由facebook开发的,不过后来停止维护了,appium有个分支在持续维护,注意别下错版本了
几个角色
先从纯浏览器,以Selenium为例,说明几个角色:
1.Chrome/XXAPP:浏览器,webview等,实际发请求与网站交互的东东
2.WebDriver:浏览器无关的驱动浏览器的规范,各家浏览器实现它,比如chrome实现的是chromedriver,其通过cdp操作chrome
3.selenium:它根据webdriver的协议与webdriver通信,间接驱动浏览器
附:chromdriver下载地址: 114及以前的版本:https://chromedriver.storage.googleapis.com/index.html 113及之后的版本:https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json
chrome及其驱动更新十分频繁,如果使用过程中出现了问题可考虑更新版本。
Chrome
CDP
Chrome DevTools Protocol (CDP) 是 Chrome 浏览器提供的一套用于与浏览器进行交互的协议,它允许开发者通过编程方式控制 Chrome 或其他基于 Chromium 的浏览器,从而实现自动化测试、性能分析、调试等功能。CDP 支持的功能非常广泛,包括但不限于页面导航、DOM 操作、网络请求监控等,像chrome内置的DevTools实际就是使用了它。
它是基于WebSocket协议工作,定义了命令与事件,可在DevTools上打开协议监视器查看具体的交互信息,在GitHub最新版的代码及文档,还可进一步探索无处不在的awesome-chrome-devtools,它是控制Chrome的核心,需完全掌握!selenium的cdp使用的是trio-cdp/py-cdp,其它也有各自的实现。
注意:CDP是和window(tab)绑定的,每个tab都要创建一个对应的session,所以打开新的标签页后之前执行的cdp是不会主动对它起作用的,例如:
self.execute_cdp_command("Page.addScriptToEvaluateOnNewDocument", {"source": js_code, })
这段代码是在加载frame脚本前先执行这段JS,常被用于hook JS,它会在当前tab页的后续所有页面生效,但新打开的tab不受它控制!亦或者在抓取/拦截流量时,新打开的标签页也会脱离控制
webdriver-bidi
WebDriver BiDi (Bidirectional) 是一种新的协议,旨在提供一种更高效、更灵活的方式来与浏览器进行双向通信。它允许开发者通过编程方式控制和监控浏览器的行为,类似于 Chrome DevTools Protocol (CDP),但更加现代化和易于使用。WebDriver BiDi 的主要目标是改进现有的 WebDriver 协议,使其更适合现代 Web 开发的需求。相关文档可见w3c,selenium也实现了它,它主要是提效的,而chrome对其的支持能力还很有限,通常用在抓流量中!如selenium使用CDP抓流量时,拦截使用network的功能,捕获可使用profile的log功能,也可直接使用fetch功能,这些都会变成事件,此时就可以用bidi去收了,例如:
async def http_filter(self):
""" http 处理器,包括拦截与记录的功能
"""
async with self.driver.bidi_connection() as connection:
session, devtools = connection.session, connection.devtools
pattern = [
{"urlPattern": f"*baidu.com"},
{"urlPattern": f"*qiandu.com"},
]
pattern = map(devtools.fetch.RequestPattern.from_json, pattern)
await session.execute(devtools.fetch.enable(patterns=pattern)) # 设置网络事件过滤
await session.execute(devtools.network.enable()) # 开启网络追踪,所有期待的网络事件都会发送到客户端
listener = session.listen(devtools.fetch.RequestPaused, devtools.network.ResponseReceived)
async for event in listener:
if isinstance(event, devtools.fetch.RequestPaused):
url = event.request.url
if 'qiandu.com' in url:
try:
await session.execute(
devtools.fetch.fail_request(
event.request_id,
error_reason=devtools.network.ErrorReason.CONNECTION_REFUSED
)
)
except Exception as e:
logger.warning(f"丢弃千度出错 - {e}")
elif isinstance(event, devtools.network.ResponseReceived):
_type = event.type_
request_id = event.request_id
req_url = event.response.url
resp_code = event.response.status
rt = devtools.network.ResourceType
# 只关心需要的
if _type not in [rt.XHR, rt.DOCUMENT, rt.FETCH, rt.PREFETCH]:
continue
# 获取请求和响应体
try:
cdp_resp = await session.execute(devtools.network.get_request_post_data(request_id))
req_body = cdp_resp
except Exception as e:
req_body = None
try:
cdp_resp = await session.execute(devtools.network.get_response_body(request_id))
if cdp_resp[1]: # base64Encoded
resp_body = base64.b64decode(cdp_resp[0]).decode(errors='ignore')
else:
resp_body = cdp_resp[0]
except Exception as e:
resp_body = None
注:
1.它使用了trio库,可能会和asyncio等产生冲突。
- 拦截与捕获包还可使用代理来做,如selenium-wire利用mitmproxy来进行更细致的操作,不过由于涉及到证书,需要对浏览器做更多hook,否则它可能变成一个风控点。
electron
现在很多应用使用electron开发,它也可以用chromedriver去驱动,通常分析可使用debugtron去调试,而要改包(如启用多开功能)则需要重打包:
# 提取
npx asar extract app.asar ./unpacked
# 修改
# ...
# 安装为开发依赖
npm install --save-dev electron@20.0.0
# 调试启动
npx electron --inspect-brk=9229 .
chrome extension
要想操作chrome,除了用chromedriver/cdp/修改源码,还可以直接写扩展(extension),其实之前还支持native client(plugin)但是现在不再可用,这里专注于扩展,即平时应用商店下载的那玩意儿,CRX文件,它是一个压缩包,里面由资源文件(css/json/img...),html与js组成,可见其逻辑是由js完成的。
开发
可参考creating-chrome-extensions-with-typescript去使用typescript开发扩展,api可参考chrome extensions api ref,甚至有时不用写代码,比如要拦截一个网络请求,可以利用declarativeNetRequest,它只需要设置一些值就可以实现拦截操作。
写好的插件直接在chrome://extensions里,打开开发者模式,指定目录即可加载,也可使用该页面的打包功能生成crx,此时会要求私玥便于更新,如果没有它会自己生成,未发布的插件别人无法直接使用,可通过浏览器参数加载使用。
分析
打开商店找到想要分析的插件,url里一串字符就是它的id,可直接去crxextractor下载插件,下载后解压即可,安装过的插件也可以去 ~/Library/Application\ Support/Google/Chrome/Default/Extensions/{id}/打开。
注意版本:现在最新版是V3,老版本的插件已经不让用了
Android
混合应用
很多app不是纯native开发,而是套h5(hybrid),这种应用其实就是浏览器打开网页(webview),在android上也可以直接用selenium测试(google的儿子当然用chromedriver),由Remote debug Android devices和Remote debugging WebViews文档,这里以微信为例:
1.连接Android并配置好开发者权限
2.在微信里打开setWebContentsDebuggingEnabled,访问一次http://debugxweb.qq.com/?inspector=true即可,如果这种方式失效,可以通过hook等方式开启,比如WebViewPP就提到几种方式
3.查看cat /proc/net/unix | grep @得到@chrome_devtools_remote(若有chrome浏览器运行)和@webview_devtools_remote_<pid>,根据ps -ef|grep <pid>确定微信打开了调试
4.在浏览器中用chrome://inspect#devices就可以调试它了
5.也可以把它的端口映射出来adb forward tcp:5000 localabstract:webview_devtools_remote_<pid>,再用curl localhost:5000/json/version查看版本等信息
6.下载对应版本的chromedriver,直接下载PC版的就行,令其在PC上运行
7.运行后,就可以在代码里驱动它了,如下(参数含义见官方文档):
options = Options()
options.add_experimental_option("androidPackage", "com.tencent.mm")
options.add_experimental_option("androidUseRunningApp", True)
options.add_experimental_option("androidActivity", ".plugin.webview.ui.tools.WebViewUI") # 或 .plugin.webview.ui.tools.MMWebViewUI
options.add_experimental_option("androidProcess", "com.tencent.mm")
options.add_experimental_option("androidDeviceSerial", "932AY05YGZ") # 设备序号,adb devices可见
options.add_argument('--no-sandbox')
driver = webdriver.Chrome('/path/to/chromedriver', options=options)
得到driver后就可以像之前的代码一样操纵它了
注:企鹅开发的vConsole也能简单调试,使用方法就是想办法注入进去。
设置代理
1.直接用adb命令:
# add 设置立即生效
adb shell settings put global http_proxy 127.0.0.1:8888
# delete 删除重启生效 adb shell reboot 必须三条全部执行哦!
adb shell settings delete global http_proxy
adb shell settings delete global global_http_proxy_host
adb shell settings delete global global_http_proxy_port
# 或 它不必重启
adb shell settings put global http_proxy :0
2.使用AndroidProxySetter工具设置
为了保证连接的稳定性,最好使用网络连接adb(反直觉):
adb tcpip 5555 # 重启设备的adbd并将其绑定到5555端口,若存在多个设备可使用-s serial指定,下文同
adb shell ip addr show wlan0 # 查看ip地址
adb connect ip-address-of-device:5555 # 让adbs连接到adbd
adb devices # 验证
adb disconnect <device-ip-address> # 断开连接
纯chrome
如果要群控安卓的chrome,有两种方式,直接附加可直接使用chromdriver实现,但它限制极大,所以建议还是要配合appium+uiautomator2:
option = UiAutomator2Options()
option.app_activity = 'com.google.android.apps.chrome.Main'
option.browser_name = 'chrome' # appium默认上下文为native,这会自动切到chrome的第一个window上
option.platform_name = 'Android'
option.udid = self.device_serial # 序列号
option.new_command_timeout = 0 # 防止超时退出
option.chromedriver_executable = # 可设置chromedriver的路径
option.chrome_options = {'args': [
'--disable-popup-blocking', # 可设置各种启动参数,详见chrome文档,不过大部分都不会生效
]}
self.driver = webdriver.Remote('http://localhost:4723', options=option) # 连接到appium server
self.driver.implicitly_wait(self.WAIT_TIME)
这里它只提供了最基础的命令,实际控制chrome有个很重要的接口就是直接执行cdp命令,可以为appium的python库打补丁来实现该功能:
remote_commands = {
"executeCdpCommand": ("POST", f"/session/$sessionId/goog/cdp/execute"),
}
# 赋值给 AppiumConnection._commands
其它
随便记点不知道写哪里的,想一出是一出的东西~
架构设计
这是一门大学问,要兼具性能、扩展性和稳定性等,对于后两者,简单来说就是分层,一层套一层,底层封装UI原子操作,一定要提供丰富的操作参数,例如触控操作,要提供触控数组的每个时间点的位置,按压力度,按压范围等,而在中间层又要做一层抽象,再来一层写业务逻辑,再...反正之后合适的时间再慢慢谈~
抓包与改包
1.直接使用浏览器提供的接口,如chrome的cdp直接提供网络事件处理接口,注册就可以实现抓与改,但存在稳定性问题
2.若只拿数据,可以直接去解析dom树
3.使用mitmproxy等中间人手段 (访问http://mitm.it 安装证书)
注:抓的包存储时可用pcap/har(har viewer / har_analyzer)/txt等格式存储,也可以放es里便于分析
操作UI的技巧
shadow_root是dom内嵌,直接使用查询语法是获取不到元素的,需要先找到它的父元素,获取shadow_root作为子dom,再在它的基础上实现查询,selenium实现了该功能哦,就是在element.shadow_root就行,前提是创建时没有设置close参数,否则需要hook改参数。
airtest的使用技巧
定位方式
它默认只能通过子图找,有时子图没有明显的特征,就可以截一些额外部分,再根据子图的9个坐标去定位,或者对于文字类型,用OCR的效果会更好:
def find_element(self, element_location_str: Union[Template, str]) -> Optional[tuple[int, int]]:
if isinstance(element_location_str, Template):
return exists(element_location_str)
else:
snap = self._airtest_dev.snapshot()
ocr = CnOcr()
out = ocr.ocr(snap)
for line in out:
if element_location_str in line['text']:
position = line['position']
x_coordinates = position[:, 0]
y_coordinates = position[:, 1]
center_x = (np.max(x_coordinates) + np.min(x_coordinates)) // 2
center_y = (np.max(y_coordinates) + np.min(y_coordinates)) // 2
return center_x, center_y
return None
子图管理
它的另一个缺陷是只针对同一分辨率的手机,如果机型变了截图也会出问题,而且它的代码和资源混合难以管理,可参考安卓的资源管理方式,让它自动寻找合适的静态资源(子图等)
原子操作增强
例如它的滑动操作是很粗糙的,无法指定滑动轨迹等额外参数,但它根据不同的目标会使用不同的客户端去发出模拟的事件,可以增强它来实现所需功能
参考
1.深入理解 Chrome DevTools -- 匠心