Featured image of post 一种基于uiautomator2的小猿口算速刷方法

一种基于uiautomator2的小猿口算速刷方法

专职欺负小朋友:<)

基本原理

将题目文本从界面ui通过xpath提取后计算出答案,然后用uiautomator2 swipe的方式将其绘制再屏幕上

经过500题的性能测试,本项目再大多情况下可以维持再1.0s每题左右的速度。

实现方法

获取题目文本

通过xpath实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@logger.logit(level=logger.INFO, msg="Getting text from UI element")
def getText(parent_id, device):
    """
    获取指定ID父元素下所有二级子元素的文本

    :param parent_id: 父元素的资源ID
    :return: 二级子元素文本列表
    """
    assert device is not None, "Device must be provided"
    # 构建父元素的XPath
    parent_xpath = f'//*[@resource-id="{parent_id}"]'
    # 获取所有二级子元素(直接子元素的直接子元素)
    secondary_children = device.xpath(f'{parent_xpath}/*/*').all()
    # 提取文本内容
    texts = []
    for child in secondary_children:
        # 获取元素信息
        info = child.info
        # 优先获取text属性,其次获取contentDescription
        text = info.get('text', '')
        if text:
            texts.append(text)
    results = ["".join(t.split()) for t in texts]
    return results

输出

通过将数字、符号转化为矢量点阵储存再静态文件中,再在输出时进行缩放,计算出每个笔画的坐标,从而绘制字符。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 数字定义 (0-9)
NUMBER_0 = [[(0,0), (0,1), (1,1), (1,0), (0,0)]]  # 单笔画矩形
NUMBER_1 = [[(0.5,0), (0.5,1)]]                   # 单竖线
NUMBER_2 = [[(0,0), (0.6,0),(0.55,0.65), (0,1), (1,1)]]  # 之字形
NUMBER_3 = [[(0,0),(1,0),(0,0.5),(1,0.5),(1,1),(0,1)]]
NUMBER_4 = [[(0.5,0), (0,0.5), (1,0.5)], [(0.6,0), (0.4,1)]]  # 双笔画(T形+竖线)
NUMBER_5 = [[(0,0),(0,0.5), (1,0.5), (1,1), (0,1)],[(0,0),(1,0)]]  # 倒S形
NUMBER_6 = [[(1,0), (0,0.3), (0,1), (1,1), (1,0.7), (0,0.3)]]  # 环形
NUMBER_7 = [[(0,0), (1,0), (0.5,1)]]              # 上横+斜线
NUMBER_8 = [[(0,0),(1,1),(0,1),(1,0),(0,0)]]
NUMBER_9 = [[(1,0),(0,0),(0.5,0.3),(1,0),(1,1)]]

# 符号定义
GREATER_THAN = [[(0,0),(1,0.5),(0,1)]]
LESS_THAN = [[(1,0),(0,0.5),(1,1)]]
SCORE_LINE = [[(0,0.5), (1,0.5)]]               # 水平中线
POINT = [[(0.85,0.85), (0.85,0.9)]]             # 小数点(右下角短竖线)
EQUAL_TO = [[(0,0.4),(1,0.4)], [(0,0.6),(1,0.6)]]  # 双水平线

通过matplotlib对字符进行检验

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
if __name__ == "__main__":
    import matplotlib.pyplot as plt
    def draw_character(definition, title=""):
        fig, ax = plt.subplots(figsize=(2,2))
        ax.invert_yaxis()  # 翻转Y轴(左上角为原点)
        ax.set_title(title, fontsize=12)
        for path in definition:
            x, y = zip(*path)
            ax.plot(x, y, 'bo-', linewidth=3, markersize=6)
            ax.set_xlim(-0.1, 1.1)
            ax.set_ylim(1.1, -0.1)  # 保持左上角原点
            ax.set_aspect('equal')
            ax.axis('off')
        plt.show()
    for i in range(10):
        draw_character(globals()[f'NUMBER_{i}'], title=f'NUM {i}')
    draw_character(GREATER_THAN, title='GT')
    draw_character(LESS_THAN, title='LT')
    draw_character(SCORE_LINE, title='SL')
    draw_character(EQUAL_TO, title='EQ')
    draw_character(POINT, title='PT')

进行绘制

  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
