在移动端自动化测试领域,基于文字内容的操作一直是一个技术难点。HttpRunner通过集成OCR(光学字符识别)技术,实现了高精度的文字定位与点击功能,为开发者提供了更加直观和可靠的自动化测试方案。

核心架构设计

用户指定文字 → 截图 → OCR识别 → 文字定位 → 坐标计算 → 执行点击

HttpRunner的OCR点击机制采用了分层架构设计,将复杂的图像识别流程抽象为清晰的模块边界。整个系统由用户接口层、文字查找层、OCR服务层和坐标计算层组成,各层职责明确,耦合度低。

用户接口层提供了简洁的API,开发者只需调用TapByOCR()方法并传入目标文字即可完成操作。该方法内部处理了截图文件命名、错误处理策略以及点击位置计算等复杂逻辑。

func (dExt *XTDriver) TapByOCR(text string, opts ...option.ActionOption) error {
actionOptions := option.NewActionOptions(opts...)
log.Info().Str("text", text).Interface("options", actionOptions).Msg("TapByOCR") // 自动生成截图文件名,便于调试追踪
if actionOptions.ScreenShotFileName == "" {
opts = append(opts, option.WithScreenShotFileName(fmt.Sprintf("tap_by_ocr_%s", text)))
} // 执行文字定位操作
textRect, err := dExt.FindScreenText(text, opts...)
if err != nil {
if actionOptions.IgnoreNotFoundError {
return nil // 容错处理,适用于可选操作场景
}
return err
} // 智能点击位置计算
var point ai.PointF
if actionOptions.TapRandomRect {
point = textRect.RandomPoint() // 防检测随机点击
} else {
point = textRect.Center() // 精确中心点击
} return dExt.TapAbsXY(point.X, point.Y, opts...)
}

文字识别与定位算法

文字查找模块是整个系统的核心,负责在屏幕截图中准确定位目标文字。该模块支持多种匹配模式,包括精确匹配、包含匹配和正则表达式匹配,能够适应不同的业务场景需求。

func (dExt *XTDriver) FindScreenText(text string, opts ...option.ActionOption) (textRect ai.OCRText, err error) {
options := option.NewActionOptions(opts...) // 处理相对坐标转换,支持屏幕区域限制
if len(options.Scope) == 4 {
windowSize, _ := dExt.WindowSize()
absScope := options.Scope.ToAbs(windowSize)
opts = append(opts, absScope.Option())
} // 获取完整的OCR识别结果
ocrTexts, err := dExt.GetScreenTexts(opts...)
if err != nil {
return
} // 在识别结果中查找目标文字
textRect, err = ocrTexts.FindText(text, opts...)
if err != nil {
log.Warn().Msgf("FindText failed: %s", err.Error())
return
} log.Info().Str("text", text).
Interface("textRect", textRect).Msgf("FindScreenText success")
return textRect, nil
}

OCR数据结构与坐标转换

OCR服务返回的原始数据需要经过标准化处理才能被后续模块使用。HttpRunner定义了完整的数据结构来描述文字识别结果,包括文字内容、边界框坐标等关键信息。

OCR服务返回的坐标点数组遵循特定的顺序规则:左上、右上、右下、左下。这种标准化的坐标表示方式确保了不同OCR服务提供商之间的兼容性。

type OCRResult struct {
Text string `json:"text"` // 识别的文字内容
Points []PointF `json:"points"` // 四个顶点坐标,顺序固定
} func (o OCRResults) ToOCRTexts() (ocrTexts OCRTexts) {
for _, ocrResult := range o {
rect := image.Rectangle{
// 利用左上和右下两个关键点构建矩形
Min: image.Point{
X: int(ocrResult.Points[0].X), // 左上角X
Y: int(ocrResult.Points[0].Y), // 左上角Y
},
Max: image.Point{
X: int(ocrResult.Points[2].X), // 右下角X
Y: int(ocrResult.Points[2].Y), // 右下角Y
},
} rectStr := fmt.Sprintf("%d,%d,%d,%d",
rect.Min.X, rect.Min.Y, rect.Max.X, rect.Max.Y) ocrText := OCRText{
Text: ocrResult.Text,
Rect: rect,
RectStr: rectStr,
}
ocrTexts = append(ocrTexts, ocrText)
}
return
}

高精度文字匹配机制

文字匹配算法支持多种策略,包括简单的字符串包含匹配和复杂的正则表达式匹配。为了提高匹配的准确性,系统还提供了区域过滤功能,允许开发者将搜索范围限制在屏幕的特定区域内。

