咱们今天不聊那些虚头巴脑的理论,直接上干货。你是不是也遇到过这种抓狂的情况:手里有个老旧的ERP系统、一个没有API接口的内部工具,或者是一个反爬机制严密的网页弹窗,你想把里面的数据弄出来,但右键“检查元素”发现全是黑盒?这时候,传统的爬虫脚本就像是个瞎子,什么都看不见。
别急,我是Agnes,今天我就带你用Python给这个“瞎子”装上眼睛。我们要做的,不是去破解软件的加密,而是像人类一样,去“看”屏幕上的文字。这就是UI自动化测试和RPA(机器人流程自动化)的核心——通过识别图形界面元素来获取文本。
为什么你需要“看见”屏幕?
在深入代码之前,我们先理清一个概念:为什么不用Requests或BeautifulSoup?
因为那些库处理的是HTTP请求和HTML源码。而很多桌面软件(WinForms, WPF, Qt)或者复杂的Web应用(Shadow DOM, Canvas绘制),它们的文本并不直接暴露在DOM树里,或者根本不在浏览器中运行。
这时候,我们需要借助操作系统的底层接口,去询问窗口:“嘿,把你那个编辑框里的字念给我听听。”
目前主流的两大流派:
- Windows平台:
pywinauto+uiautomation。这是王者组合,专门对付Windows原生应用。 - 跨平台/Web平台:
PyAutoGUI+OCR(如Tesseract或PaddleOCR)。这是万能钥匙,虽然慢点,但只要有像素,就能读出来。
今天,我会重点讲最稳定、最高效的 pywinauto 方案,因为它不需要截图,直接获取原生文本,速度快且准确率高。
第一步:环境搭建与“透视眼”准备
工欲善其事,必先利其器。我们需要安装几个关键的库。
pip install pywinauto Pillow opencv-python numpy
pywinauto: 核心库,用于连接Windows窗口和控制控件。Pillow&opencv-python: 备用方案,如果pywinauto搞不定,我们就截图然后用OCR识别(虽然今天我们主要讲原生抓取,但备胎得备好)。
接下来,我们要装一个神器:Inspect.exe 或 Accessibility Insights。 这是微软自带的工具(或者你可以下载Accessibility Insights for Windows),它能让你看到任何窗口的“内部结构”。就像X光一样,你能看到哪个按钮是“Button”,哪个文本框是“Edit”或“TextBox”。
专家提示:很多新手失败的原因就是没分清控件类型。有的软件用的是Win32 API(旧式),有的是UIA(现代,基于Accessibility)。
pywinauto默认首选backend='uia',如果不行,再尝试backend='win32'。
第二步:实战演练——抓取一个典型的Windows窗口
假设我们有一个简单的记事本,或者某个内部系统的登录框。我们要做的步骤是:
- 连接到目标进程或窗口。
- 定位到具体的文本框控件。
- 读取其文本属性。
场景一:抓取记事本中的内容(简单入门)
让我们写一段代码,打开记事本,输入一些字,然后把它抓回来。这能帮你建立信心。
from pywinauto import Application
import time
# 1. 启动应用程序
app = Application(backend="uia").start("notepad.exe")
# 等待窗口完全加载
app.top_window().wait('ready', timeout=10)
# 2. 找到文本编辑控件
# 在Notepad中,主文本区域通常被称为 'Edit' 或者 'RichEditBox'
# 我们可以通过类名(class_name)或者控制类型(control_type)来定位
edit_control = app.top_window().child_window(control_type="Edit")
# 3. 模拟输入(假装我们是用户)
edit_control.type_keys("Hello Agnes! This is a test.", with_spaces=True)
time.sleep(1) # 稍微停顿,确保输入完成
# 4. 核心操作:获取文本
# get_value() 是最常用的方法,它返回控件的当前值
text_content = edit_control.get_value()
print(f"抓取到的内容是: {text_content}")
# 清理现场
app.top_window().close()
代码解析:
Application(backend="uia"): 指定使用UI Automation后端,这是现代Windows应用的标准。child_window(control_type="Edit"): 这是关键。我们告诉程序,我要找那个类型为“编辑框”的东西。get_value(): 直接提取文本,而不是截图。这意味着你拿到的是纯字符串,可以直接存入数据库或Excel。
场景二:抓取复杂的企业级软件(挑战升级)
现实世界没那么美好。你可能面对的是一个Deepin、WPF或者Qt开发的软件。有时候,control_type="Edit" 找不到,或者找到了多个。这时候需要更精准的定位策略。
让我们看一个更健壮的查找方式:通过标题、类名或层级关系。
from pywinauto import Application
from pywinauto.findwindows import ElementNotFoundError
def grab_text_from_app(app_name, control_title=None, control_class=None):
"""
通用抓取函数
:param app_name: 应用程序标题或进程名
:param control_title: 控件的标题或名称
:param control_class: 控件的类名
:return: 文本内容
"""
try:
# 连接已运行的窗口,或者启动新窗口
# 如果窗口已经打开,可以用 connect
# app = Application(backend="uia").connect(title_re=app_name)
app = Application(backend="uia").start(app_name)
# 获取顶层窗口
window = app.window(title_re=app_name)
window.wait('visible', timeout=10)
# 定义查找条件
kwargs = {}
if control_title:
kwargs['title'] = control_title
if control_class:
kwargs['class_name'] = control_class
# 如果没指定具体控件,尝试获取第一个Edit控件
if not kwargs:
kwargs['control_type'] = "Edit"
# 查找控件
control = window.child_window(**kwargs)
# 获取文本
text = control.get_value()
return text
except ElementNotFoundError as e:
print(f"错误:找不到指定的控件。请检查标题或类名是否正确。细节: {e}")
return None
except Exception as e:
print(f"发生未知错误: {e}")
return None
# 示例调用(假设你有一个名为 "MyApp" 的程序,其登录框标题为 "Password")
# result = grab_text_from_app("MyApp", control_title="Password")
这里有个坑:有些软件的文本框是自定义绘制的,get_value() 可能返回空字符串。这时候怎么办?
第三步:终极保底方案——OCR光学字符识别
如果 pywinauto 搞不定,比如那个文本框是画在Canvas上的,或者被加密渲染了,我们就退一步:截图+OCR。
虽然速度比直接读取慢,但它是万能的。只要人眼能看到的,OCR就能读出来。
我们需要结合 Pillow 截图和 pytesseract 或百度/阿里OCR API。为了演示本地离线方案,我们用 pytesseract(需要安装Tesseract-OCR引擎)。
import os
import subprocess
from PIL import ImageGrab
import pytesseract
def grab_text_via_ocr(target_window_title, crop_area=None):
"""
通过截图和OCR抓取文本
:param target_window_title: 窗口标题
:param crop_area: 可选,裁剪区域 (left, top, right, bottom),相对屏幕坐标
:return: 识别出的文本
"""
# 1. 找到窗口并获取屏幕坐标
from pywinauto import Desktop
desktop = Desktop(backend="uia")
try:
window = desktop.window(title_re=target_window_title)
rect = window.rectangle() # 获取窗口矩形区域 (left, top, right, bottom)
except Exception:
print("无法找到窗口")
return None
# 2. 截图
# 如果指定了裁剪区域,可以在rect基础上进一步裁剪,这里简化为截取整个窗口
screenshot = ImageGrab.grab(bbox=(rect.left, rect.top, rect.right, rect.bottom))
# 3. 预处理图像(提高OCR准确率的关键!)
# 转换为灰度图
img_gray = screenshot.convert('L')
# 二值化,增加对比度
threshold = 128
img_binary = img_gray.point(lambda x: 0 if x < threshold else 255, '1')
# 保存临时图片以便调试
temp_img_path = "temp_ocr.png"
img_binary.save(temp_img_path)
# 4. 调用Tesseract进行识别
# config='--psm 6' 表示假设图像是一个统一的文本块
text = pytesseract.image_to_string(img_binary, lang='chi_sim+eng', config='--psm 6')
# 清理临时文件
if os.path.exists(temp_img_path):
os.remove(temp_img_path)
return text.strip()
# 使用示例
# ocr_result = grab_text_via_ocr("我的神秘软件")
# print(f"OCR识别结果: {ocr_result}")
专家经验谈:
很多人直接用 ImageGrab.grab() 然后扔给OCR,结果识别率惨不忍睹。预处理是灵魂。
- 灰度化:去掉颜色干扰。
- 二值化:黑白分明,让文字边缘锐利。
- 降噪:如果背景有杂色,可能需要高斯模糊后再二值化。
- 语言包:务必指定
lang='chi_sim'或'chi_sim+eng',否则默认英文模型识别中文全是乱码。
第四步:高级技巧与避坑指南
作为过来人,我必须提醒你几个在实际项目中经常遇到的“雷区”。
1. 动态ID与弱引用
有些软件的控件ID是动态生成的,比如 Edit_12345。这时候不要用硬编码的ID,要用正则表达式 title_re 或者层级关系 parent.child_window()。
2. 多线程与线程安全
pywinauto 在某些情况下对线程敏感。如果你在主线程中启动应用,最好也在主线程中进行操作。如果需要异步,确保使用 Application.connect() 而不是重复启动。
3. 权限问题
Windows 10⁄11 的某些应用(如UWP应用)运行在沙箱中,普通权限的Python脚本可能无法获取其内部控件。
- 解决方案:以管理员身份运行你的Python脚本。
- 替代方案:如果还是不行,只能求助于OCR,因为截图不需要高权限。
4. 性能优化
如果你需要抓取大量数据,频繁地 type_keys 和 get_value 会很慢。
- 技巧:批量操作。先定位好所有控件,然后再循环读取。
- 技巧:减少截图频率。OCR是很耗CPU的,能用
pywinauto直接读就别截图。
第五步:一个完整的综合案例——自动化报表提取
假设你要从公司的“财务管理系统”中,每天提取“应收账款”表格里的金额。这个系统是C/S架构,界面如下:
- 窗口标题:
Finance System v2.0 - 查询按钮:
btn_search - 结果列表:
list_view_results
我们不能直接读列表,但可以读“总计”栏,或者通过点击导出CSV功能。这里我们演示如何通过模拟点击和读取状态栏来完成。
from pywinauto import Application
import time
class FinanceBot:
def __init__(self):
self.app = None
self.window = None
def start(self):
# 启动软件
self.app = Application(backend="uia").start(r"C:\Program Files\FinanceSystem\client.exe")
self.window = self.app.window(title_re="Finance System")
self.window.wait('ready', timeout=15)
print("系统已启动,等待操作...")
def login(self, username, password):
# 定位用户名和密码框
# 注意:实际开发中需通过Inspect工具确认控件名称
user_box = self.window.child_window(title="Username", control_type="Edit")
pass_box = self.window.child_window(title="Password", control_type="Edit")
btn_login = self.window.child_window(title="Login", control_type="Button")
user_box.set_edit_text(username)
pass_box.set_edit_text(password)
btn_login.click_input()
time.sleep(2) # 等待登录跳转
print("登录成功")
def extract_receivables(self):
# 导航到应收账款模块
menu_tree = self.window.child_window(title="Navigation Tree", control_type="Tree")
# 展开节点... (简化演示)
receivables_item = menu_tree.child_window(title="Accounts Receivable")
receivables_item.double_click_input()
time.sleep(3)
# 执行查询
btn_query = self.window.child_window(title="Query", control_type="Button")
btn_query.click_input()
time.sleep(2)
# 获取结果统计
# 假设有一个标签显示 "Total Amount: $10,000.00"
total_label = self.window.child_window(title_re="Total.*Amount", control_type="Text")
total_text = total_label.window_text()
# 简单的字符串解析
import re
match = re.search(r'\$([\d,]+\.?\d*)', total_text)
if match:
amount = float(match.group(1).replace(',', ''))
print(f"今日应收账款总额: ${amount}")
return amount
else:
print("未找到总金额标签")
return None
def close(self):
if self.app:
self.app.kill()
# 运行机器人
if __name__ == "__main__":
bot = FinanceBot()
try:
bot.start()
bot.login("admin", "password123")
bot.extract_receivables()
finally:
bot.close()
结语:技术之外的思考
写到这里,你可能已经掌握了如何抓取GUI文本的精髓。但我想说的是,工具只是手段,逻辑才是核心。
在选择 pywinauto 还是 OCR 时,你要权衡稳定性和兼容性。
- 如果是内部系统,界面固定,首选
pywinauto,速度快,数据准。 - 如果是外部软件,界面经常变,或者涉及游戏、加密软件,
OCR+图像识别是你的救命稻草。
另外,别忘了伦理和法律边界。自动化抓取数据仅限于你拥有权限的系统,或者用于个人效率提升。不要尝试绕过付费墙、窃取他人隐私或破坏系统安全。
希望这篇文章能帮你解开软件自动化的难题。如果你在实际操作中遇到具体的控件定位问题,记得先用 Inspect.exe 看看它的“真面目”。
祝你在自动化的道路上,玩得开心,效率翻倍!如果有其他技术问题,随时欢迎交流。