class Inputer:
    def __init__(self, device=None):
        if device is None:
            self.device = u2.connect()
        else:
            self.device = device
        self.device_height = self.device.info['displayHeight']
        self.device_width = self.device.info['displayWidth']
        self.canvas_bounds = [[100, 100], [
            self.device_width-100, self.device_height-100]]
        self.canvas_bound_0 = (
            self.canvas_bounds[0][0]+50, self.canvas_bounds[0][1]+50)
        self.canvas_bound_1 = (
            self.canvas_bounds[1][0]-50, self.canvas_bounds[1][1]-50)
        self.canvas_width = self.canvas_bound_1[0] - self.canvas_bound_0[0]
        self.canvas_height = self.canvas_bound_1[1] - self.canvas_bound_0[1]

    def getPoint(self, route: list, upperleft, height, width):
        """
        获取路径点
        :param route: 路径定义
        :param upperleft: 画布左上角坐标
        :param height: 画布高度
        :param width: 画布宽度
        :return: 路径点列表
        """
        points = [(upperleft[0]+i[0]*width+random.randint(-10, 10),
                   upperleft[1]+i[1]*height+random.randint(-10, 10)) for i in route]
        return points

    def getLayout(self, answer):
        """
        获取输入布局
        :param answer: 输入的答案
        :return: 输入布局列表
        """
        layout = []
        match answer:
            case int() | float():
                # 数字或小数
                answer_str = str(answer)
                length = len(answer_str)
                char_width = min(self.canvas_width / length, 200)
                char_height = self.canvas_height
                indent = 10
                for char in answer_str:
                    char_upperleft = (self.canvas_bound_0[0] + self.canvas_width / 2 - length * char_width / 2 + char_width * (len(layout) + 0.5)+indent*len(layout),
                                      self.canvas_bound_0[1])
                    if char.isdigit():
                        layout.append([(self.getPoint(
                            i, char_upperleft, char_height, char_width)) for i in NUMBERS[int(char)]])
                    elif char == '.':
                        layout.append(
                            [(self.getPoint(i, char_upperleft, char_height, char_width)) for i in route.POINT])
                    else:
                        raise ValueError(f"未知字符: {char}")
            case [_, _]:
                # 分数
                numerator, denominator = answer
                length = max(len(str(numerator)), len(str(denominator)))
                char_width = min(self.canvas_width / length, 200)
                char_height = self.canvas_height / 3
                indent = 10
                # 分子
                for char in str(numerator):
                    char_upperleft = (self.canvas_bound_0[0] + self.canvas_width / 2 - length * char_width / 2 + char_width * (len(layout))+indent*len(layout),
                                      self.canvas_bound_0[1])
                    if char.isdigit():
                        layout.append([(self.getPoint(
                            i, char_upperleft, char_height, char_width)) for i in NUMBERS[int(char)]])
                    else:
                        raise ValueError(f"未知字符: {char}")
                # 分数线
                line_upperleft = (self.canvas_bound_0[0],
                                  self.canvas_bound_0[1] + self.canvas_height / 3)
                layout.append([(self.getPoint(i, line_upperleft, char_height,
                              self.canvas_width)) for i in route.SCORE_LINE])
                # 分母
                for char in str(denominator):
                    char_upperleft = (self.canvas_bound_0[0] + self.canvas_width / 2 - length * char_width / 2 + char_width * (len(layout) - len(str(numerator)) - 1)+indent*(len(layout) - len(str(numerator)) - 1),
                                      self.canvas_bound_0[1] + self.canvas_height / 3 * 2)
                    if char.isdigit():
                        layout.append([(self.getPoint(
                            i, char_upperleft, char_height, char_width)) for i in NUMBERS[int(char)]])
                    else:
                        raise ValueError(f"未知字符: {char}")
            case str():
                # 符号
                char_width = self.canvas_width/2
                char_height = self.canvas_height/2
                char_upperleft = (self.canvas_bound_0[0] + self.canvas_width / 2 - char_width / 2,
                                  self.canvas_bound_0[1] + self.canvas_height / 2 - char_height / 2)
                if answer == '>':
                    layout.append([(self.getPoint(
                        i, char_upperleft, char_height, char_width)) for i in route.GREATER_THAN])
                elif answer == '<':
                    layout.append([(self.getPoint(
                        i, char_upperleft, char_height, char_width)) for i in route.LESS_THAN])
                elif answer == '=':
                    layout.append(
                        [(self.getPoint(i, char_upperleft, char_height, char_width)) for i in route.EQUAL_TO])
                else:
                    raise ValueError(f"未知符号: {answer}")
            case _:
                raise ValueError(f"不支持的答案类型: {type(answer)}")
        return layout

    @logger.logit(level=logger.INFO, msg="Drawing input layout on device")
    def draw(self, layout):
        """
        在设备上绘制输入布局
        :param layout: 输入布局列表
        """
        for char in layout:
            for path in char:
                self.device.swipe_points(path, 0.015)
                logger.log(
                    f"Swiped path: {[(i[0], self.device_height-i[1]) for i in path]}", logger.DEBUG)
        return True
使用 Hugo 构建
主题 StackJimmy 设计