安卓app自动化操作方案实现
PC群控实机方案
准备
python uiautomator2 实现自动化操作
使用weditor实现元素解析
使用adb+wifi实现手机与PC的通信
uiautomator2代码例子
class FlightWarning(object):
def __init__(self):
self.app_name = "com.umetrip.android.msky.app"
self.phone_ip = phone_ip
self.d = None
# 随机等待
@staticmethod
def human_delay(min=1.0, max=3.0):
time.sleep(random.uniform(min, max))
# 登录
def login(self):
res = {'flag': 'end', 'msg': '支付失败!', 'img': None, 'is_pay': False}
try:
# 通过设备序列号连接
d = u2.connect(self.phone_ip) # 192.168.90.93:15552
self.d = d
# d.debug = True # 启动内置调试器 http://localhost:7912
# d.app_start(app_name, stop=False)
# 通过包名启动
d.app_start(self.app_name)
# xpath定位
self.human_delay(min=3.0, max=5.0)
d.xpath(
'//*[@resource-id="com.umetrip.android.msky.app:id/home_bottom_bar"]/android.widget.LinearLayout[5]/android.widget.RelativeLayout[1]/android.widget.ImageView[1]').click()
self.human_delay()
d.xpath('//*[@content-desc="点击登录"]').click()
# 使用ADB命令输入文本
self.human_delay()
d.xpath('//*[@resource-id="com.umetrip.android.msky.app:id/regist_name_edit"]').click()
self.human_delay()
d.xpath('//*[@resource-id="com.umetrip.android.msky.app:id/regist_name_edit"]').set_text("")
d.shell(f"input text 账号")
self.human_delay()
d.xpath('//*[@resource-id="com.umetrip.android.msky.app:id/regist_pass_edit"]').click()
d.shell(f"input text 密码")
self.human_delay()
d.press("back")
d.xpath('//*[@resource-id="com.umetrip.android.msky.app:id/iv_agreement_image"]').click()
self.human_delay()
d.xpath('//*[@resource-id="com.umetrip.android.msky.app:id/login_button"]').click()
self.human_delay(min=3.0, max=5.0)
element = d.xpath('//*[@resource-id="com.umetrip.android.msky.app:id/strict_login_get_code"]')
if element.exists:
logger.info("需要短信验证码")
d.xpath('//*[@resource-id="com.umetrip.android.msky.app:id/strict_login_get_code"]').click()
time.sleep(3) # 等待短信验证码
code = '短信验证码'
d.shell(f"input text {code}")
self.human_delay()
d.xpath('//*[@resource-id="com.umetrip.android.msky.app:id/strict_login_done"]').click()
self.human_delay(min=3.0, max=5.0)
element = d.xpath('//*[@resource-id="com.umetrip.android.msky.app:id/regist_pass_edit"]')
if element.exists:
logger.info("再次输入密码")
d.xpath('//*[@resource-id="com.umetrip.android.msky.app:id/regist_pass_edit"]').click()
d.shell(f"input text 密码")
d.press("back")
d.xpath('//*[@resource-id="com.umetrip.android.msky.app:id/iv_agreement_image"]').click()
self.human_delay()
d.xpath('//*[@resource-id="com.umetrip.android.msky.app:id/login_button"]').click()
else:
logger.info("不需要短信验证码")
if d.xpath('//*[@content-desc="我的订单"]').wait(timeout=10): # 等待最多15秒
res['flag'] = 'success'
res['msg'] = '登录成功!'
logger.info(res['msg'])
else:
res['msg'] = '登录失败!'
logger.info(res['msg'])
file_path = f"登录.png"
d.screenshot(file_path)
return res
except Exception as e:
res['msg'] = f'登录-运行异常:{e}'
logger.error(f'登录-异常:{traceback.format_exc()}')
return res
# finally:
# try:
# d.app_stop(self.app_name) # 关闭app
# except Exception:
# pass
# 登出
def logout(self):
res = {'flag': 'end', 'msg': '支付失败!', 'img': None, 'is_pay': False}
try:
if not self.d.xpath('//*[@content-desc="我的订单"]').wait(timeout=4.5):
self.d.xpath(
'//*[@resource-id="com.umetrip.android.msky.app:id/home_bottom_bar"]/android.widget.LinearLayout[5]/android.widget.RelativeLayout[1]/android.widget.ImageView[1]').click()
self.d.xpath(
'//*[@resource-id="com.umetrip.android.msky.app:id/fl_weex_container"]/android.widget.FrameLayout[1]/android.widget.FrameLayout[2]/android.widget.FrameLayout[2]/android.widget.ImageView[2]').click()
self.d.xpath(
'//*[@resource-id="com.umetrip.android.msky.app:id/fl_weex_container"]/android.widget.FrameLayout[1]/android.widget.FrameLayout[1]/android.widget.FrameLayout[2]').click()
if self.d.xpath('//*[@resource-id="com.umetrip.android.msky.app:id/tv_loginAuthBtn"]').wait(
timeout=10): # 等待最多15秒
res['flag'] = 'success'
res['msg'] = '登出成功!'
logger.info(res['msg'])
else:
res['msg'] = '登出失败!'
logger.info(res['msg'])
file_path = f"登出.png"
self.d.screenshot(file_path)
return res
except Exception as e:
res['msg'] = f'登出-运行异常:{e}'
logger.error(f'登出-异常:{traceback.format_exc()}')
return res
# 查询
def query(self, depart, arrive, airline, phone_ip):
res = {'flag': 'end', 'msg': '查询预警-初始信息', 'flight_data': [], 'depart': depart, 'arrive': arrive,
'airline': airline}
try:
logger.info(f'查询预警-开始:{depart}->{arrive}->{airline}->{phone_ip}')
airline_cn = airline_schedule.get(airline)
if not airline_cn:
res['msg'] = f'查询预警-不支持航空公司:{airline}!'
return res
# 方法2:直接连接
try:
self.d = u2.connect(phone_ip)
except Exception:
# 如果失败,尝试重新初始化
self.d = u2.connect(phone_ip)
# d.debug = True # 启动内置调试器 http://localhost:7912
# d.app_start(app_name, stop=False)
# 通过包名启动
try:
self.d.screen_on()
self.d.swipe_ext("up")
except:
pass
self.human_delay(min=3.0, max=3.0)
try:
self.d.app_stop(self.app_name)
except Exception:
pass
self.human_delay(min=1.0, max=2.0)
self.d.app_start(self.app_name)
# xpath定位
self.human_delay(min=3.0, max=5.0)
self.d.xpath(
'//*[@resource-id="com.umetrip.android.msky.app:id/home_bottom_bar"]/android.widget.LinearLayout[4]/android.widget.RelativeLayout[1]/android.widget.ImageView[1]').click()
self.human_delay()
self.d.xpath('//*[@content-desc="按起降地"]').click()
self.human_delay(min=1.0, max=3.0)
# 输入出发机场
self.d.click(0.181, 0.24)
self.human_delay(min=3.0, max=4.0)
self.d.xpath('//*[@text="输入城市或机场中文/英文/三字码"]').click()
self.human_delay(min=12.0, max=15.0)
s_code = f"input text {depart}"
self.d.shell(s_code)
# self.d.send_keys(depart)
self.human_delay(min=4.0, max=5.0)
self.d.click(0.075, 0.158)
self.human_delay(min=2.0, max=3.0)
# 输入到达机场
self.d.click(0.838, 0.24)
self.human_delay(min=3.0, max=4.0)
self.d.xpath('//*[@text="输入城市或机场中文/英文/三字码"]').click()
self.human_delay(min=12.0, max=15.0)
s_code = f"input text {arrive}"
self.d.shell(s_code)
self.d.click(0.397, 0.097)
# self.d.shell(f"input text PVG")
# self.d.send_keys(arrive)
self.human_delay(min=4.0, max=5.0)
self.d.click(0.371, 0.161)
self.human_delay(min=3.0, max=4.0)
# 点击查询
self.d.click(0.498, 0.481)
# self.human_delay(min=4.0, max=5.0)
logger.info(f'输入出发到达完成时间:{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())}')
# 筛选zh航班 //*[@content-desc="筛选航班"] SZX
self.d.xpath('//*[@content-desc="筛选航班"]').click()
zh_flag = False
for _ in range(5):
# if self.d.xpath('//*[@content-desc="深圳航空"]').wait(timeout=3):
ele = self.d.xpath('//*[@content-desc="%s"]' % airline_cn)
if ele.exists:
logger.info(f"出现{airline_cn}")
zh_flag = True
break
else:
logger.info(f"没有出现{airline_cn}")
self.d.swipe_ext("up")
continue
if not zh_flag:
logger.info(f"没有出现{airline_cn}")
res['msg'] = f'查询预警-没有出现{airline_cn},预警信息!'
return res
# self.d.swipe_ext("up") # 上滑找到 深圳航空
self.d.xpath('//*[@content-desc="深圳航空"]').click()
self.d.xpath('//*[@content-desc="查看结果"]').click()
logger.info(f'筛选所需航班完成时间:{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())}')
# 航班数据 - 简化版本
flight_data = []
previous_bottom_hash = None
for i in range(4):
ele2 = self.d.xpath('//*[@content-desc="预警"]')
if ele2.exists:
for i2 in range(5):
result_ces = self.capture_element_screenshot(
f'//android.widget.ScrollView/android.widget.FrameLayout[1]/android.widget.FrameLayout[{i2 + 1}]/android.widget.FrameLayout[1]',
f'预警{str(i) + str(i2)}.jpg')
if result_ces['flag'] == 'error':
break
if result_ces['flag'] == 'success':
logger.info(f"{depart + '->' + arrive + '->' + airline}截图成功:{result_ces['img']}")
flight_data.append({"raw_text": '', 'data': {}, 'img': result_ces['img']})
# 滑动前获取底部区域哈希
current_hash = self.get_bottom_hash()
# 截图航班信息
self.d.swipe_ext("up")
self.human_delay(min=1.0, max=2.0)
# 如果是第一次循环,只保存哈希值不比较
if i == 0:
previous_bottom_hash = current_hash
continue
# 比较滑动前后的底部区域哈希
if current_hash == previous_bottom_hash:
logger.info(f"第{i + 1}次滑动后内容无变化,终止循环")
break
# 更新前一次的哈希值为当前哈希值
previous_bottom_hash = current_hash
if flight_data:
# 使用图片路径进行去重
seen_imgs = set()
unique_flight_data = []
for f in flight_data:
if f['img'] not in seen_imgs:
seen_imgs.add(f['img'])
unique_flight_data.append(f)
flight_data = unique_flight_data
for f in flight_data:
result_uo = self.umi_ocr(f['img'])
f['raw_text'] = result_uo['data']
result_pfd = self.parse_flight_data(f['raw_text'], depart, arrive)
if result_pfd['flag'] == 'success':
f['data'] = result_pfd['data']
res['flight_data'] = flight_data
res['flag'] = 'success'
res['msg'] = '查询预警-查询成功!'
logger.info(f'查询预警-返回数据:{res}')
return res
except Exception:
logger.info(f"查询预警-运行异常:{traceback.format_exc()}")
return res
finally:
try:
self.d.app_stop(self.app_name)
except Exception:
pass
def get_bottom_hash(self):
"""
获取屏幕底部区域的哈希值
"""
try:
screenshot = self.d.screenshot()
width, height = screenshot.size
# 截取屏幕底部20%的区域
bottom_region = screenshot.crop((0, int(height * 0.7), width, height))
buffer = BytesIO()
bottom_region.save(buffer, format='PNG')
return hashlib.md5(buffer.getvalue()).hexdigest()
except Exception as e:
logger.error(f"获取底部哈希时出错: {e}")
return None
# 解析数据
def parse_flight_data(self, flight_str, depart, arrive):
res = {'flag': 'end', 'msg': '解析数据-初始信息', "data": {}}
try:
flight_info = {
'航班号': '',
'出发机场': depart,
'到达机场': arrive,
'前序航班预计到达': '',
'前序航班已到达': '',
'航班状态': '预警',
'预计出发': '',
'预计到达': '',
'计划出发': '',
'计划到达': '',
}
# '深圳航空ZH18941实际承运CA1894 11:15宝安T3 预计11:57 前序航班已于09:14到达 13:55浦东T2 预计13:43 预警'
flight_num = 'ZH' + re.findall(r'深圳航空ZH(.{4})', flight_str)[0]
flight_info['航班号'] = 'ZH' + flight_num
if '前序航班' in flight_str:
if '前序航班已于' in flight_str:
previous_flight = re.findall(r'前序航班已于(\d{1,2}:\d{2})到达', flight_str)[0]
flight_info['前序航班已到达'] = previous_flight
exp = r'前序航班已于\d{1,2}:\d{2}到达 ' + r'(\d{1,2}:\d{2})'
flight_info['计划到达'] = re.findall(exp, flight_str)[0]
else:
previous_flight = re.findall(r'前序航班预计(\d{1,2}:\d{2})', flight_str)[0]
flight_info['前序航班预计到达'] = previous_flight
exp = f'前序航班预计{flight_info["前序航班预计到达"]}到达 ' + r'(\d{1,2}:\d{2})'
flight_info['计划到达'] = re.findall(exp, flight_str)[0]
else:
logger.info("没有前序航班!")
try:
exp = '深圳航空' + flight_num + ' ' + r'(\d{1,2}:\d{2})'
flight_info['计划出发'] = re.findall(exp, flight_str)[0]
except:
exp = r' (\d{1,2}:\d{2})'
flight_info['计划出发'] = re.findall(exp, flight_str)[0]
flight_info['预计出发'] = re.findall(r' 预计(\d{1,2}:\d{2})', flight_str)[0]
flight_info['预计到达'] = re.findall(r' 预计(\d{1,2}:\d{2})', flight_str)[1]
logger.info(flight_info)
res['flag'] = 'success'
res['data'] = flight_info
res['msg'] = '解析数据-成功!'
logger.info(f"{res['msg']}-{flight_info}")
return res
except Exception as e:
res['msg'] = f'解析数据-异常:{e}'
logger.info(f'解析数据-异常:{traceback.format_exc()}')
return res
# 捕捉元素截图
def capture_element_screenshot(self, xpath_expression, save_path) -> dict:
"""
对指定XPath元素进行截图
Args:
d: uiautomator2 设备实例
xpath_expression: XPath表达式
save_path: 截图保存路径
Returns:
bool: 是否成功截图
"""
res = {'flag': 'end', 'msg': '截图失败!', 'img': None}
try:
# 查找元素
element = self.d.xpath(xpath_expression).get()
if not element:
res['msg'] = f"截图失败-未找到元素: {xpath_expression}"
logger.info(res['msg'])
return res
# 获取元素的边界信息
bounds = element.info['bounds']
left = bounds['left']
top = bounds['top']
right = bounds['right']
bottom = bounds['bottom']
if (bottom - top) not in [254, 344]:
res['msg'] = "截图失败-元素高度不是标准高度跳过"
logger.info(res['msg'])
return res
# 获取整个屏幕的截图
screenshot = self.d.screenshot()
# 处理不同版本的返回值
if hasattr(screenshot, 'save'): # 已经是 PIL Image 对象
img = screenshot
else: # 是字节数据
img = Image.open(io.BytesIO(screenshot))
# 裁剪出元素的区域
element_img = img.crop((left, top, right, bottom))
# 将PIL Image保存到内存缓冲区(二进制数据)
img_byte = io.BytesIO()
element_img.save(img_byte, format='PNG')
img_byte_arr = img_byte.getvalue()
# 从二进制数据解码为OpenCV图像
image_array = np.frombuffer(img_byte_arr, dtype=np.uint8)
result_coe = self.check_order_exists(image_array,
r'yj_template.jpg')
if result_coe:
# 保存元素截图
element_img.save(save_path)
res['msg'] = f"截图保留成功:{save_path}"
res['img'] = base64.b64encode(img_byte_arr).decode('utf-8')
res['flag'] = 'success'
logger.info(res['msg'])
return res
else:
res['msg'] = "截图失败-不是预警航班!"
logger.info(res['msg'])
return res
except Exception as e:
logger.info(f"截图失败-运行异常: {traceback.format_exc()}")
res['msg'] = f"截图失败-运行异常:{e}"
res['flag'] = "error"
return res
@staticmethod
def check_order_exists(target, template_path, threshold=0.9):
# 读取图片(灰度模式)
# target = cv2.imread(target_path, 0)
target = cv2.imdecode(target, cv2.IMREAD_COLOR)
template = cv2.imread(template_path, cv2.IMREAD_COLOR)
logger.info(f"目标图片尺寸: {target.shape}")
logger.info(f"模板图片尺寸: {template.shape}")
# 模板匹配
res = cv2.matchTemplate(target, template, cv2.TM_CCOEFF_NORMED)
_, max_val, _, _ = cv2.minMaxLoc(res)
# 判断匹配度是否超过阈值
return max_val > threshold
@staticmethod
def image_to_base64(image_path):
# 打开图像文件
img = Image.open(image_path)
# 创建一个字节流缓冲区
buffered = BytesIO()
# 将图像保存到字节流中,格式为PNG
img.save(buffered, format="PNG")
# 获取字节流中的字节数据
img_bytes = buffered.getvalue()
# 将字节数据编码为Base64字符串
img_base64 = base64.b64encode(img_bytes).decode('utf-8')
return img_base64
# 识别图片信息
def umi_ocr(self, img_base64):
res = {'flag': 'end', 'msg': '识别失败!', 'data': None}
try:
url = 'http://127.0.0.1:1224/api/ocr'
# 使用示例
data = {
"base64": img_base64,
"options": {
"ocr.language": "models/config_chinese.txt",
"ocr.cls": True,
"ocr.limit_side_len": 4320,
"tbpu.parser": "multi_none",
"data.format": "text"
}
}
response = session.post(url, json=data, timeout=10)
# {'code': 100, 'data': '深圳航空ZH1894 1实际承运CA1894 11:15宝安T3 实际12:40 13:55浦东T2 实际14:39 到达', 'score': 0.9312354241098676, 'time': 0.08330988883972168, 'timestamp': 1758506588.0610397}
if response.json().get('code') != 100:
res['msg'] = f'识别失败:{response.json().get("data")}!'
return res
res['data'] = response.json()['data']
res['flag'] = 'success'
res['msg'] = '识别成功!'
return res
except Exception as e:
res['msg'] = f'识别异常:{e}'
return res
weditor进行页面分析
# 开启命令
python -m weditor




