节约"阳寿"——某电商618活动自动化
0x1、引言
对于电商来说,夏天 中最重要的日子莫过于——618大促,看看这类APP的LOGO就知道了~
又是一大波蹲点抢券,各种凑凑凑,签到、关注店铺、加购商品等 价格歧视 策略。
最近某电商刚开展618分19亿的活动,依旧是那些 简单无聊但费阳寿的任务,不薅白不薅,问了群里的小伙伴,貌似还没有脚本:
不过,虽然能用,但还不够好,小结那里也列出问题了:
其中最大的问题还是,任务判定用了第三方的OCR API,白嫖有次数限制,脚本做不了几天就歇菜了,索性用新的思路重新写一个吧~
0x2、从OCR文本匹配判定 → 图片相似度判定
之前判定执行什么任务的流程:
- 判断Airtest匹配到的 "去完成" 图标对应的哪个任务描述;
- 生成屏幕截图,裁剪此任务描述区域;
- 调用百度OCR提取文本,遍历Task匹配词;
- 命中Task,点击去完成按钮,并执行后续操作;
简单粗暴,就是每个任务都要消耗一次识别,很快就会达到每个月的白嫖上限。而实际上,任务描述区域这块:
是 固定不变的,完全可以这样做:
- 定好五个截图的坐标区域;
- 生成屏幕截图后,裁剪出这五个区域,然后划分判定;
- 任务判定时,拿要判定的任务区域的截图,进行相似度对比;
- 大于一个特定值,如0.9,说明是某类任务,然后执行后续操作即可;
① 生成屏幕截图
利用Airtest的截图API生成截图
# 生成屏幕截图
def gen_snapshot():
snapshot_path = os.path.join(crop_temp_dir, str(round(time.time() * 1000)) + ".jpg")
temp = snapshot(filename=snapshot_path)['screen']
return snapshot_path
② 获取任务描述区域的坐标
直接用画图软件打开截图,依次描出五个任务描述区域的坐标区域,如:
然后调API获取屏幕密度,根据横宽比例动态算一下实际上的坐标点:
# 获得屏幕分辨率
def get_screen_density():
return android.get_current_resolution()
# 动态计算生成任务描述信息区域列表
def cal_task_desc_area():
screen_density = get_screen_density()
horizontal_percent = math.ceil(screen_density[0] / default_density[0])
vertical_percent = math.ceil(screen_density[1] / default_density[1])
task_crop_x_start_cur = task_crop_x_start * horizontal_percent
task_crop_x_end_cur = (task_crop_x_start + task_crop_w_h[0]) * horizontal_percent
desc_area_list = []
for y_start in task_crop_y_start:
desc_area_list.append((task_crop_x_start_cur, y_start * vertical_percent, task_crop_x_end_cur,
(y_start + task_crop_w_h[1]) * vertical_percent))
return desc_area_list
接着就是编写裁剪的函数,然后裁剪一波了~
# 裁剪特定区域列表
def clip_area_list(snapshot_path, area_list):
img = Image.open(snapshot_path)
for task_desc in area_list:
region = img.crop(task_desc)
save_path = os.path.join(crop_temp_dir, "crop_" + str(round(time.time() * 1000))) + ".jpg"
region.save(save_path)
task_desc_crop_path_list.append(save_path)
img.close()
logger.info("截图裁剪完毕...")
if __name__ == '__main__':
cp_file_utils.is_dir_existed(crop_temp_dir)
# 连接设备
auto_setup(__file__, logdir=True, devices=["android://127.0.0.1:5037/替换成自己的设备id"])
logger.info("初始化完成...")
shot = gen_snapshot()
task_desc_area_list = cal_task_desc_area()
clip_area_list(shot, task_desc_area_list)
logger.info("输出截图文件路径:{0}".format(task_desc_crop_path_list))
运行等待裁剪完毕后,可以看到五个任务描述区域的截图:
然后先依次改名成t1.jpg,t2.jpg...,接下来找个稍微靠谱点的图片相似度算法了~
0x3、图片相似度算法测试
算法代码来源:《Python 五种图片相似度比较方法》
① 均值哈希算法
- 将图片大小调整为10x10,转灰度图;
- 求出平均灰度,大于平均灰度值更改为1,反之为0,生成哈希值;
- 对比两个图片矩阵的相似度,最后返回相似百分比;
代码如下:
# 均值哈希算法
def average_hash(img, shape=(10, 10)):
# 缩放为10*10
img = cv2.resize(img, shape)
# 转换为灰度图
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# s为像素和初值为0,hash_str为hash值初值为''
s = 0
hash_str = ''
# 遍历累加求像素和
for i in range(shape[0]):
for j in range(shape[1]):
s = s + gray[i, j]
# 求平均灰度
avg = s / 100
# 灰度大于平均值为1相反为0生成图片的hash值
for i in range(shape[0]):
for j in range(shape[1]):
if gray[i, j] > avg:
hash_str = hash_str + '1'
else:
hash_str = hash_str + '0'
return hash_str
② 差值哈希算法
- 将图片大小调整为10x11,转灰度图;
- 比较每行当前值与相邻的下一个值的大小,如果当前值较大,灰度值更改为1,反之为0,生成哈希值;
- 对比两个图片矩阵的相似度,最后返回相似百分比;
代码如下:
# 差值哈希算法
def difference_hash(img, shape=(10, 10)):
# 缩放10*11
img = cv2.resize(img, (shape[0] + 1, shape[1]))
# 转换灰度图
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
hash_str = ''
# 每行前一个像素大于后一个像素为1,相反为0,生成哈希
for i in range(shape[0]):
for j in range(shape[1]):
if gray[i, j] > gray[i, j + 1]:
hash_str = hash_str + '1'
else:
hash_str = hash_str + '0'
return hash_str
③ 感知哈希算法
- 将图片大小调整为32x32,转灰度图,进行离散余弦变换(dct)交换;
- opencv实现10x10掩码操作,并求出掩码区域均值,掩码区域像素值大于平均值掩码区域矩阵值设为1,反之为0;
- 对比两个图片矩阵的相似度,最后返回相似百分比;
代码如下:
# 感知哈希算法
def perception_hash(img):
# 缩放32*32
img = cv2.resize(img, (32, 32)) # , interpolation=cv2.INTER_CUBIC
# 转换为灰度图
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 将灰度图转为浮点型,再进行dct变换
dct = cv2.dct(np.float32(gray))
# opencv实现的掩码操作
dct_roi = dct[0:10, 0:10]
_hash = []
average = np.mean(dct_roi)
for i in range(dct_roi.shape[0]):
for j in range(dct_roi.shape[1]):
if dct_roi[i, j] > average:
_hash.append(1)
else:
_hash.append(0)
return _hash
④ 单通道直方图算法
- 使用rgb三个通道中的某个通道;
- 使用图像直方图的函数,直方图均衡化,计算出0-255的数值;
- 对比两个直方图的重合度,最后返回相似百分比;
代码如下:
# 单通道直方图算法
def single_channel_calculate(image1, image2):
hist1 = cv2.calcHist([image1], [0], None, [256], [0.0, 255.0])
hist2 = cv2.calcHist([image2], [0], None, [256], [0.0, 255.0])
# 计算直方图的重合度
degree = 0
for i in range(len(hist1)):
if hist1[i] != hist2[i]:
degree = degree + (1 - abs(hist1[i] - hist2[i]) / max(hist1[i], hist2[i]))
else:
degree = degree + 1
degree = degree / len(hist1)
return degree
⑤ 三直方图算法
- 将图片大小调整为256x256,并分离出rgb三个通道数组;
- 使用图像直方图函数,直方图均衡化,计算出0-255的数值;
- 对比两个直方图的重合度,最后返回相似百分比;
代码如下:
# 三通道直方图算法
def three_channel_calculate(image1, image2, size=(256, 256)):
# 将图像resize后,分离为RGB三个通道,再计算每个通道的相似值
image1 = cv2.resize(image1, size)
image2 = cv2.resize(image2, size)
sub_image1 = cv2.split(image1)
sub_image2 = cv2.split(image2)
sub_data = 0
for im1, im2 in zip(sub_image1, sub_image2):
sub_data += single_channel_calculate(im1, im2)
sub_data = sub_data / 3
return sub_data
不同算法相似度结果测试
测试前,还得先补上hash值比较的函数:
# hash值对比
def cmp_hash(hash1, hash2, shape=(10, 10)):
n = 0
# hash长度不同则返回-1代表传参出错
if len(hash1) != len(hash2):
return -1
# 遍历判断
for i in range(len(hash1)):
# 相等则n计数+1,n最终为相似度
if hash1[i] == hash2[i]:
n = n + 1
return n / (shape[0] * shape[1])
直接裁剪五个任务描述区域,然后跟样本对比,测试代码如下:
# 测试函数
def test_pic_match():
# 暂存目录
temp_save_dir = os.path.join(os.getcwd(), "temp")
cp_file_utils.is_dir_existed(temp_save_dir)
# 生成截图
snapshot_path = os.path.join(temp_save_dir, str(round(time.time() * 1000)) + ".jpg")
temp = snapshot(filename=snapshot_path)['screen']
img = Image.open(snapshot_path)
task_desc_crop_path_list = []
for task_desc in cal_task_desc_area():
region = img.crop(task_desc)
save_path = os.path.join(temp_save_dir, "crop_" + str(round(time.time() * 1000))) + ".jpg"
region.save(save_path)
task_desc_crop_path_list.append(save_path)
img.close()
logger.info("截图裁剪完毕,开始进行比对...")
default_save_dir = os.path.join(os.getcwd(), "default")
origin_default_img_list = []
for i in range(1, 6):
pic_path = os.path.join(default_save_dir, "t{0}.jpg".format(i))
origin_default_img_list.append(cv2.imread(pic_path))
for task_desc_crop_path in task_desc_crop_path_list[:1]:
img_temp = cv2.imread(task_desc_crop_path)
print("均值哈希算法,匹配相似度:")
for index, img in enumerate(origin_default_img_list):
print("{0} = t{1}.jpg:{2}".format(task_desc_crop_path, index + 1,
cmp_hash(average_hash(img_temp), average_hash(img))))
print("差值值哈希算法,匹配相似度:")
for index, img in enumerate(origin_default_img_list):
print("{0} = t{1}.jpg:{2}".format(task_desc_crop_path, index + 1,
cmp_hash(difference_hash(img_temp), difference_hash(img))))
print("感知哈希算法,匹配相似度:")
for index, img in enumerate(origin_default_img_list):
print("{0} = t{1}.jpg:{2}".format(task_desc_crop_path, index + 1,
cmp_hash(perception_hash(img_temp), perception_hash(img))))
print("单通道直方图算法,匹配相似度:")
for index, img in enumerate(origin_default_img_list):
print("{0} = t{1}.jpg:{2}".format(task_desc_crop_path, index + 1,
single_channel_calculate(img_temp, img)))
print("三通道直方图算法,匹配相似度:")
for index, img in enumerate(origin_default_img_list):
print("{0} = t{1}.jpg:{2}".format(task_desc_crop_path, index + 1,
three_channel_calculate(img_temp, img)))
测试结果如下:
em...好像有点不对劲,怎么差那么多?打开原图一对比:
卧槽?
裁剪区域对不上,能匹配上才怪,手动扣的坐标点着实不靠谱,得想个法子拿到更精确的值~
0x4、获取更准确的坐标区域
依次点击Android Studio菜单栏:Tools → Layout Inspector 打开布局分析工具:
好家伙,腾讯x5内核,尝试用TBS Studio 试试能否调试:
打扰了,不过其实我们也不需要调试应用,只需要获取所需的坐标而已,打开Chrome浏览器,输入:chrome://inspect/#devices
点击 inspect
,会弹出新窗口,静待片刻:
可以,这就拿到裁剪区域的宽高,203px和12px了,不过x,y坐标可不好拿啊,Console写一波抠脚JS:
// 拿到文字结点列表
var element_list = $x("//div[@class='jmdd_react_task_panel_list_subtitle task_panel_info_subtitle']")
// 定义获取结点x,y,w,h的函数
for(i in element_list) { console.log(element_list[i].getBoundingClientRect()) }
运行结果如下:
同时再根据这个坐标点裁剪,配合宽度自适应算出缩放比例:
可以看到,有了精确的坐标再裁剪,就非常稳妥了,此时再试下图片相似度的算法:
可以,很稳健,接着开始编写具体处理任务类~
0x5、编写不同的任务处理方式
打年兽那里已经写得很详细了,就不再复述了,只是加多了一个任务图片关联的字段:
然后就是死循环执行这个脚本了,目前只覆盖了五种任务,还是对无法处理的样本进行监测:
比如这个任务:
定位到截图:
果然,是没有覆盖到的任务,复制此截图,新建一种任务,然后在doing()处添加执行脚本,看下具体要做啥:
选中单选按钮,然后点击确认授权,这种页面一看就是原生的,直接用AirTestIDE的poco,双击选中结点,直接粗暴生成定位代码:
然后新建Task,按照具体逻辑编写代码即可:
最后,初始化任务列表时,把这个Task加上即可~
新的任务类型也是如此~
0x6、小结
本节小小地优化了一下之前打年兽的脚本,完整的实现思路已经有给出,相信读者照着也能写出自己的自动化jio本。代码写得非常乱,后续补齐剩余任务类型,异常状态处理再上传一波吧,感兴趣的可以先Star占坑:CPAuto
不过开发群里有小伙伴说昨天已经看到脚本了:
不知道是不是基于iPhone的快捷指令,去年打年兽也有,不过后面被和谐了,不用接电脑肯定香啊。不过,本节的自动化脚本也有它的优点:闷声发大财 ~
转载自:https://juejin.cn/post/7101266262046539812