func (t OCRTexts) FindText(text string, opts ...option.ActionOption) (result OCRText, err error) {
options := option.NewActionOptions(opts...) var results []OCRText
for _, ocrText := range t.FilterScope(options.AbsScope) {
if options.Regex {
// 正则表达式匹配,支持复杂模式
matched, _ := regexp.MatchString(text, ocrText.Text)
if matched {
results = append(results, ocrText)
}
} else {
// 包含匹配,处理大部分常见场景
if strings.Contains(ocrText.Text, text) {
results = append(results, ocrText)
}
}
} if len(results) == 0 {
return result, errors.Wrap(code.CVResultNotFoundError,
fmt.Sprintf("text %s not found in %v", text, t.texts()))
} // 支持多匹配结果的索引选择
idx := options.Index
if idx >= len(results) {
idx = len(results) - 1
}
return results[idx], nil
}

智能坐标计算算法

在确定目标文字的边界框后,系统需要计算精确的点击坐标。HttpRunner提供了两种点击策略:中心点击和随机点击。中心点击适用于对精度要求较高的场景,而随机点击则可以有效规避反作弊系统的检测。

func (t OCRText) Center() PointF {
rect := t.Rect
x, y := float64(rect.Min.X), float64(rect.Min.Y)
width, height := float64(rect.Dx()), float64(rect.Dy())
point := PointF{
X: x + width*0.5, // 几何中心X坐标
Y: y + height*0.5, // 几何中心Y坐标
}
return point
} func (t OCRText) RandomPoint() PointF {
rect := t.Rect
x, y := float64(rect.Min.X), float64(rect.Min.Y)
width, height := float64(rect.Dx()), float64(rect.Dy())
point := PointF{
X: x + width*rand.Float64(), // 随机X坐标
Y: y + height*rand.Float64(), // 随机Y坐标
}
return point
}

坐标系统与范围限制

为了支持不同分辨率的设备,HttpRunner采用了相对坐标和绝对坐标的双重体系。相对坐标使用0到1的浮点数表示屏幕位置的百分比,而绝对坐标则使用实际的像素值。这种设计使得测试脚本能够在不同设备间无缝迁移。

func (s Scope) ToAbs(windowSize types.Size) AbsScope {
x1, y1, x2, y2 := s[0], s[1], s[2], s[3]
// 相对坐标到绝对坐标的线性映射
absX1 := int(x1 * float64(windowSize.Width))
absY1 := int(y1 * float64(windowSize.Height))
absX2 := int(x2 * float64(windowSize.Width))
absY2 := int(y2 * float64(windowSize.Height))
return AbsScope{absX1, absY1, absX2, absY2}
}

VEDEM OCR服务集成

HttpRunner集成了VEDEM等专业的OCR服务提供商,通过HTTP API的方式实现图像识别功能。系统采用multipart/form-data格式上传截图,并通过action参数指定所需的服务类型。

func (s *vedemCVService) ReadFromBuffer(imageBuf *bytes.Buffer, opts ...option.ActionOption) (
imageResult *CVResult, err error) { bodyBuf := &bytes.Buffer{}
bodyWriter := multipart.NewWriter(bodyBuf) // 指定OCR服务类型
for _, action := range screenshotActions {
bodyWriter.WriteField("actions", action)
} // 使用高精度OCR集群
bodyWriter.WriteField("ocrCluster", "highPrecision") // 上传图像数据
formWriter, err := bodyWriter.CreateFormFile("image", "screenshot.png")
size, err := formWriter.Write(imageBuf.Bytes()) // 发送HTTP请求
req, err = http.NewRequest("POST", os.Getenv("VEDEM_IMAGE_URL"), copiedBodyBuf)
resp, err = client.Do(req)
}

配置选项与错误处理

系统提供了丰富的配置选项,允许开发者根据具体需求调整OCR点击行为。这些选项包括重试机制、错误处理策略、匹配模式等,能够有效提高自动化测试的稳定性和可靠性。

type ActionOptions struct {
MaxRetryTimes int `json:"max_retry_times,omitempty"` // 重试次数控制
Interval float64 `json:"interval,omitempty"` // 重试间隔设置
IgnoreNotFoundError bool `json:"ignore_NotFoundError,omitempty"` // 容错策略
Index int `json:"index,omitempty"` // 多结果索引
TapRandomRect bool `json:"tap_random_rect,omitempty"` // 随机点击开关
Regex bool `json:"regex,omitempty"` // 正则匹配开关
}