adb+wifi控制手机
编写的uiautomator2代码运行电脑和手机wifi必须为同一网络
# 查看手机ip

# 先执行 adb 命令 开启端口
adb devices
List of devices attached
FA7A51A03802 device
adb -s FA7A51A03802 tcpip 15556
restarting in TCP mode port: 15556
# 然后通过
d = u2.connect('192.168.90.1:15556') # 就可以完成对手机的连接
方案总结
需要自己编写控制调度程序,实机维护起来比较麻烦,对一些简单,执行次数较少的项目比较适用
脱离PC方案
准备
使用一些开源工具如: autojs等 完成对手机的自动化操作实现,及任务获取上传
我这里使用的是 hamibot
hamibot实现自动化操作 (注意 是基于autojs二开的项目,有人维护,但是使用要额外付费,源码需要存放在对方服务器)
其他库推荐: autox,auto6,autojs pro,EasyClick等
自己编写任务发布接收服务,用于发布和接收手机自动化结果
hamibot代码例子
// hamibot具体操作使用直接参考官网文档就行,写的挺详细的
function main_zh() {
log('开始执行脚本main_zh')
var new_task = {
'tid': '',
'airline': '',
'depart': '',
'arrive': '',
'rawImg': [],
'routeStatus': true,
'msg': '查询失败!',
'flag': 'fail',
'robotName': hamibot.robotName
};
let key = '通信加密key';
let packageName = "com.umetrip.android.msky.app"; // 替换为目标应用的包名
var src, res, regionImg, img_b64, body_json;
let specialAirport = ['PKX', 'SHA', 'CTU', 'WUX'];
auto.waitFor();
// log('当前运行设备:' + hamibot.robotName);
// 返回结果
function result_call(msg) {
new_task['msg'] = msg;
response = http.postJson('http://你的服务地址/submitResult', new_task, options = {
headers: {
'sing': hasher.hash(key + currentTime, 'md5'),
"Content-Type": "application/json"
},
})
if (response.statusCode !== 200) {
log('请求失败: ' + response.statusCode + ' ' + response.statusMessage);
return '提交结果失败'
} else {
log('请求成功: ' + response.statusCode + ' ' + response.statusMessage);
return '提交结果成功'
}
// hamibot.exit(); // 没获得授权,退出
}
function closeApp(appName) {
// 1. 获取应用包名
var name = getPackageName(appName); // 尝试通过应用名称获取包名
if (!name) {
// 如果未找到包名
if (getAppName(appName)) {
// 检查参数是否为包名
name = appName; // 确认参数是包名
} else {
return false; // 应用不存在返回失败
}
}
// 2. 进入应用设置页
app.openAppSetting(name); // 打开应用详情页
text(app.getAppName(name)).waitFor(); // 等待应用名称出现确认界面跳转
sleep(1500); // 界面加载延迟(可选)
// 3. 查找关闭控件
images.save(captureScreen(), '/storage/emulated/0/fullScreenImg.png', 'png', 100);
sleep(2000);
res = ocr.recognizeText(images.read('/storage/emulated/0/fullScreenImg.png'));
log("识别结果: " + res)
if (res.indexOf('强行停止') !== -1) {
log("识别结果: " + res)
// 检查控件是否可用
click(590, 503);
sleep(2500);
// 4. 处理确认弹窗
textMatches(/(.*确.*|.*定.*)/).findOne().click(); // 确认关闭操作
log(app.getAppName(name) + ' 已关闭');
sleep(1000);
back(); // 返回上级界面
} else {
log('无法关闭 ' + app.getAppName(name));
back();
}
}
// 获取当前时间并格式化为 YYYYMMDDHH 格式
function getCurrentTime() {
const now = new Date();
const year = now.getFullYear();
// 月份要+1,因为getMonth()返回0-11
const month = (now.getMonth() + 1).toString().padStart(2, '0');
const day = now.getDate().toString().padStart(2, '0');
const hours = now.getHours().toString().padStart(2, '0');
// 组合成 YYYYMMDDHH 格式
return year + month + day + hours;
}
// 获取后台任务 航线状态
const currentTime = getCurrentTime();
var response = http.get('http://你的服务地址/getTask?robotName=' + hamibot.robotName, {
headers: {
'sing': hasher.hash(key + currentTime, 'md5'),
},
});
if (response.statusCode !== 200) {
log('请求任务失败: ' + response.statusCode + ' ' + response.statusMessage);
// hamibot.exit(); // 没获得授权,退出
return '获取任务失败'
} else {
body_json = response.body.json()
log(body_json);
if (body_json['flag'] === 'success') {
new_task['airline'] = body_json['task']['航司']
new_task['depart'] = body_json['task']['出发']
new_task['arrive'] = body_json['task']['到达']
new_task['tid'] = body_json['tid']
log('获取任务成功: ' + body_json['tid']);
} else {
new_task['flag'] = 'fail';
new_task['msg'] = '请求任务失败!';
log('任务数据异常');
log(response.body.json());
// hamibot.exit(); // 没获得授权,退出
return '任务数据异常!'
}
}
if (!new_task['depart'] || !new_task['arrive']) {
log('任务数据异常2: ' + response.body.json());
// hamibot.exit(); // 没获得授权,退出
return '任务数据异常2!'
}
if (app.launchPackage(packageName)) {
log("应用启动成功");
} else {
log("应用启动失败");
result_call('应用启动失败!')
// hamibot.exit(); // 没获得授权,退出
}
// 启用线程,点击截图确认按钮
threads.start(function () {
const check = () => {
// OPPO ColorOS 适配
click('立即开始');
setTimeout(function () {
check();
}, 500);
};
check();
});
if (!requestScreenCapture()) {
// hamibot.exit(); // 没获得授权,退出
}
threads.shutDownAll(); // 结束线程
sleep(2000); // 可选,短暂延迟,避免截图包含确认框
// hamibot.exit(); // 没获得授权,退出
// // 针对第一次打开应用的一些列操作
images.save(captureScreen(), '/storage/emulated/0/fullScreenImg.png', 'png', 100);
res = ocr.recognizeText(images.read('/storage/emulated/0/fullScreenImg.png'));
// log(res);
// 注册认证航旅纵横后
if (res.indexOf('注册认证航旅纵横后') !== -1) {
log("第一次APP");
sleep(2000);
click('同意');
sleep(2000);
click(349, 1086);
sleep(5000);
click(429, 771);
sleep(5000);
click('残忍拒绝');
}
// 循环3次 检查动态
var foundDynamic = false;
for (let i = 0; i < 3; i++) {
// 每次循环都重新获取截图,避免图片被回收
sleep(1000);
images.save(captureScreen(), '/storage/emulated/0/fullScreenImg.png', 'png', 100);
log('开始识别,尝试 ' + (i + 1));
// 安全地裁剪图片
sleep(1000);
try {
src = images.read('/storage/emulated/0/fullScreenImg.png');
regionImg = images.clip(src, 465, 1242, 80, 36);
images.save(regionImg, '/storage/emulated/0/regionImg.png', 'png', 100);
if (!regionImg) {
log("图片裁剪失败");
sleep(1000);
continue;
}
} catch (e) {
log("图片裁剪错误: " + e);
sleep(1000);
continue;
}
// 识别文本 需要等图片保存好 不然识别为空
sleep(2500);
res = ocr.recognizeText(regionImg);
log(res);
// 检查是否包含"动态"
if (res.indexOf('立即升级') !== -1) {
log("点击立即升级");
longClick(329, 843);
sleep(5000);
}
if (res.indexOf('动态') !== -1) {
log("点击动态");
longClick(503, 1231);
foundDynamic = true;
break; // 跳出循环
} else {
log("动态按钮未加载,等待1秒后重试");
sleep(1000); // 等待1秒
}
// 回收图片资源
if (regionImg) {
try {
regionImg.recycle();
} catch (e) {
// 忽略回收错误
}
}
}
if (!foundDynamic) {
log("4次尝试后动态按钮仍未加载");
// 使用示例
closeApp(packageName);
result_call('4次尝试后动态按钮仍未加载!')
// hamibot.exit(); // 没获得授权,退出
}
sleep(2000);
log("点击起降地");
longClick(358, 177);
sleep(5000);
log("点击出发");
longClick(113, 306);
sleep(5000);
log("输入框");
longClick(340, 128);
sleep(5000);
log("输入文本");
setText(new_task['depart']);
sleep(1000);
longClick(583, 128);
sleep(5000);
log("点击第一个元素"); // 双机场要特殊处理
if (specialAirport.includes(new_task['depart'])) {
longClick(583, 289);
} else {
longClick(583, 209);
}
sleep(2500);
log("点击降落");
longClick(609, 302);
sleep(2000);
log("输入框");
longClick(340, 128);
sleep(3000);
log("输入文本");
setText(new_task['arrive']);
sleep(1000);
longClick(583, 134);
sleep(5000);
log("点击第一个元素"); // 双机场要特殊处理
if (specialAirport.includes(new_task['arrive'])) {
longClick(583, 289);
} else {
longClick(583, 209);
}
sleep(3000);
log("点击查询");
longClick(346, 614);
sleep(2500);
// 判读设备是否被封
images.save(captureScreen(), '/storage/emulated/0/fullScreenImg.png', 'png', 100);
src = images.read('/storage/emulated/0/fullScreenImg.png');
sleep(1000);
res = ocr.recognizeText(src);
// log('查看设备是否被封:', res);
// log(res);
if (res.indexOf('超过限制') !== -1) {
log("设备被封,请重试!");
// hamibot.exit(); // 没获得授权,退出
closeApp(packageName);
result_call('设备被封,请重试!')
}
if (res.indexOf('残忍拒绝') !== -1) {
log("设备被封,请重试!");
log("设备被封,请重试!");
click('残忍拒绝');
sleep(1000)
closeApp(packageName);
result_call('点击残忍拒绝,请重试!')
}
sleep(1000);
log("点击筛选");
longClick(196, 1182);
sleep(4500);
images.save(captureScreen(), '/storage/emulated/0/fullScreenImg.png', 'png', 100);
// 安全地裁剪图片
sleep(1000);
try {
src = images.read('/storage/emulated/0/fullScreenImg.png');
regionImg = images.clip(src, 465, 1242, 80, 36);
images.save(regionImg, '/storage/emulated/0/regionImg.png', 'png', 100);
if (!regionImg) {
log("图片裁剪失败");
sleep(1000);
}
} catch (e) {
log("图片裁剪错误: " + e);
sleep(1000);
}
// 识别文本 需要等图片保存好 不然识别为空
sleep(2500);
res = ocr.recognizeText(regionImg);
log(res);
// 检查是否包含"动态"
if (res.indexOf('动态') !== -1) {
log("还是出现动态");
closeApp(packageName);
new_task['routeStatus'] = true;
images.save(captureScreen(), '/storage/emulated/0/输入出发到达机场失败.png', 'png', 100);
result_call('输入出发到达机场失败!');
}
// 滑动点击深圳航空
var zh_flag = false;
var templ = images.read('/storage/emulated/0/zh.png');
for (let i = 0; i < 3; i++) {
images.save(captureScreen(), '/storage/emulated/0/fullScreenImg.png', 'png', 100);
src = images.read('/storage/emulated/0/fullScreenImg.png');
var p = findImage(src, templ);
if (p) {
log('找到啦:' + p);
click(p.x + 5, p.y + 5);
sleep(1000); // 可选,短暂延迟,避免截图包含确认框
click(531, 1150);
zh_flag = true;
break; // 跳出循环
} else {
log('没找到滑动一页');
swipe(413, 1065, 413, 614, 1000);
sleep(1000);
}
}
sleep(2500);
// 判断有没有 可用航班信息 计划 预计 无用航班信息 延误 到达 取消
if (!zh_flag) {
log("筛选没有ZH航班!");
closeApp(packageName);
result_call("筛选没有ZH航班!")
}
// 循环6次 预计航班
let templ_plan = images.read('/storage/emulated/0/预警模版.png');
for (let i = 0; i < 4; i++) {
// 每次循环都重新获取截图,避免图片被回收
sleep(1000);
images.save(captureScreen(), '/storage/emulated/0/fullScreenImg.png', 'png', 100);
log('开始识别,尝试 ' + (i + 1));
// 安全地裁剪图片
sleep(1000);
try {
src = images.read('/storage/emulated/0/fullScreenImg.png');
regionImg = images.clip(src, 582, 331, 111, 795);
images.save(regionImg, '/storage/emulated/0/regionImg.png', 'png', 100);
if (!regionImg) {
log("图片裁剪失败");
sleep(1000);
continue;
}
} catch (e) {
log("图片裁剪错误: " + e);
sleep(1000);
continue;
}
// 识别文本 需要等图片保存好 不然识别为空
sleep(2500);
res = ocr.recognizeText(regionImg);
log(res);
// 检查是否包含计划 预警 计划
if (res.indexOf('计划') !== -1 || res.indexOf('预警') !== -1) {
new_task['routeStatus'] = true;
log("存在计划航班,航线有效");
// 单独处理预警航班信息
if (res.indexOf('预警') !== -1) {
// 测试 预警模版位置 截取此航班信息进行ocr识别 待处理
images.save(captureScreen(), '/storage/emulated/0/fullScreenImg.png', 'png', 100);
sleep(500);
src = images.read('/storage/emulated/0/fullScreenImg.png');
sleep(2500);
var p2 = images.matchTemplate(src, templ_plan)
// 修正部分:正确使用 MatchingResult 对象
if (p2 != null && p2.points.length > 0) {
// 方法1:使用 points 属性遍历坐标点
p2.points.forEach(function (point, index) {
log('匹配点' + index + ': 坐标(' + point.x + ', ' + point.y + ')');
log('匹配点' + index + ': 坐标(' + point.x + ', ' + point.y + ')');
regionImg = images.clip(src, 0, point.y - 73, 681, 231);
images.save(regionImg, '/storage/emulated/0/预警' + i + index + '.png', 'png', 100);
img_b64 = images.toBase64(regionImg, "png", 100)
sleep(2000);
// 添加重复检查 - 方法1: 使用 includes 检查
if (!new_task['rawImg'].includes(img_b64)) {
new_task['rawImg'].push(img_b64);
// log(img_b64);
log('添加了新的图片,当前数量:' + new_task['rawImg'].length);
} else {
log('跳过重复图片');
}
});
// 计算坐标进行截图
// 处理完成进行翻页处理
log('处理完预计滑动一页');
swipe(343, 1128, 343, 668, 1000);
sleep(1000);
} else {
log('没找到滑动一页');
swipe(343, 1128, 343, 668, 1000);
sleep(1000);
}
} else {
log('当前页无预警滑动一页');
swipe(343, 1128, 343, 668, 1000);
}
} else {
log("未发现可用航班信息,滑动一页");
break
}
// 回收图片资源
if (regionImg) {
try {
regionImg.recycle();
} catch (e) {
// 忽略回收错误
}
}
}
if (!new_task['routeStatus']) {
log("无可用航班信息,废除此航线!");
closeApp(packageName);
result_call("无可用航班信息,废除此航线!")
}
new_task['flag'] = 'success';
log('任务id:' + new_task['tid'] + '查询完成!');
closeApp(packageName)
result_call('查询完成!');
};
main_zh();
任务调度原理
hamibot脚本预估执行时间,如1分钟执行完,在每次脚本开始,请求自己的任务发布服务(ip需要再公网可调用)获取任务,脚本执行完返回结果给自己服务,成任务的调用
方案总结
可以脱离对PC的依赖,通过自动化框架的app执行脚本,完成对任务的调度
缺点 hamibot源码在对方服务器保存,某些实现并不好用,某些功能可能要额外付费,可以尝试其他自动化框架进行替换
优点 有人维护项目比较稳定,文档比较详细清晰
此种方案可以在实机部署,也可以在云手机部署,灵活度更高,可以胜任更复杂的项目

浙公网安备 33010602011771号