本文为稀土技能社区首发签约文章,14天内制止转载,14天后未获授权制止转载,侵权必究!

0x1、导言

【杰哥带你玩转Android自动化】学亿点有备无患的

Hi,我是杰哥,上节《学穿ADB》 带着咱们:

对ADB相关的姿态进行了深入学习,并写了一个 “某办公软件打卡主动化” 的简略练手事例。

不知道读者朋友们,有没有跟着动手试试看,纸上得来终觉浅,绝知此事要躬行,收藏夹吃灰是大忌,没试过的赶紧玩起来!

文尾处说道:这个主动打卡脚本的完结过于简略粗暴 (定时 + adb包名发动运用),存在一系列问题。

而单靠ADB并不能解决,所以本节咱们再学点有备无患的 “主动化姿态“,拓宽认知,为下节主动打卡脚本的「赋能」做准备。

本节学习路线组织如下:

  • 了解下OCR文字辨认的相关概念,运用 pytesseract库chineseocr_lite 库辨认图片文字;
  • 了解音讯推送相关,运用 Server酱 推送音讯到微信;
  • 学习主动化中Python常用的 图片处理操作
  • 运用Android体系中的 system/bin/uiautomator.jar包,导出当时页面的一切控件信息,并对xml内容进行解析;

话不多说,直接开冲~

【杰哥带你玩转Android自动化】学亿点有备无患的


0x2、OCR文字辨认

OCR,Optical Character Recognition,译作 光学字符辨认,亦称 计算机文字辨认,指的是运用光学技能和计算机技能,对文本材料的图画文件进行剖析辨认处理,获取文字及版面信息的进程。

说人话便是:运用这项技能,从图片中直接提取文字

OCR技能是完结文字快速录入的一项关键技能,运用场景可就多了:

  • 车牌路牌辨认;
  • 证件卡片辨认;
  • 验证码辨认;
  • 古籍辨认;
  • PDF转Word等…

如今大部分手机和APP都自带OCR,读者们应该都玩过吧?没玩过的能够翻开微信体验下:点开图片 → 长按 → 提取文字:

【杰哥带你玩转Android自动化】学亿点有备无患的

简略介绍下 印刷体文字 的主要辨认流程 (详细讲解可见【独家】一文读懂文字辨认(OCR))

前期处理

  • 灰度化 → 彩色图画中每个像素的色彩由 RGB三个重量 决定,取值规模0-255,关于计算机来说,这样一个像素点会有256256256=16777216种色彩的改变规模。而灰度图是一种 RGB重量相同 的特别色图画,一个像素点的改变规模只要0-255这256种。图画灰度化的目的是为了简化矩阵,进步运算速度。
  • 二值化 → 将文字与布景进一步分开,将灰度值图画信号转化为只要黑(1)和白(0)的二值图画信号,可简略理解为 “是非化“,常用办法有:重量法、最大值法、均匀值法、加权均匀法等;
  • 降噪 → 依据噪点的特征进行去噪;
  • 歪斜校对 → 歪斜的文档图画对后期的字符切割、辨认和图画紧缩等工作会发生较大影响,为了确保后续处理的正确性,需求对文本图画进行歪斜检测和校对;
  • 大小规范化 → 将输入的恣意尺寸的文字都处理成统一尺寸的规范文字,以便与预先存储在字典中的参阅模板相匹配;
  • 图画滑润 → 去掉笔划上的孤立白点和笔划外部的孤立黑点,以及笔划边际的凹凸点,使得笔划边际变得滑润;

中期处理

  • 版面剖析 → 将文本图画切割为不同部分,并标定各部分属性,如:文本、图画、表格;
  • 版面理解 → 获取文章逻辑结构,包含各区域的逻辑属性、文章的层次联系和阅读顺序;
  • 图画切分 → 对文本图画进行切分,便利对单个文字进行辨认处理,有两个类型:行(列)切分和字切分;
  • 特征提取 → 从单个字符图画上提取统计特征或结构特征,匹配特征库中与待辨认文字相似度最高的文字;
  • 模型练习 → 从很多符号预料中主动学习出图画的特征;

