原文:towardsdatascience.com/the-invisible-bug-that-broke-my-automation-how-ocr-changed-the-game-c35e1c79a591

那是一个安静的星期五上午 10 点,我正盯着另一个失败的测试报告。这不仅仅是一个测试用例,这是我上周在做一些 UI 更改后审查和调试的。而现在它神秘地失败了。又一次……

错误?从表面上看,没有错误。

应该是这样的:这段文本应该在欢迎屏幕上显示:“Bienvenue à bord de l’application my-app-name!”

我查看了应用程序的屏幕截图,文本显示正确,没有问题:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/d610947c386ad7bd7a1a01f64d062507.png

作者手机屏幕截图显示预期的文本正确出现——由于保密问题而模糊处理

非中等成员可以通过这个链接继续阅读(来自 Medium 移动应用的读者可能因为一个现有的错误而无法点击链接,请在评论中找到链接)

我决定查看 Appium XML:

<XCUIElementTypeStaticText type="XCUIElementTypeStaticText" 
value="Bienvenue à bord de l'application&amp;#10;my-app-name !" 
enabled="true" visible="false" accessible="true" x="879" y="522" 
width="312" height="50" index="7"/>

那个小小的" "——一个偷偷摸摸的换行字符——已经悄悄地进入了我的应用程序的 XML 输出。它之前在 Appium 检查器中没有出现,我的开发人员发誓应用程序的代码库中没有发生变化。然而,我现在正在试图弄清楚为什么同一个测试在一个版本的应用程序中完美无缺地工作,但在另一个版本中却彻底失败。

我不禁想:“我们还要多久才能与这种脆弱的文本表示形式作斗争?”

打破一切的撇号

这不仅仅是一个单一的测试用例;在阅读报告时,我发现更多的测试失败,我查看了屏幕截图,文本与预期相同。我检查了 Appium XML,并发现了这个:

# Exemple of the expected text : 
That's not a big deal

Appium XML:

<XCUIElementTypeStaticText type="XCUIElementTypeStaticText" 
value="That's not a big deal" 
enabled="true" visible="false" accessible="true" x="879" y="522" 
width="312" height="50" index="7"/>

你发现了不同吗?我敢打赌你没有。

这不是同一个撇号:‘与‘不同

  • '(撇号或单直引号):这是标准的 ASCII 单引号(代码 39),它被称为直引号,常用于编程和文本文件。

  • '(右单引号):这是一个排版引号,它是扩展 Unicode 字符集的一部分(代码 U+2019)。它通常用于正确的排版(称为花括号引号智能引号),在自然语言文本中常用以表示所有格(例如,John's book)。

在 GitHub 上提出问题

我在应用程序的早期版本上运行了相同的测试,所有测试都通过了。

猜猜看?

测试通过了。应该是应用程序中有什么变化。但这似乎并不公平,因为 Appium 本应处理文本编码问题。我在 GitHub 上提出了这个问题,并强调了这一点。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/5c2dd1ce6aa91d19f5046ee04bf3d172.png

作者笔记本电脑截图

寻找更好的解决方案

对我来说,这只是一个更大问题的症状。

我意识到我们需要更稳健的东西,或者可能用不同的方法来解决问题……直接想到了如何制作不依赖于 XML 树和定位器的自动化……太棒了!但计算机视觉技术和像素到像素的方法并不适合我的许多需求。

这个想法让我探索 OCR 库。我之前使用过一些,但是在不同的上下文中。我知道它对于所有语言、文本样式等可能不是那么稳健,但我想知道:这些 OCR 库能否帮助弥合应用程序显示内容和测试看到的内容之间的差距?

我尝试了不同的方法、库和参数来探索和测试它们能为游戏带来什么,以及我所发现的内容改变了今天我对待测试自动化的方式。

让我分享一些这些见解,以及你如何将这些见解应用到自己的项目中。

我已经在第一部分(固定在我的个人资料中)中写过了定位问题,第二篇文章是关于计算机视觉技术,点击关注按钮以保持对我的文章的关注,我将会讨论自动驾驶测试和生成式 AI 进行测试断言(目前正在撰写一个关于类似主题的研究论文;如果你对此感兴趣,请随时在 X 上联系我。)

