开发 macOS 应用时,把屏幕上的文字“抓”下来复制到剪贴板,是一个很常见的自动化需求。之前我一直用第三方 OCR 服务或者 Tesseract,但配置麻烦、识别中文效果也不理想。后来发现 macOS 系统本身自带了 OCR 能力,通过 Vision 框架就能调用,不需要联网,也不依赖任何第三方 SDK。看到有人分享了一个叫 “Monkey see, monkey do” 的小项目,思路就是“眼睛看到什么,就能复制什么”,本质上就是屏幕截取 + 原生 OCR。这篇文章我就把这条链路完整拆解一遍,带你从零实现一个 macOS 原生 OCR 文字复制工具,代码可以直接跑。
本文适合 macOS 开发者、自动化脚本爱好者,也适合想了解 Vision 框架文本识别的大前端和 AI 方向读者。学完你可以掌握如何调用 macOS 原生 OCR 识别图片,如何截取屏幕区域,如何把识别结果写入剪贴板,并了解权限配置、性能优化和常见坑点。
1. 背景:macOS 上实现 OCR 有哪些方案
1.1 什么是 OCR
OCR(Optical Character Recognition,光学字符识别)的目的是从图像中提取文字。比如一张截图、一张扫描件、甚至摄像头拍到的画面,只要里面有文字,OCR 就能把它们变成可编辑、可复制的文本。
在 macOS 上做文字识别,常见方案有:
| 方案 | 类型 | 优缺点 |
|---|---|---|
| Tesseract | 开源本地引擎 | 免费,但需要安装语言包,中文识别精度一般 |
| 百度 OCR / 腾讯 OCR | 云端 API | 精度高,但要联网、有调用配额,涉及隐私 |
| macOS Vision | 系统原生框架 | 免费、离线、集成度高,支持中文,无需额外依赖 |
从工程角度看,如果你的工具只在 macOS 上跑,且不想把用户截图上传到云端,Vision 框架是最合适的选择。它内置在系统里,性能和识别率都经过苹果优化,尤其是 macOS 12 之后对中文、日文、韩文的支持已经比较完善。
1.2 “Monkey see, monkey do” 想表达什么
“Monkey see, monkey do” 是一个英文俗语,意思是“看到什么就模仿什么”。放到 OCR 工具里,就是一个很形象的产品逻辑:屏幕上出现什么文字,工具就把它识别并复现出来。
你可以想象这样的使用场景:
- 看视频时发现字幕里有想要保存的句子,直接一框选就复制;
- 阅读加密 PDF 或图片型 PDF 时,无法选中文字,用 OCR 工具提取;
- 软件界面上的报错信息无法复制,直接截屏识别;
- 开会时想把幻灯片里的文字快速做成笔记。
这些需求都可以用 macOS 原生能力实现,不需要购买专业软件。
1.3 为什么推荐使用系统原生 OCR
使用 Vision 框架的好处很明显:
- 离线可用:不依赖网络,不把数据上传到第三方服务器;
- 免费无限量:本地计算,没有 API 配额和费用;
- 隐私安全:截图留在本机,适合处理敏感文档;
- 中文支持好:系统语言包已经内置,开箱即用;
- 与 macOS 系统深度集成:可以配合快捷键、Automator、Shortcuts 使用。
如果你的应用只面向 macOS,优先用系统能力往往是性价比最高的方案。
2. 环境准备与版本说明
在开始写代码之前,先确认你的开发环境和运行环境。
2.1 操作系统版本
Vision 框架从 macOS 10.15 开始引入文本识别能力,但早期的VNRecognizeTextRequest仅支持英文。如果你需要识别中文,建议使用 macOS 12 Monterey 及之后版本,此时 Live Text 已内置,识别质量和语言丰富度提升明显。
本文示例基于 macOS 13/14 实测,理论兼容 macOS 12+。如果你的系统是 macOS 10.15 或 11,代码大部分可用,但中文识别可能受限。
2.2 开发工具
- Xcode:可以使用 Xcode 14 或更高版本,命令行工具也可以单独安装;
- Swift:示例代码基于 Swift 5;
- Command Line Tools:如果你不想建完整 Xcode 工程,直接用
swift命令编译运行也可以。
检查 Command Line Tools 是否安装:
xcode-select --install查看 Swift 版本:
swift --version2.3 运行权限
如果你的 OCR 工具只识别图片文件,不需要额外权限。但如果你要实现屏幕实时取词,需要给终端或 App 授权“屏幕录制”权限:
- 系统设置 → 隐私与安全性 → 屏幕录制;
- 把你运行工具所在的终端 App(比如 Terminal、iTerm)勾选上。
如果你要把工具打包成独立 App,还需在 App 的Info.plist中声明:
<key>NSScreenCaptureUsageDescription</key> <string>需要截取屏幕内容以进行 OCR 文字识别</string>如果是命令行工具,没有 Info.plist,但系统仍会在第一次调用截屏 API 时弹出授权提示。
2.4 示例项目结构
为了让示例尽量简单,我们不建 Xcode 工程,直接用 Swift 脚本。项目结构如下:
monkey-see/ ├── main.swift ├── Package.swift # 可选,如果使用 SwiftPM └── images/ └── sample.png你可以把main.swift当成独立命令行工具运行。
3. 核心原理:Vision 框架如何做文字识别
Vision 是苹果提供的计算机视觉框架。它并不直接对外暴露 UI,而是通过 request-handler 模式工作。
3.1 核心概念
VNRecognizeTextRequest:文本识别请求对象,负责配置识别参数;VNImageRequestHandler:图像请求处理器,负责把图片数据交给 Vision 分析;VNRecognizedTextObservation:识别结果,包含文字内容和坐标位置。
一个完整的识别流程是:
- 把图片转换为
CGImage; - 创建
VNRecognizeTextRequest; - 创建
VNImageRequestHandler并执行请求; - 遍历
VNRecognizedTextObservation提取文本。
3.2 识别精度设置
recognitionLevel有两个选项:
.fast:速度快,适合实时截屏;.accurate:精度高,适合从图片中提取文字。
对于截图场景,建议先用.accurate提高准确率,如果性能不够再改为.fast。
3.3 语言支持
通过recognitionLanguages可以指定识别语言。常用语言代码:
zh-Hans:简体中文;en-US:英文;ja-JP:日文;ko-KR:韩文。
如果希望同时识别中英文,可以传入多个语言:
request.recognitionLanguages = ["zh-Hans", "en-US"]注意:并非所有语言在所有系统版本上都可用,建议做一次可用语言查询。
3.4 坐标系统
Vision 的坐标系统与 UIKit/AppKit 不同。Vision 使用归一化坐标,原点在图片左下角,y 轴向上。如果你需要把文字框画在截图原图上,需要做坐标转换。
3.5 核心代码示例
下面是一个最小可用的 Swift 函数,用来识别一张 PNG 图片中的所有文字:
import Vision import AppKit func recognizeText(from image: NSImage) -> String { guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return "" } var resultText = "" let request = VNRecognizeTextRequest { request, error in guard let observations = request.results as? [VNRecognizedTextObservation] else { return } for observation in observations { if let candidate = observation.topCandidates(1).first { resultText += candidate.string + "\n" } } } request.recognitionLevel = .accurate request.recognitionLanguages = ["zh-Hans", "en-US"] request.usesLanguageCorrection = true let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) try? handler.perform([request]) return resultText }这段代码中:
topCandidates(1)返回置信度最高的一个候选文本;usesLanguageCorrection会利用语言模型修正识别结果,默认开启;try? handler.perform忽略错误,实际项目中建议用do-catch处理。
4. 完整实战:从屏幕截取到复制文字
接下来我们实现一个完整的命令行工具:用户启动程序后,截取当前屏幕,自动识别屏幕上的所有文字,并复制到剪贴板。
4.1 创建 Swift 脚本
先创建一个文件夹:
mkdir monkey-see && cd monkey-see touch main.swift用文本编辑器打开main.swift。
4.2 获取屏幕截图
macOS 截取主屏幕可以使用 Core Graphics 的CGDisplayCreateImage。这个 API 会返回一个CGImage,可以直接交给 Vision 处理。
import CoreGraphics func captureScreen() -> CGImage? { let screenRect = CGMainDisplayBounds() guard let image = CGDisplayCreateImage(CGMainDisplayID()) else { return nil } return image }CGMainDisplayID()返回主显示器 ID,CGDisplayCreateImage会生成该显示器的图像。如果你想截取某个区域,可以在生成图片后使用cropping(to:)裁剪。
4.3 对截图进行 OCR 识别
我们复用上一节的recognizeText函数,但输入变成CGImage,避免 NSImage 转换开销:
import Vision func recognizeText(from cgImage: CGImage) -> String { var resultText = "" let request = VNRecognizeTextRequest { request, error in guard error == nil else { print("识别错误: \(error!.localizedDescription)") return } guard let observations = request.results as? [VNRecognizedTextObservation] else { return } for observation in observations { if let candidate = observation.topCandidates(1).first { resultText += candidate.string + "\n" } } } request.recognitionLevel = .accurate request.recognitionLanguages = ["zh-Hans", "en-US"] request.usesLanguageCorrection = true let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) do { try handler.perform([request]) } catch { print("执行失败: \(error.localizedDescription)") } return resultText }4.4 写入剪贴板
macOS 里写入剪贴板最简单的方式是使用NSPasteboard:
import AppKit func copyToPasteboard(_ text: String) { let pasteboard = NSPasteboard.general pasteboard.clearContents() pasteboard.setString(text, forType: .string) }注意:命令行工具使用 AppKit 需要保证有图形会话,普通终端运行没有问题。
4.5 主程序组装
在main.swift里把这三步串起来:
import Foundation import AppKit import Vision import CoreGraphics // ... 上面定义的函数 ... @main struct MonkeySee { static func main() { print("正在截取屏幕...") guard let screenImage = captureScreen() else { print("截屏失败") exit(1) } print("正在进行 OCR 识别...") let text = recognizeText(from: screenImage) guard !text.isEmpty else { print("没有识别到文字") exit(0) } copyToPasteboard(text) print("识别完成,已复制以下内容到剪贴板:") print("-----------------------------") print(text) } }如果你不想用@main,也可以直接把顶层代码写在main.swift中。
4.6 编译运行
由于 Swift 命令行工具可以直接从源文件编译,执行:
swiftc -o monkey-see main.swift -framework Vision -framework AppKit -framework CoreGraphics然后运行:
./monkey-see首次运行可能会弹出“终端想要截取屏幕图像”的授权提示,点击允许后需要重新运行。
4.7 测试效果
准备一张包含中英文文字的图片,或者打开任意网页,运行工具后:
正在截取屏幕... 正在进行 OCR 识别... 识别完成,已复制以下内容到剪贴板: ----------------------------- 这个网站使用 Cookie 来增强您的浏览体验。 Hello, welcome to my blog!打开文本编辑器粘贴,就能看到完整内容。
5. 进阶:实现矩形区域 OCR 和快捷键唤起
整屏识别虽然简单,但实际使用时往往只需要识别某一块区域。比如弹窗中的错误信息、视频字幕、表格中的某一列。下面我们增加区域选择能力。
5.1 使用screencapture命令配合区域截图
最简单的方式是调用系统自带的screencapture命令行工具。它可以让你手动框选区域。
screencapture -i /tmp/ocr_selection.png如果希望截取后光标变成十字准线,并允许用户拖动选择区域,使用-i交互模式。截图成功后,生成的 PNG 文件再交给 OCR 识别。
5.2 Swift 调用screencapture
在 Swift 中可以用Process执行系统命令:
import Foundation func captureSelection() -> URL? { let url = URL(fileURLWithPath: "/tmp/ocr_selection.png") let process = Process() process.executableURL = URL(fileURLWithPath: "/usr/sbin/screencapture") process.arguments = ["-i", url.path] do { try process.run() process.waitUntilExit() if FileManager.default.fileExists(atPath: url.path) { return url } } catch { print("截屏失败: \(error)") } return nil }注意screencapture在较新的 macOS 中路径通常是/usr/sbin/screencapture,也可能位于/sbin,建议用xcrun --find screencapture查找。
5.3 从文件加载图片并识别
拿到截图文件后,用 NSImage 加载:
if let url = captureSelection() { guard let image = NSImage(contentsOf: url), let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { print("无法加载截图") exit(1) } let text = recognizeText(from: cgImage) copyToPasteboard(text) print(text) }5.4 绑定快捷键
如果你希望按下某个快捷键就启动“区域 OCR”流程,可以使用全局热键库,比如:
- Carbon 的
RegisterEventHotKey; - 第三方库
HotKey(SwiftPM); - macOS 自带 Shortcuts 快捷指令。
用 Carbon 实现全局快捷键比较繁琐,这里给出思路。更推荐使用HotKey这个开源库,它封装了事件监听,使用起来更现代。
import HotKey let hotKey = HotKey(key: .space, modifiers: [.control]) hotKey.keyDownHandler = { // 执行区域截图和 OCR startRegionCaptureAndRecognize() }注意:第三方库需要额外集成,如果是个人学习,完全可以用 Automator 或者 BetterTouchTool 去绑定快捷键。
5.5 权限补充
使用screencapture -i时,系统同样会要求屏幕录制权限。如果出现黑屏或无法截图,请检查权限设置。
6. 常见问题与排查思路
6.1 问题对照表
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 截屏返回空图 | 没有屏幕录制权限 | 系统设置中勾选终端的屏幕录制权限 |
| 识别不到任何文字 | 图片中没有文字 / 识别语言未配置 | 换测试图片;添加语言参数 |
| 中文识别乱码 | recognitionLanguages只设置了en-US | 添加zh-Hans |
| 坐标错位 | Vision 坐标系与 AppKit 不一致 | 转换坐标:y = 1 - normalizedY |
| 运行效率低 | 使用.accurate且图片过大 | 调整为.fast,或缩小图片尺寸 |
| 命令行工具无法使用 AppKit | 未导入AppKit框架 | 编译时加-framework AppKit |
| 弹出安全提示 | 系统首次请求屏幕录制权限 | 点击允许后重启终端 |
6.2 识别不了中文怎么办
首先确认系统版本。macOS 12 以下对中文支持有限,建议升级系统。其次在请求中明确指定中文:
request.recognitionLanguages = ["zh-Hans"]如果仍无效,用下面代码查询当前可用的识别语言:
let supported = try? VNRecognizeTextRequest.supportedRecognitionLanguages(for: .accurate, revision: VNRecognizeTextRequestRevision3) print(supported ?? [])输出结果会列出所有支持的语言代码。
6.3 截屏权限出现“系统数据占用过大”或无法截取
这和 OCR 本身无关,但很多用户会遇到。如果你的 macOS 系统提示“系统数据占用过大”,可以检查“屏幕录制”权限列表是否有残留的已卸载 App。清理方法:重置终端权限或重启系统。
如果鼠标光标在screencapture -i交互模式下不显示,可能是全屏截图权限未开启,或者正在使用虚拟机。虚拟机环境下屏幕捕获行为与真实 macOS 不同,请参考虚拟机的显示驱动配置。
6.4 OCR 识别结果顺序不对
Vision 返回的观察结果并不是绝对按阅读顺序排列的。解决办法是手动排序:
let sortedObservations = observations.sorted { a, b in if abs(a.boundingBox.midY - b.boundingBox.midY) > 0.01 { return a.boundingBox.midY > b.boundingBox.midY } else { return a.boundingBox.minX < b.boundingBox.minX } }由于 Vision 坐标原点在左下角,midY越大表示位置越靠上。按从下到上?不,这里我们是要按阅读顺序:先按 y 降序(上到下),同一行内按 x 升序(左到右)。
6.5 编译时找不到 Vision 框架
确保命令行编译时添加了-framework Vision:
swiftc -o monkey-see main.swift -framework Vision -framework AppKit -framework CoreGraphics如果你使用 SwiftPM,则需要在Package.swift中链接系统框架。
7. 最佳实践与工程建议
7.1 合理选择识别精度
如果你的应用需要实时处理视频帧,建议使用.fast,并在每帧之间做节流。对于静态截图,.accurate更合适。
7.2 提高识别准确率
- 识别前对图片做二值化或增强对比度;
- 将图片缩放至合适大小,过大或过小都会影响效果;
- 对旋转的文字设置
recognitionLevel = .accurate,但仍有限度; - 对结果做后处理,例如去掉多余空格、修正标点。
7.3 隐私与安全
- 不要把截屏图片明文保存在磁盘上,用完即删;
- 如果网络传输 OCR 结果,务必加密;
- 如果你的 App 会处理敏感数据,在隐私政策中明确说明;
- 在用户授权前不要触发截图,否则可能被系统拒绝。
7.4 处理多语言
建议根据用户系统语言动态设置识别语言,而不是写死。可以通过Locale.preferredLanguages获取当前语言,映射到 Vision 的语言代码。
7.5 内存管理
Vision 处理大图时会占用较多内存。在命令行工具中,识别完成后及时释放对象。如果是 iOS/macOS App,建议将识别任务放到后台队列:
DispatchQueue.global(qos: .userInitiated).async { let text = recognizeText(from: cgImage) DispatchQueue.main.async { self.textView.string = text } }7.6 日志与错误处理
生产工具一定要有完整的错误日志。可以把每次识别耗时、识别语言、候选文字数量记录下来,方便后续调优。
7.7 打包成 App
如果你把工具分发给同事使用,可以打包成.app,并在Info.plist声明权限。使用osascript或 Automator 也能快速做成“服务”菜单项,无需 Xcode。
7.8 与最新系统兼容
macOS Sequoia 发布后,很多用户遇到“若要打开此 App,你需要从 macOS 恢复启动,并将安全策略更改为完整安全”的问题。这本质上是自定义内核扩展或未签名 App 的安全策略设置。对普通 OCR 工具来说,建议使用 Developer ID 签名,避免用户被安全策略卡住。
7.9 性能优化方向
如果你要做“全局 OCR 搜索”这类工具,可以考虑:
- 监听屏幕变化,增量识别;
- 使用
VNImageRequestHandler的regionOfInterest只识别变化区域; - 使用 GPU 加速,但 Vision 默认已经使用系统加速能力;
- 建立文字缓存,避免重复识别。
8. 总结与下一步学习方向
通过本文的实战,你已经掌握了 macOS 原生 OCR 的核心链路:
- 使用
VNRecognizeTextRequest识别图片文字; - 使用
CGDisplayCreateImage或screencapture截取屏幕; - 使用
NSPasteboard写入剪贴板; - 处理屏幕录制权限;
- 通过识别语言参数解决中文支持问题。
这套方案非常适合做个人效率工具,比如“截图 OCR”“屏幕取词”“离线文字提取”。如果想继续深入,可以尝试:
- 在 iOS 上使用同样的 Vision API 实现拍照取词;
- 结合 Core ML 做自定义文字分类;
- 利用 Live Text 的底层能力做更聪明的 OCR 应用;
- 把 Swift 代码封装成命令行工具,通过 Python 调用,形成更灵活的自动化脚本。
关于 Python 调用,可以借助PyObjC框架,但需要安装对应 macOS 版本支持的 pyobjc-framework-Vision 包。环境配置时要注意 Python 版本与 PyObjC 的兼容性,建议在虚拟环境中安装。
最后提醒一点:屏幕录制权限在开发调试阶段经常被忽略,一旦发现截屏全黑,优先检查“隐私与安全性”里的授权列表。调试完成后,把权限限定在最小范围,保障用户隐私。
如果你也打算做一个类似的“看到什么就复制什么”的小工具,不妨先从今天的示例改造起。加上一个快捷键,再配合区域截图,就是一个相当顺手的日常效率工具了。