辨认后处理

  • 版面康复 → 依据版面剖析和OCR的成果,重构出包含文字信息和版面信息的电子文档;
  • 辨认校对 → 特定的语言上下文的联系,对辨认成果进行校对;

上述内容看得一脸懵逼,云里雾里?定心,这些技能咱们无需把握,专业的工作交给专业的人,大约了解下就好。

【杰哥带你玩转Android自动化】学亿点有备无患的

咱们只关注 能提取图片文字的OCR东西这个东西该怎样用


① pytesseract (不引荐)

网上随手搜下关键字 Python OCR辨认文字,一堆烂大街的教程都是教你用 pytesseract 这个基于Google开源的 Tesseract-OCR 引擎封装的库来进行文字辨认。

【杰哥带你玩转Android自动化】学亿点有备无患的

用法确实简略,先装两个东西:

# 装置 pytesseract
pip install pytesseract
# 装置Tesseract (区别体系)
# Windows体系 → 可到:https://github.com/UB-Mannheim/tesseract/wiki 下载exe装置包
# 注:记住选中文包,默认只支撑英文辨认
# 装置完,来到tesseract.exe地点的目录,仿制途径,在PATH环境变量中新增此途径。
# macOS体系可用brew装置 → brew install tesseract

装置装备完,翻开指令行键入 tesseract -v 能够检查对应版别:

【杰哥带你玩转Android自动化】学亿点有备无患的

如果装置时没勾选中文包也没联系,能够到 tessdata 中下载中文数据集 chi_sim.traineddata,然后放到 tessdata 目录下 (其它语言也是如此):

【杰哥带你玩转Android自动化】学亿点有备无患的

随手拿个图片试试水:

【杰哥带你玩转Android自动化】学亿点有备无患的

辨认代码如下:

import pytesseract
from PIL import Image
if __name__ == '__main__':
    image = Image.open("test_ocr.jpg")
    text = pytesseract.image_to_string(image, lang='chi_sim')
    print(text)

输出成果如下:

【杰哥带你玩转Android自动化】学亿点有备无患的

em…辨认率好像不是很高?尝试把文字部分抠出来 (手动截图),再辨认一下:

【杰哥带你玩转Android自动化】学亿点有备无患的

榜首行文本算是完好辨认了,但第二行文本依旧辨认不出来。想进步辨认率,除了 对图片进行处理 (二值化、灰度等),还能够自己 练习数据模型,对练习办法感兴趣可移步至 Training for Tesseract 5 自行查阅尝试。

【杰哥带你玩转Android自动化】学亿点有备无患的

得自己练习模型,这波算是劝退咱们了,究竟咱们的期望是:简略调下API,就能获取较为精确的辨认成果


② chineseocr_lite (引荐)

自己不想练习模型,直接用他人练习好的模型,不就好了?

【杰哥带你玩转Android自动化】学亿点有备无患的

随手搜下”OCR途径”,漫山遍野的供给服务商,以百度OCR为例,官网控制台注册OCR相关服务,接着 pip install baidu-aip 装置模块,然后直接调用:

from aip import AipOcr
# 读取文件内容
def get_file_content(file_path):
    with open(file_path, 'rb') as fp:
        return fp.read()
class BaiDuOCR:
    # 初始化,相关字段到官网控制台自行获取
    def __init__(self):
        self.APP_ID = "xxx"
        self.API_KEY = "xxx"
        self.SECRET_KEY = "xxx"
        self.client = AipOcr(self.APP_ID, self.API_KEY, self.SECRET_KEY)
    # 辨认图片
    def general(self, pic_path):
        orc_result = self.client.basicGeneral(get_file_content(pic_path))
        if orc_result is not None:
            print("辨认成果:" + str(orc_result))
        else:
            print("辨认失利")
            raise Exception("辨认失利反常")
if __name__ == '__main__':
    ocr_client = BaiDuOCR()
    ocr_client.general('test_ocr.jpg')

运转辨认成果如下:

【杰哥带你玩转Android自动化】学亿点有备无患的

啧啧,完美辨认,其它OCR辨认途径的集成办法也是相似,参照对应官方文档即可。

当然,辨认服务是要 收费的 的 (有提供少数的白嫖次数),究竟,人家也是要盈余的,有企业收购需求能够考虑下~

【杰哥带你玩转Android自动化】学亿点有备无患的

除了付费的服务供给商外,还有一些优秀的开源OCR辨认库,比方笔者在 《破大防!这个开源库,竟能让APP日常使命主动化变得如此简略》 中用到的 DayBreak-u/chineseocr_lite,辨认速度和精确率都十分高。

运用办法很简略,先把项目clone到本地:

git clone https://github.com/DayBreak-u/chineseocr_lite.git

接着cd到目录下,键入发动指令:

python backend/main.py

一般是运转不起来的,相关依赖都没有装,报缺啥,你就pip装啥,比方笔者依次就装了这些:

# Python 高性能Web框架
pip install tornado
# opencv → cv2
pip install opencv-python
# ONNX格式的机器学习模型的高性能推理引擎
pip install onnxruntime
# 小型动态图形计算库,将输入的图形途径进行处理
pip install pyclipper
# 空间几何目标库,支撑点线面等集合目标及相关空间操作
pip install shapely

该装的都装完了,执行发动指令,终端最后会输出一个内网的ip地址:

【杰哥带你玩转Android自动化】学亿点有备无患的

仿制到浏览器翻开,把要辨认的图片传入,接着点击辨认,静待辨认完结:

【杰哥带你玩转Android自动化】学亿点有备无患的

辨认成果精确无误不说,文字区域相对图片的位置也符号出来了:

【杰哥带你玩转Android自动化】学亿点有备无患的

接着要做的工作便是:抓包编写代码 (模仿上传图片 + 解析辨认成果),而这部分折腾进程在 《模仿上传 & 成果解析》 中已经写得巨详细了,不再复述了,直接给出东西代码:

import socket
import requests as r
from collections import OrderedDict
local_ocr_base_url = "http://{}:8089".format(socket.gethostbyname(socket.gethostname()))
local_ocr_tr_run_url = local_ocr_base_url + "/api/tr-run/"
def picture_local_ocr(pic_path):
    upload_files = {'file': open(pic_path, 'rb'), 'compress': 960}
    # 发送请求会主动加上Content-Type,不要手多加上,加了会报错
    resp = r.post(local_ocr_tr_run_url, files=upload_files)
    return extract_text(resp.json())
def extract_text(origin_data_dict):
    text_dict = OrderedDict()
    raw_out = origin_data_dict['data']['raw_out']
    if raw_out is not None:
        for raw in raw_out:
            text_dict[raw[1]] = (raw[0][0][0], raw[0][0][1], raw[0][27][0], raw[0][28][1])
        return text_dict
    else:
        print("Json数据解析反常")
if __name__ == '__main__':
    print(picture_local_ocr('test_ocr.jpg'))

运转打印输出成果如下:

【杰哥带你玩转Android自动化】学亿点有备无患的

行吧,想用OCR文字辨认的时候,先把服务跑起来,然后调下 picture_local_ocr() 就好了,十分简略~

【杰哥带你玩转Android自动化】学亿点有备无患的


0x3、音讯推送

在主动化脚本运转进程中,有时需求将一些音讯及时奉告咱们,以便进行一些决策,比方:主动打卡成功或失利。