2. 从基础知识开始

OCR 通过分析图像、识别文本模式并将这些模式转换为机器可读字符来工作。

这里是魔法分解(一般而言):

  1. 图像处理:OCR 工具通过将图像转换为灰度、去除噪声或锐化边缘来预处理图像,以增强文本的清晰度。

  2. 文本检测:该工具识别图像中可能包含文本的区域。

  3. 字符识别:在这一步中,OCR 将单个字符的形状与其内置或训练的数据集进行匹配,以确定每个字符是什么。

  4. 输出生成:识别出的字符以机器可读的格式(纯文本、JSON、CSV 等)输出。

市面上有很多 OCR 工具;以下是一个快速回顾:

  • Tesseract OCR:这是一个存在多年的开源强大工具,支持超过 100 种语言……轻量级、可定制,非常适合文本密集型应用程序。

  • OpenCV 的 OCR 模块:主要以其图像处理能力而闻名,但它集成了 OCR 功能。

  • Google Vision API:如果你在寻找一个设置简单且精度极高的云 OCR 工具,它利用 AI 处理从噪点图像到多语言文本的各个方面。


构建我的 OCR 方法

所有这一切都始于一个简单的目标:在截图中发现特定的文本短语,定位它,并突出显示它。乍一看,这似乎是一个简单的任务。只需在图像上运行 OCR,找到文本,然后画一个框,对吧?

还不止这些。

让我带您了解我是如何使用 OCR 构建OCRAutomator类的,为什么它不像听起来那么简单,以及我们是如何一步步解决挑战的。

第 1 步:从图像中提取文本

第一步是从截图提取文本,使用 OCR 技术。为此,我使用了Tesseract。它可以将图像分解成单个字符并返回它们的坐标位置。

下面是执行繁重任务的代码:

def perform_ocr(self):
  self.ocr_data = pytesseract.image_to_data(self.image, output_type=Output.DICT)
  return [text.strip() for text in self.ocr_data['text'] if text.strip()]

使用这个功能,我现在可以将测试运行期间拍摄的截图输入到 OCR 引擎中,并检索文本及其边界框。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/5fb7d943de57d911b55468697f85fd81.png

这是 OCR 边界框后的截图

但有一个立即的问题:文本是碎片化的,一个短语如"Hello, world!"经常被分成单独的单词,甚至是个别字符。


第 2 步:将单词组合成短语

为了理解 OCR 输出,我需要将单词组合成连贯的短语……同一行上的单词应被视为一个整体,但前提是它们足够接近,属于同一个短语。

为了这个目的,我编写了一个函数,用于检查单词是否在同一行上,并且处于合理的水平距离内:

def group_words_into_phrases(self, max_distance=20):
    """Group OCR words into phrases based on line number and proximity."""
    phrases = []
    current_phrase = []
    for i in range(len(self.ocr_data['text'])):
            if not self.ocr_data['text'][i].strip():
                if current_phrase:
                    phrases.append(current_phrase)
                    current_phrase = []
                continue
            if not current_phrase:
                current_phrase.append(i)
            else:
                prev_idx = current_phrase[-1]
                # Check if the words are on the same line and close enough
                same_line = (self.ocr_data['line_num'][i] == self.ocr_data['line_num'][prev_idx])
                distance = self.ocr_data['left'][i] - (self.ocr_data['left'][prev_idx] + self.ocr_data['width'][prev_idx])
                if same_line and distance <= max_distance:
                    current_phrase.append(i)
                else:
                    phrases.append(current_phrase)
                    current_phrase = [i]
        if current_phrase:
            phrases.append(current_phrase)
        return phrases

通过这种方式,我最终可以从散乱的单词中重建短语,如“欢迎使用应用”。这对于准确找到文本,尤其是如果我想稍后点击它,至关重要。


第 3 步:突出显示目标短语

现在到了有趣的部分:为了记录目的和便于后续调试,在图像中视觉突出显示文本。一旦单词被组合成短语,我可以通过取其单词的最小和最大坐标来计算短语的边界框。