跨平台点击框架的思考扩展

基于HttpRunner现有的OCR实现,我们可以构建一个更加完善的跨平台点击操作体系。理想的设计应该遵循效率优先的策略,即优先使用最高效的控件定位方案,当控件定位难以实现时选择图像识别,最后再选择OCR文字识别作为兜底方案。

这种分层策略能够在保证操作成功率的同时,最大化执行效率。控件定位直接与系统API交互,速度最快但受限于应用的可访问性支持;图像识别无需网络调用,适合离线场景但受设备分辨率影响;OCR识别通用性最强,可以处理任何可见文字,但需要网络服务支持且耗时较长。

统一点击接口设计

type TapStrategy int

const (
TapByControl TapStrategy = iota // 控件定位优先
TapByImage // 图像识别优先
TapByOCR // OCR识别优先
TapByAuto // 自动降级策略
) type UnifiedTapOptions struct {
Strategy TapStrategy `json:"strategy"`
ControlOptions *ControlTapOptions `json:"control_options,omitempty"`
ImageOptions *ImageTapOptions `json:"image_options,omitempty"`
OCROptions *OCRTapOptions `json:"ocr_options,omitempty"`
FallbackEnabled bool `json:"fallback_enabled"`
MaxRetryTimes int `json:"max_retry_times"`
} func (dExt *XTDriver) TapUnified(opts UnifiedTapOptions) error {
if opts.Strategy == TapByAuto {
return dExt.tapWithFallback(opts)
}
return dExt.tapBySingleStrategy(opts)
}

控件定位点击实现

控件定位是最高效的点击方式,直接利用操作系统提供的可访问性API来定位UI元素。这种方式不需要图像处理,响应速度快,且不受屏幕分辨率和主题变化影响。

type ControlTapOptions struct {
Locator string `json:"locator"` // 定位器类型
Value string `json:"value"` // 定位值
Timeout float64 `json:"timeout"` // 等待超时
WaitVisible bool `json:"wait_visible"` // 等待可见
WaitEnabled bool `json:"wait_enabled"` // 等待可点击
} func (dExt *XTDriver) TapByControl(locator, value string, opts ...option.ActionOption) error {
log.Info().Str("locator", locator).Str("value", value).Msg("TapByControl") // 根据定位器类型选择定位策略
var element WebElement
var err error switch locator {
case "id":
element, err = dExt.FindElementByID(value)
case "xpath":
element, err = dExt.FindElementByXPath(value)
case "name":
element, err = dExt.FindElementByName(value)
case "accessibility_id":
element, err = dExt.FindElementByAccessibilityID(value)
case "class_name":
element, err = dExt.FindElementByClassName(value)
default:
return fmt.Errorf("unsupported locator type: %s", locator)
} if err != nil {
return errors.Wrap(err, "control element not found")
} // 等待元素状态就绪
if err := dExt.waitElementReady(element, opts...); err != nil {
return err
} // 执行点击操作
return element.Click()
}

图像识别点击实现

图像识别方案使用预先截取的按钮图片作为模板,通过图像匹配算法在屏幕上定位目标按钮。这种方法适合处理那些无法通过控件定位器访问的自定义UI组件。

type ImageTapOptions struct {
Path string `json:"path"` // 模板图片路径
Confidence float64 `json:"confidence"` // 匹配置信度阈值
Grayscale bool `json:"grayscale"` // 灰度匹配模式
Timeout float64 `json:"timeout"` // 匹配超时时间
} func (dExt *XTDriver) TapByImage(imagePath string, opts ...option.ActionOption) error {
actionOptions := option.NewActionOptions(opts...)
log.Info().Str("imagePath", imagePath).Interface("options", actionOptions).Msg("TapByImage") // 加载模板图片
templateImg, err := dExt.loadTemplateImage(imagePath)
if err != nil {
return errors.Wrap(err, "failed to load template image")
} // 获取当前屏幕截图
screenshot, err := dExt.TakeScreenshot()
if err != nil {
return errors.Wrap(err, "failed to take screenshot")
} // 执行模板匹配
matchResult, err := dExt.templateMatch(screenshot, templateImg, actionOptions.Confidence)
if err != nil {
return errors.Wrap(err, "template matching failed")
} // 计算点击坐标
clickPoint := matchResult.Center()
if actionOptions.TapRandomRect {
clickPoint = matchResult.RandomPoint()
} return dExt.TapAbsXY(clickPoint.X, clickPoint.Y)
} func (dExt *XTDriver) templateMatch(screenshot, template image.Image, threshold float64) (*ImageMatchResult, error) {
// 使用OpenCV或其他图像处理库进行模板匹配
// 返回匹配位置和置信度
result := &ImageMatchResult{} // 模板匹配算法实现
confidence, location := cv.MatchTemplate(screenshot, template) if confidence < threshold {
return nil, fmt.Errorf("match confidence %.2f below threshold %.2f", confidence, threshold)
} result.Confidence = confidence
result.Rectangle = location
return result, nil
}