而笔者知道的关于音讯推送的计划有这些:

  • 发送邮件 (免费,python中主要用到两个内置库smtplib-登录邮箱,email-构建邮件内容)
  • 发送短信 (付费,调短信途径提供的API,各种传自己的信息,而且有限制);
  • 集成第三方音讯推送SDK (花钱不说,或许还得自己写个接纳端的APP);
  • 各种群机器人 (企业微信、钉钉、飞书、tg等);
  • 微信音讯 (Server酱、微信测验号等)

上述计划都能够,按照自己实际情况来就好,这儿只提一嘴笔者一直在用的 Server酱。用法比较简略,翻开官网扫码登录 (要接纳音讯的微信):

【杰哥带你玩转Android自动化】学亿点有备无患的

然后点击通道装备,选择方糖服务号:

【杰哥带你玩转Android自动化】学亿点有备无患的

接着点SendKey跳转,能够在此测验发送音讯:

【杰哥带你玩转Android自动化】学亿点有备无患的

手机微信立马到推送:

【杰哥带你玩转Android自动化】学亿点有备无患的

接着便是写代码调调API咯,直接给出东西代码:

import requests as r
send_key = "xxx"  # SendKey,官网自行获取
send_url = "https://sctapi.ftqq.com/%s.send" % send_key
def send_wx_message(title, desp, short, channel=9):
    """
    发送微信音讯
    :param title: 标题,必填,最大长度32
    :param desp: 音讯内容,选填,最大长度为 32KB
    :param short: 音讯卡片内容,选填,最大长度64,不指定会主动截图desp的前30个显现
    :param channel: 途径,支撑最多两个通道,用竖线离隔,9为方糖服务号
    :return: None
    """
    resp = r.post(send_url, data={'title': title, 'desp': desp, 'short': short, 'channel': channel})
    if resp:
        if resp.status_code == 200:
            print("音讯发送成功")
        else:
            print("音讯发送失利")
        print(resp.text)
if __name__ == '__main__':
    send_wx_message("测验标题", "测验音讯内容\n\n" * 16, "测验卡片")

运转输出成果如下:

【杰哥带你玩转Android自动化】学亿点有备无患的

微信相同收到音讯推送,细心的你或许发现了 [1/5],这是每天只能发5条?

【杰哥带你玩转Android自动化】学亿点有备无患的

切当点来说是 新版免费额度每天只要5条,关于咱们的主动打卡场景来说是 够用 的了。

如果你还有其他主动化的需求,需求 频繁用到音讯推送的,也能够按需订阅会员,丰俭由人~

当然,你还能够运用 微信测验号企业微信 动手搭建一个相似的推送途径,具体实践能够参阅:《运用python推送音讯至手机微信最全版》


0x4、图片处理

便是一些主动化里常常用到的图片处理操作,比方:区域裁剪分辨率调整转灰度二值化 等,也没啥好讲,直接上东西代码,读者按需Copy即可:

import os
import time
from PIL import Image
def get_picture_size(pic_path):
    """
    获得图片尺寸
    :param pic_path: 图片途径
    :return: 图片宽度、图片高度,图片格式,返回样例:(1080, 1080, 'JPEG')
    """
    img = Image.open(pic_path)
    return img.width, img.height, img.format
def crop_area(pic_path, start_x, start_y, end_x, end_y):
    """
    裁剪图片
    :param pic_path: 图片途径
    :param start_x: x轴开始坐标
    :param start_y: y轴开始坐标
    :param end_x: x轴终点坐标
    :param end_y: y轴终点坐标
    :return: 生成的截图途径
    """
    img = Image.open(pic_path)
    region = img.crop((start_x, start_y, end_x, end_y))
    save_path = os.path.join(os.getcwd(), "crop_" + str(round(time.time() * 1000)) + ".jpg")
    region.save(save_path)
    return save_path