def calculate_bounding_box(self, phrase_indices):
    """Calculate the bounding box of a phrase."""
    x0 = min(self.ocr_data['left'][i] for i in phrase_indices)
    y0 = min(self.ocr_data['top'][i] for i in phrase_indices)
    x1 = max(self.ocr_data['left'][i] + self.ocr_data['width'][i] for i in phrase_indices)
    y1 = max(self.ocr_data['top'][i] + self.ocr_data['height'][i] for i in phrase_indices)
    return x0, y0, x1, y1

def highlight_phrase(self, target_phrase, output_path, highlight_color="red"):
    """Highlight the target phrase in the image and save the result."""
    phrases = self.group_words_into_phrases()
    draw = ImageDraw.Draw(self.image)
    for phrase_indices in phrases:
        phrase_text = " ".join(self.ocr_data['text'][i].strip() for i in phrase_indices)
        if target_phrase.lower() in phrase_text.lower():
            x0, y0, x1, y1 = self.calculate_bounding_box(phrase_indices)
            draw.rectangle([x0, y0, x1, y1], outline=highlight_color, width=3)
    self.image.save(output_path)

第 4 步:测试解决方案

最后,是时候测试它了。

就这样,目标文本被框起来并用红色突出显示,使其视觉验证变得容易。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/1703d7f5091528bb337b8f2b1388ada2.png

在一些处理后,OCR 检测到的文本的边界框

使用 OCR 进行自动化

在图像中查找和突出显示文本是一个好步骤,但这并不直接服务于自动化。我们的目的是通过点击文本、验证其存在或等待其出现等方式与应用程序进行交互等。

让我们添加一些 Appium(在我这个案例中,因为我正在开发移动应用),这样OCRAutomator类就变得对测试自动化有用。


点击文本

第一项任务是启用与文本的交互。相当简单:我们使用 OCR 检测文本,获取坐标,然后计算这些坐标的中心(检测到的文本的边界框坐标)。使用 Appium TouchAction,我可以进行点击:

from appium.webdriver.common.touch_action import TouchAction
def click_on_text(self, target_phrase, driver):
    """
    Find the target text and click it using Appium TouchAction.
    """
    self.perform_ocr()
    phrases = self.group_words_into_phrases()
    for phrase_indices in phrases:
        phrase_text = " ".join(self.ocr_data['text'][i].strip() for i in phrase_indices)
        if target_phrase.lower() in phrase_text.lower():
            center_x, center_y = self.calculate_text_center(phrase_indices)
            TouchAction(driver).tap(x=center_x, y=center_y).perform()
            print(f"Clicked on '{target_phrase}' at ({center_x}, {center_y})")
            return
    raise ValueError(f"Text '{target_phrase}' not found in the image.")

通过这种方式,我现在可以点击任何可见的文本,绕过对脆弱定位器的需求。


等待文本出现

有时,文本不会立即加载。为了验证文本显示,我使用一个简单的 wait_until_text_displayed 函数,利用现有的 OCR 逻辑:

import time
def wait_until_text_displayed(self, target_phrase, driver, timeout=10):
    """
    Wait until the target text is visible within the given timeout.
    """
    start_time = time.time()
    while time.time() - start_time < timeout:
        self.perform_ocr()
        for text in self.ocr_data['text']:
            if target_phrase.lower() in text.lower():
                print(f"Text '{target_phrase}' found!")
                return True
        time.sleep(1)
    raise TimeoutError(f"Text '{target_phrase}' not displayed within {timeout} seconds.")

这确保了测试不会继续进行,直到目标文本在屏幕上被确认可见。

最后的想法

在本文中,我们所看到的是使用 Pytesseract 库进行 OCR,该库轻量级且易于实现。我强烈建议您探索基于模型的 OCR、EasyOCR 库等——这可能会为更复杂的情况打开更强大和可扩展的解决方案的大门。

关注以阅读关于测试自动化中生成式 AI 的下一篇文章。您还可以查看我的以前的文章:

测试自动化中人工智能的演变:从定位器到生成式 AI(第二部分)

测试自动化中人工智能的演变:从定位器到生成式 AI(第二部分)

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