DSL语法设计与参数规范

为了提供开发者友好的接口,我们需要设计清晰的DSL语法来描述不同的点击操作。每种点击方式都有其特定的参数需求,DSL应该能够灵活地表达这些差异。

# 控件定位点击
- tap_by_control:
locator: "id" # 定位器类型: id, xpath, name, accessibility_id, class_name
value: "com.app:id/login_btn" # 定位值
options:
timeout: 10.0 # 等待超时时间
wait_visible: true # 等待元素可见
wait_enabled: true # 等待元素可点击
max_retry_times: 3 # 最大重试次数 # 图像识别点击
- tap_by_image:
path: "./images/login_button.png" # 模板图片路径
options:
confidence: 0.8 # 匹配置信度阈值 (0-1)
grayscale: false # 是否使用灰度匹配
timeout: 15.0 # 匹配超时时间
tap_random_rect: false # 是否随机点击位置 # OCR文字点击
- tap_by_ocr:
text: "登录" # 目标文字
options:
index: 0 # 多匹配结果时的索引
scope: [0.0, 0.5, 1.0, 1.0] # 搜索区域限制
regex: false # 是否使用正则表达式
tap_random_rect: true # 随机点击防检测 # 自动降级策略
- tap_unified:
strategy: "auto" # auto, control, image, ocr
fallback_enabled: true # 启用降级机制
control_options:
locator: "id"
value: "login_btn"
image_options:
path: "./images/login.png"
confidence: 0.75
ocr_options:
text: "登录"
regex: false

写在最后

这种多元化的点击策略设计特别适合复杂的企业级应用测试场景。在实际使用中,开发者可以根据应用特点选择合适的策略组合:

对于原生应用,优先使用控件定位,因为它直接利用系统API,速度快且准确。对于混合应用或游戏,图像识别往往更加可靠。对于国际化应用或动态内容,OCR识别提供了最大的灵活性。

通过这种分层设计,HttpRunner不仅保持了原有OCR功能的强大能力,还扩展了更多高效的定位方案,为开发者提供了一个真正跨平台、高可靠性的自动化测试解决方案。


关注 【松哥AI自动化】 公众号,每周获取深度技术解析,从源码角度彻底理解各种工具的实现原理。更重要的是,遇到技术难题时,直接联系我!我会根据你的具体情况,提供最适合的解决方案和技术指导。