def resize_picture(pic_path, width, height):
    """
    调整图片分辨率
    :param pic_path: 图片途径
    :param width: 调整后的图片宽
    :param height: 调整后的图片高
    :return: 调整后的图片途径
    """
    img = Image.open(pic_path)
    resized_img = img.resize((width, height), Image.ANTIALIAS)
    save_path = os.path.join(os.getcwd(), "resized_" + str(round(time.time() * 1000)) + ".jpg")
    resized_img.save(save_path)
    return save_path
def resize_picture_percent(pic_path, percent):
    """
    按份额调整图片分辨率
    :param pic_path: 图片途径
    :param percent: 缩放份额
    :return: 调整后的图片途径
    """
    img = Image.open(pic_path)
    resized_img = img.resize((int(img.width * percent), int(img.height * percent)), Image.ANTIALIAS)
    save_path = os.path.join(os.getcwd(), "resized_" + str(round(time.time() * 1000)) + ".jpg")
    resized_img.save(save_path)
    return save_path
def picture_to_gray(pic_path):
    """
    转灰度图
    :param pic_path: 图片途径
    :return: 转化后的图片途径
    """
    img = Image.open(pic_path)
    gray_img = img.convert('L')
    save_path = os.path.join(os.getcwd(), "gray_" + str(round(time.time() * 1000)) + ".jpg")
    gray_img.save(save_path)
    return save_path
def picture_to_black_white(pic_path):
    """
    图片二值化(是非)
    :param pic_path: 图片途径
    :return: 转化后的图片途径
    """
    img = Image.open(pic_path)
    gray_img = img.convert('1')
    save_path = os.path.join(os.getcwd(), "bw_" + str(round(time.time() * 1000)) + ".jpg")
    gray_img.save(save_path)
    return save_path
if __name__ == '__main__':
    print(get_picture_size('test_ocr.jpg'))
    print(crop_area('test_ocr.jpg', 0, 0, 100, 200))
    print(resize_picture('test_ocr.jpg', 300, 600))
    print(resize_picture_percent('test_ocr.jpg', 0.5))
    print(picture_to_gray('test_ocr.jpg'))
    print(picture_to_black_white('test_ocr.jpg'))

运转输出成果如下:

【杰哥带你玩转Android自动化】学亿点有备无患的


0x5、获取当时页面一切控件信息

有时,咱们需求 定位 到页面中的某个控件的 地点区域,然后触发一些交互,比方登录时:

【杰哥带你玩转Android自动化】学亿点有备无患的

  • 先定位到点击账号/暗码输入文本框,点击获得焦点,然后进行输入;
  • 先定位到同意协议单选框,点击勾选;

有文字的控件 能够通过上面的OCR文字辨认大约定位到 位置区域,而 关于没有文字 的控件这种办法就不太行得通了。

当然你不嫌麻烦,直接截图,然后用 PS抠像素点坐标 也能够。

【杰哥带你玩转Android自动化】学亿点有备无患的

当然,有更高效便捷的办法,如果是 原生控件堆砌的页面,能够运用Android体系中的 system/bin/uiautomator.jar包,把当时屏幕上一切控件信息直接 dumpxml 中。调用指令如下:

# dump出一切控件信息到xml中
adb shell /system/bin/uiautomator dump --compressed /手机存储途径/ui.xml
# 将文件从手机导出到PC
adb pull /手机存储途径/ui.xml 本地途径

直接在Python中调用这两行指令:

def current_ui_xml(save_dir=None):
    """
    获取当时页面的布局xml
    :param save_dir: 文件保存根目录
    :return: 布局xml文件的本地途径
    """
    ui_xml_name = "ui_%d.xml" % (int(round(t * 1000)))
    start_cmd('adb shell /system/bin/uiautomator dump --compressed  /sdcard/%s' % ui_xml_name)
    ui_xml_path = os.path.join(os.getcwd() if save_dir is None else save_dir, ui_xml_name)
    start_cmd('adb pull /sdcard/%s %s' % (ui_xml_name, ui_xml_path))
    return ui_xml_path

运转输出成果如下:

【杰哥带你玩转Android自动化】学亿点有备无患的

