本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
0x1、引言
Hi,我是杰哥,上节《学穿ADB》 带着大家:
对ADB相关的姿势进行了深入学习,并写了一个 "某办公软件打卡自动化" 的简单练手案例。
不知道读者朋友们,有没有跟着动手试试看,纸上得来终觉浅,绝知此事要躬行,收藏夹吃灰是大忌,没试过的赶紧玩起来!
文尾处说道:这个自动打卡脚本的实现过于简单粗暴 (定时 + adb包名启动应用),存在一系列问题。
而单靠ADB并不能解决,所以本节我们再学点有备无患的 "自动化姿势",拓宽认知,为下节自动打卡脚本的「赋能」做准备。
本节学习路线安排如下:
- 了解下OCR文字识别的相关概念,使用 pytesseract库 和 chineseocr_lite 库识别图片文字;
- 了解消息推送相关,使用 Server酱 推送消息到微信;
- 学习自动化中Python常用的 图片处理操作;
- 利用Android系统中的 system/bin/uiautomator.jar包,导出当前页面的所有控件信息,并对xml内容进行解析;
话不多说,直接开冲~
0x2、OCR文字识别
OCR,Optical Character Recognition,译作 光学字符识别,亦称 计算机文字识别,指的是利用光学技术和计算机技术,对文本资料的图像文件进行分析识别处理,获取文字及版面信息的过程。
说人话就是:利用这项技术,从图片中直接提取文字。
OCR技术是实现文字快速录入的一项关键技术,应用场景可就多了:
- 车牌路牌识别;
- 证件卡片识别;
- 验证码识别;
- 古籍识别;
- PDF转Word等...
如今大部分手机和APP都自带OCR,读者们应该都玩过吧?没玩过的可以打开微信体验下:点开图片 → 长按 → 提取文字:
简单介绍下 印刷体文字 的主要识别流程 (详细讲解可见【独家】一文读懂文字识别(OCR))
前期处理
- 灰度化 → 彩色图像中每个像素的颜色由 RGB三个分量 决定,取值范围0-255,对于计算机来说,这样一个像素点会有256256256=16777216种颜色的变化范围。而灰度图是一种 RGB分量相同 的特殊色图像,一个像素点的变化范围只有0-255这256种。图像灰度化的目的是为了简化矩阵,提高运算速度。
- 二值化 → 将文字与背景进一步分开,将灰度值图像信号转换为只有黑(1)和白(0)的二值图像信号,可简单理解为 "黑白化",常用方法有:分量法、最大值法、平均值法、加权平均法等;
- 降噪 → 根据噪点的特征进行去噪;
- 倾斜校正 → 倾斜的文档图像对后期的字符分割、识别和图像压缩等工作会产生较大影响,为了保证后续处理的正确性,需要对文本图像进行倾斜检测和校正;
- 大小规范化 → 将输入的任意尺寸的文字都处理成统一尺寸的标准文字,以便与预先存储在字典中的参考模板相匹配;
- 图像平滑 → 去掉笔划上的孤立白点和笔划外部的孤立黑点,以及笔划边缘的凹凸点,使得笔划边缘变得平滑;
中期处理
- 版面分析 → 将文本图像分割为不同部分,并标定各部分属性,如:文本、图像、表格;
- 版面理解 → 获取文章逻辑结构,包括各区域的逻辑属性、文章的层次关系和阅读顺序;
- 图像切分 → 对文本图像进行切分,方便对单个文字进行识别处理,有两个类型:行(列)切分和字切分;
- 特征提取 → 从单个字符图像上提取统计特征或结构特征,匹配特征库中与待识别文字相似度最高的文字;
- 模型训练 → 从大量标记预料中自动学习出图像的特征;
识别后处理
- 版面恢复 → 根据版面分析和OCR的结果,重构出包含文字信息和版面信息的电子文档;
- 识别校正 → 特定的语言上下文的关系,对识别结果进行校正;
上述内容看得一脸懵逼,云里雾里?放心,这些技术我们无需掌握,专业的事情交给专业的人,大概了解下就好。
我们只关注 能提取图片文字的OCR工具 及 这个工具该怎么用?
① pytesseract (不推荐)
网上随手搜下关键字 Python OCR识别文字,一堆烂大街的教程都是教你用 pytesseract 这个基于Google开源的 Tesseract-OCR 引擎封装的库来进行文字识别。
用法确实简单,先装两个东西:
# 安装 pytesseract
pip install pytesseract
# 安装Tesseract (区分系统)
# Windows系统 → 可到:https://github.com/UB-Mannheim/tesseract/wiki 下载exe安装包
# 注:记得选中文包,默认只支持英文识别
# 安装完,来到tesseract.exe所在的目录,复制路径,在PATH环境变量中新增此路径。
# macOS系统可用brew安装 → brew install tesseract
安装配置完,打开命令行键入 tesseract -v
可以查看对应版本:
如果安装时没勾选中文包也没关系,可以到 tessdata 中下载中文数据集 chi_sim.traineddata,然后放到 tessdata 目录下 (其它语言也是如此):
随手拿个图片试试水:
识别代码如下:
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)
输出结果如下:
em...识别率好像不是很高?尝试把文字部分抠出来 (手动截图),再识别一下:
第一行文本算是完整识别了,但第二行文本依旧识别不出来。想提高识别率,除了 对图片进行处理 (二值化、灰度等),还可以自己 训练数据模型,对训练方法感兴趣可移步至 Training for Tesseract 5 自行查阅尝试。
得自己训练模型,这波算是劝退我们了,毕竟我们的期望是:简单调下API,就能获取较为准确的识别结果。
② chineseocr_lite (推荐)
自己不想训练模型,直接用别人训练好的模型,不就好了?
随手搜下"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')
运行识别结果如下:
啧啧,完美识别,其它OCR识别平台的集成方法也是类似,参照对应官方文档即可。
当然,识别服务是要 收费的 的 (有提供少量的白嫖次数),毕竟,人家也是要盈利的,有企业采购需求可以考虑下~
除了付费的服务供应商外,还有一些优秀的开源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地址:
复制到浏览器打开,把要识别的图片传入,接着点击识别,静待识别完成:
识别结果准确无误不说,文字区域相对图片的位置也标记出来了:
接着要做的事情就是:抓包、编写代码 (模拟上传图片 + 解析识别结果),而这部分折腾过程在 《模拟上传 & 结果解析》 中已经写得巨详细了,不再复述了,直接给出工具代码:
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'))
运行打印输出结果如下:
行吧,想用OCR文字识别的时候,先把服务跑起来,然后调下 picture_local_ocr() 就好了,非常简单~
0x3、消息推送
在自动化脚本运行过程中,有时需要将一些消息及时告知我们,以便进行一些决策,比如:自动打卡成功或失败。
而笔者知道的关于消息推送的方案有这些:
- 发送邮件 (免费,python中主要用到两个内置库smtplib-登录邮箱,email-构建邮件内容)
- 发送短信 (付费,调短信平台提供的API,各种传自己的信息,而且有限制);
- 集成第三方消息推送SDK (花钱不说,可能还得自己写个接收端的APP);
- 各种群机器人 (企业微信、钉钉、飞书、tg等);
- 微信消息 (Server酱、微信测试号等)
上述方案都可以,按照自己实际情况来就好,这里只提一嘴笔者一直在用的 Server酱。用法比较简单,打开官网扫码登录 (要接收消息的微信):
然后点击通道配置,选择方糖服务号:
接着点SendKey跳转,可以在此测试发送消息:
手机微信立马到推送:
接着就是写代码调调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, "测试卡片")
运行输出结果如下:
微信同样收到消息推送,细心的你可能发现了 [1/5],这是每天只能发5条?
确切点来说是 新版免费额度每天只有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'))
运行输出结果如下:
0x5、获取当前页面所有控件信息
有时,我们需要 定位 到页面中的某个控件的 所在区域,然后触发一些交互,比如登录时:
- 先定位到点击账号/密码输入文本框,点击获得焦点,然后进行输入;
- 先定位到同意协议单选框,点击勾选;
有文字的控件 可以通过上面的OCR文字识别大概定位到 位置区域,而 对于没有文字 的控件这种方式就不太行得通了。
当然你不嫌麻烦,直接截图,然后用 PS抠像素点坐标 也可以。
当然,有更高效便捷的方法,如果是 原生控件堆砌的页面,可以利用Android系统中的 system/bin/uiautomator.jar包,把当前屏幕上所有控件信息直接 dump 到 xml 中。调用命令如下:
# 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
运行输出结果如下:
打开这个xml文件康康:(此处以掘金APP为例~)
结构很简单,最外层是 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')
运行输出结果如下:
效果杠杠滴,读者Copy下,根据自己的具体业务按需修改即可~
0x6、小结
不知不觉又到文尾,本节简要学习了亿点和自动化有关的 "姿势(知识)",包括:OCR文字识别、消息推送、图片处理、获取当前页面的所有控件信息。
看似 轻松愉快的一节,但 实操性极强,杰哥建议:即使不自己跟着写一遍,也要Copy下代码,运行一下!
啧啧,杰哥将在下一节中用上这些姿势,把我们的打卡jio本 打磨 得 blingbling 的,敬请期待~
参考文献: