likes
comments
collection
share

节约"阳寿"——某电商618活动自动化

作者站长头像
站长
· 阅读数 5

0x1、引言

对于电商来说,夏天 中最重要的日子莫过于——618大促,看看这类APP的LOGO就知道了~

节约"阳寿"——某电商618活动自动化

又是一大波蹲点抢券,各种凑凑凑,签到、关注店铺、加购商品等 价格歧视 策略。

最近某电商刚开展618分19亿的活动,依旧是那些 简单无聊但费阳寿的任务,不薅白不薅,问了群里的小伙伴,貌似还没有脚本:

节约"阳寿"——某电商618活动自动化

节约"阳寿"——某电商618活动自动化

不过,虽然能用,但还不够好,小结那里也列出问题了:

节约"阳寿"——某电商618活动自动化

其中最大的问题还是,任务判定用了第三方的OCR API,白嫖有次数限制,脚本做不了几天就歇菜了,索性用新的思路重新写一个吧~

节约"阳寿"——某电商618活动自动化


0x2、从OCR文本匹配判定 → 图片相似度判定

之前判定执行什么任务的流程:

  • 判断Airtest匹配到的 "去完成" 图标对应的哪个任务描述;
  • 生成屏幕截图,裁剪此任务描述区域;
  • 调用百度OCR提取文本,遍历Task匹配词;
  • 命中Task,点击去完成按钮,并执行后续操作;

节约"阳寿"——某电商618活动自动化

节约"阳寿"——某电商618活动自动化

简单粗暴,就是每个任务都要消耗一次识别,很快就会达到每个月的白嫖上限。而实际上,任务描述区域这块:

节约"阳寿"——某电商618活动自动化

固定不变的,完全可以这样做:

  • 定好五个截图的坐标区域;
  • 生成屏幕截图后,裁剪出这五个区域,然后划分判定;
  • 任务判定时,拿要判定的任务区域的截图,进行相似度对比;
  • 大于一个特定值,如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

② 获取任务描述区域的坐标

直接用画图软件打开截图,依次描出五个任务描述区域的坐标区域,如:

节约"阳寿"——某电商618活动自动化

然后调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))

运行等待裁剪完毕后,可以看到五个任务描述区域的截图:

节约"阳寿"——某电商618活动自动化

然后先依次改名成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)))

测试结果如下:

节约"阳寿"——某电商618活动自动化

em...好像有点不对劲,怎么差那么多?打开原图一对比:

节约"阳寿"——某电商618活动自动化

卧槽?

节约"阳寿"——某电商618活动自动化

裁剪区域对不上,能匹配上才怪,手动扣的坐标点着实不靠谱,得想个法子拿到更精确的值~


0x4、获取更准确的坐标区域

依次点击Android Studio菜单栏:ToolsLayout Inspector 打开布局分析工具:

节约"阳寿"——某电商618活动自动化

好家伙,腾讯x5内核,尝试用TBS Studio 试试能否调试:

节约"阳寿"——某电商618活动自动化

打扰了,不过其实我们也不需要调试应用,只需要获取所需的坐标而已,打开Chrome浏览器,输入:chrome://inspect/#devices

节约"阳寿"——某电商618活动自动化

点击 inspect,会弹出新窗口,静待片刻:

节约"阳寿"——某电商618活动自动化

可以,这就拿到裁剪区域的宽高,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()) }

运行结果如下:

节约"阳寿"——某电商618活动自动化

同时再根据这个坐标点裁剪,配合宽度自适应算出缩放比例:

节约"阳寿"——某电商618活动自动化

节约"阳寿"——某电商618活动自动化

可以看到,有了精确的坐标再裁剪,就非常稳妥了,此时再试下图片相似度的算法:

节约"阳寿"——某电商618活动自动化

可以,很稳健,接着开始编写具体处理任务类~

节约"阳寿"——某电商618活动自动化


0x5、编写不同的任务处理方式

打年兽那里已经写得很详细了,就不再复述了,只是加多了一个任务图片关联的字段:

节约"阳寿"——某电商618活动自动化

然后就是死循环执行这个脚本了,目前只覆盖了五种任务,还是对无法处理的样本进行监测:

节约"阳寿"——某电商618活动自动化

比如这个任务:

节约"阳寿"——某电商618活动自动化

定位到截图:

节约"阳寿"——某电商618活动自动化

果然,是没有覆盖到的任务,复制此截图,新建一种任务,然后在doing()处添加执行脚本,看下具体要做啥:

节约"阳寿"——某电商618活动自动化

选中单选按钮,然后点击确认授权,这种页面一看就是原生的,直接用AirTestIDE的poco,双击选中结点,直接粗暴生成定位代码:

节约"阳寿"——某电商618活动自动化

然后新建Task,按照具体逻辑编写代码即可:

节约"阳寿"——某电商618活动自动化

最后,初始化任务列表时,把这个Task加上即可~

节约"阳寿"——某电商618活动自动化

新的任务类型也是如此~

节约"阳寿"——某电商618活动自动化


0x6、小结

本节小小地优化了一下之前打年兽的脚本,完整的实现思路已经有给出,相信读者照着也能写出自己的自动化jio本。代码写得非常乱,后续补齐剩余任务类型,异常状态处理再上传一波吧,感兴趣的可以先Star占坑:CPAuto

不过开发群里有小伙伴说昨天已经看到脚本了:

节约"阳寿"——某电商618活动自动化

不知道是不是基于iPhone的快捷指令,去年打年兽也有,不过后面被和谐了,不用接电脑肯定香啊。不过,本节的自动化脚本也有它的优点:闷声发大财 ~

节约"阳寿"——某电商618活动自动化


转载自:https://juejin.cn/post/7101266262046539812
评论
请登录