翻开这个xml文件康康:(此处以APP为例~)

【杰哥带你玩转Android自动化】学亿点有备无患的

结构很简略,最外层是 hierarchy标签,然后是 node标签(代表一个控件) 的嵌套,接着要做的工作便是解析这个xml,提取所需的数据了。先挑选或许要用的属性,直接界说一个Node类:

class Node:
    """
    XML节点类
    """
    def __init__(self, index=None, text=None, resource_id=None, class_name=None, package=None, content_desc=None,
                 bounds=None):
        self.index = index
        self.text = text
        self.resource_id = resource_id
        self.class_name = class_name
        self.package = package
        self.content_desc = content_desc
        self.bounds = bounds
        self.nodes = [] # 存储子节点
    def add_node(self, node):
        self.nodes.append(node)

运用 lxml库 解析xml,难点应该便是:遍历子node节点,这儿直接 递归 进行遍历,看不懂的多看几遍就好,还算简略。随手写个递归打印出一切节点,以便成果更直观,直接给出完好代码:

from lxml import etree
import re
bounds_pattern = re.compile(r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]")  # 将坐标区域格式化为元组的正则
def analysis_ui_xml(xml_path):
    """
    解析ui.xml文件
    :param xml_path: xml文件途径
    :return: 节点实例
    """
    root = etree.parse(xml_path, parser=etree.XMLParser(encoding="utf-8"))
    root_node_element = root.xpath('/hierarchy/node')[0] # 定位到根node节点
    node = analysis_element(root_node_element)
    print_node(node)    # 打印看看作用
    return node
def analysis_element(element):
    """
    递归剖析结点(转化为node目标)
    :param element:
    :return:
    """
    if element is not None and element.tag == "node":
        # 解析当时节点
        bounds_result = re.search(bounds_pattern, element.attrib['bounds'])
        node = Node(
            int(element.attrib['index']),
            element.attrib['text'],
            element.attrib['resource-id'],
            element.attrib['class'],
            element.attrib['package'],
            element.attrib['content-desc'],
            (int(bounds_result[1]), int(bounds_result[2]), int(bounds_result[3]), int(bounds_result[4]))
        )
        # 解析子节点,递归调用
        child_node_elements = element.xpath('node')
        if len(child_node_elements) > 0:
            for child_node_element in child_node_elements:
                node_result = analysis_element(child_node_element)
                if node_result:
                    node.nodes.append(node_result)
        return node
def print_node(node, space_count=0):
    """
    递归打印结点信息
    :param node: 当时节点
    :param space_count: 前面的空格数,区别不同层级用
    :return:
    """
    widget_info = "%d - %s - %s - %s - %s - %s - %s" % (
        node.index, node.text, node.resource_id, node.class_name, node.package, node.content_desc, node.bounds)
    print(" " * (2 * space_count), widget_info)
    for child_node in node.nodes:
        print_node(child_node, space_count + 1)
if __name__ == '__main__':
    # 测验解析作用
    analysis_ui_xml('ui_1665224943842.xml')

运转输出成果如下:

【杰哥带你玩转Android自动化】学亿点有备无患的

作用杠杠滴,读者Copy下,依据自己的具体业务按需修改即可~

【杰哥带你玩转Android自动化】学亿点有备无患的


0x6、小结

不知不觉又到文尾,本节扼要学习了亿点和主动化有关的 “姿态(常识)“,包含:OCR文字辨认、音讯推送、图片处理、获取当时页面的一切控件信息。

看似 轻松愉快的一节,但 实操性极强,杰哥主张:即使不自己跟着写一遍,也要Copy下代码,运转一下!

【杰哥带你玩转Android自动化】学亿点有备无患的

啧啧,杰哥将在下一节中用上这些姿态,把咱们的打卡jio本 打磨blingbling 的,敬请期待~


参阅文献

  • 【独家】一文读懂文字辨认(OCR)