上期回顾:(从抓包GitHub Copilot认证请求,认识OAuth 2.0技术

跨平台自动化框架的OCR点击操作实现详解与思考的更多相关文章

  1. 安装MACOS操作步骤详解

    安装MACOS操作步骤详解 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 关于安装MAC的操作系统其实大家都知道可以让客服帮忙提供软件上的支持,而且苹果客服都很有礼貌呢,而且非常的 ...

  2. legend3---Windows 7/8/10 系统下Laravel框架的开发环境安装及部署详解(Vagrant + Homestead)

    legend3---Windows 7/8/10 系统下Laravel框架的开发环境安装及部署详解(Vagrant + Homestead) 一.总结 一句话总结: 1.安装的话就是下载好git,va ...

  3. 《手把手教你》系列基础篇(七十六)-java+ selenium自动化测试-框架设计基础-TestNG实现DDT - 下篇(详解教程)

    1.简介 今天这一篇宏哥主要是结合实际工作中将遇到的测试场景和前边两篇学习的知识结合起来给大家讲解和分享一下,希望以后大家在以后遇到其他的测试场景也可以将自己的所学的知识应用到测试场景中. 2.测试场 ...

  4. 《手把手教你》系列基础篇(九十四)-java+ selenium自动化测试-框架设计基础-POM设计模式实现-下篇(详解教程)

    1.简介 上一篇宏哥用PageFactory实现了POM,宏哥再介绍一下如果不用PageFactory如何实现POM. 2.项目实战 在这里宏哥以百度首页登录的例子,如果用POM实现,在测试脚本中实际 ...

  5. 《手把手教你》系列基础篇(九十六)-java+ selenium自动化测试-框架之设计篇-跨浏览器(详解教程)

    1.简介 从这一篇开始介绍和分享Java+Selenium+POM的简单自动化测试框架设计.第一个设计点,就是支持跨浏览器测试. 宏哥自己认为的支持跨浏览器测试就是:同一个测试用例,支持用不同浏览器去 ...

  6. Spring框架系列(7) - Spring IOC实现原理详解之IOC初始化流程

    上文,我们看了IOC设计要点和设计结构:紧接着这篇,我们可以看下源码的实现了:Spring如何实现将资源配置(以xml配置为例)通过加载,解析,生成BeanDefination并注册到IoC容器中的. ...

  7. Spring框架系列(8) - Spring IOC实现原理详解之Bean实例化(生命周期,循环依赖等)

    上文,我们看了IOC设计要点和设计结构:以及Spring如何实现将资源配置(以xml配置为例)通过加载,解析,生成BeanDefination并注册到IoC容器中的:容器中存放的是Bean的定义即Be ...

  8. Spring框架系列(11) - Spring AOP实现原理详解之Cglib代理实现

    我们在前文中已经介绍了SpringAOP的切面实现和创建动态代理的过程,那么动态代理是如何工作的呢?本文主要介绍Cglib动态代理的案例和SpringAOP实现的原理.@pdai Spring框架系列 ...

  9. 005-Scala数组操作实战详解

    005-Scala数组操作实战详解 Worksheet的使用 交互式命令执行平台 记得每次要保存才会出相应的结果 数组的基本操作 数组的下标是从0开始和Tuple不同 缓冲数组ArrayBuffer( ...

  10. ASP.NET 操作Cookie详解 增加,修改,删除

    ASP.NET 操作Cookie详解 增加,修改,删除 Cookie,有时也用其复数形式Cookies,指某些网站为了辨别用户身份而储存在用户本地终端上的数据(通常经过加密).定义于RFC2109.它 ...

随机推荐

  1. ansible实战-2023

    环境信息:cat /etc/ansible/hosts[webserver]192.168.31.18 ansible_ssh_user=root ansible_ssh_pass=123456 ht ...

  2. Python实验4 列表与字典应用

    目的 :熟练操作组合数据类型. 试验任务: 基础:生日悖论分析.如果一个房间有23 人或以上,那么至少有两 个人的生日相同的概率大于50%.编写程序,输出在不同随机样本数 量下,23 个人中至少两个人 ...

  3. SpringSecurity配置 1

    spring security整合步骤 过滤器链 SpringSecurity的本质就是一个过滤器链,内部包含了提供各种功能的过滤器,基本案例中的过滤器链如下图所示: UsernamePassword ...

  4. Spring注解之@Autowired自动装配bean 综述

    @Autowired的工作原理是什么?在启动spring IoC时,容器自动装载了一个AutowiredAnnotationBeanPostProcessor后置处理器,当容器扫描到@Autowire ...

  5. docker部署mysql5.7后登录时出现Access denied for user 'root'@'localhost' (using password: YES)的解决方法

    1.先进去容器中安装vim工具 docker exec -it mysql bash apt-get update apt-get -y install vim 2.修改/etc/mysql/conf ...

  6. FastAPI安全机制:从OAuth2到JWT的魔法通关秘籍

    title: FastAPI安全机制:从OAuth2到JWT的魔法通关秘籍 date: 2025/06/07 08:40:35 updated: 2025/06/07 08:40:35 author: ...

  7. Axios 禁用缓存

    Disable cache for Axios1. set headers.Cache-Control = 'no cache' // not work for 0.19.x or later?2. ...

  8. SAP-EWM/ECC 系统后台配置(引用,备份)

    原文:https://blog.csdn.net/Daniel_kong198577/article/details/19615153 Unit1 – Basic Settings (default) ...

  9. APIO2025 一堆题解

    APIO2025 一堆题解 图论 AtCoder Xmas Contest 2024 A 答案永远是\(1\),6 NWRRC 2015 Graph 考虑不加边,怎么找字典序最小的拓扑序,就维护一个小 ...

  10. 探索 Vue.js 组件的最新特性

    引言: Vue.js 作为一款流行的前端框架,始终在不断发展和演进,为开发者带来新的特性和功能,以提升开发效率和用户体验.Vue.js 组件是构建 Vue 应用的基础,其最新特性为开发者提供了更强大的 ...