你是不是也有一堆 JPG 图片,文件名全是“IMG_20231025_093021.jpg”这种,想找某一张图只能一张张点开预览?我之前在整理扫描合同和产品截图时被这个问题折腾得够呛,后来干脆花两个晚上做了一个 WPF 小工具:先框住图片上要识别的区域,程序自动调用腾讯云 OCR 把区域里的文字读出来,再直接用这段文字把文件改名。今天就把完整思路、代码细节和踩坑记录分享一下,有 C# 基础或者正在做 WPF 相关工具的朋友可以直接参考。
这个工具解决的核心问题很明确:批量、区域、自动识别、自动重命名。不是整张图一股脑识别,而是只需要图片里某一块区域的文字。典型场景包括:扫描件里的合同编号、票据里的发票号码、商品截图上的型号、图纸角落里的图号。这类文件只要名字里有这些关键信息,后面检索起来会轻松很多。
1. 需求拆解与技术选型:为什么是 WPF + 腾讯 OCR
1.1 拆解“批量图片区域识别改名”这个需求
实现之前先别急着写代码,把需求拆开看。“批量图片区域识别改名”这句话里有四个关键点:
- 批量:要处理的是文件夹里几十上百张图片,单张识别用手机就够,没必要做工具;
- 区域:不是识别整张图片,而是只读取图片里某个矩形区域内的文字。这是这个需求最容易被忽视、也最容易做错的地方;
- 识别:要用 OCR 引擎把图片中的文字转换成字符串;
- 改名:把识别出来的文字清洗成合法文件名,替换掉原来的文件名,保留 jpg 扩展名。
如果把这四个点拆成独立模块,整个程序的架构就清楚了:一个负责文件遍历,一个负责图片处理和区域裁剪,一个负责 OCR 调用,一个负责重命名和冲突处理,剩下的就是 UI 交互。模块化拆分是这类小工具最值得坚持的原则,后面不管替换 OCR 服务还是调整命名规则,改动成本都很低。
还有一个点很容易被忽略:区域识别不是 OCR 服务提供的功能。常见的云 OCR 接口基本都是整图识别,所谓“区域识别”本质上是先按用户框选的坐标把图片裁剪出来,再把裁剪后的局部图片送给 OCR。理解这一点,整个流程就顺了。
1.2 为什么是 WPF,而不是 WinForms 或 Web
选 WPF 有几个实际原因,不是因为它“新”,而是因为它适合这类桌面小工具。
第一,WPF 的 UI 布局和样式定制能力比 WinForms 强太多了。这个工具需要左右两栏布局,左边是文件列表,右边是图片预览,还要在预览图上拖拽画矩形框。WPF 里用 Canvas 加上鼠标事件就能做,界面写出来也顺眼。WinForms 也能做,但坐标换算和自绘逻辑要手动处理很多,见效慢。
第二,这个工具需要读本地文件系统,和文件管理器交互频繁。做成桌面程序比做成 Web 服务更直接。当然 Electron 也能做,但为了一个几十 MB 的小工具引入整个 Chromium 运行时,个人觉得不划算,装个 100 多 MB 的东西只为了改图片名,有点杀鸡用牛刀。
第三,WPF 的数据绑定体系适合这类“列表 + 状态更新”的交互。图片列表里的每一条记录有文件名、识别结果、处理状态,用 ObservableCollection 加属性通知就能轻松做到界面实时刷新。用 WinForms 做这种列表刷新要手动操作 DataGridView,代码会啰嗦不少。
不过 WPF 有一个学习门槛:XAML 和绑定语法。如果你之前只用过 WinForms,可以直接用 Code-Behind 方式写,不强制 MVVM 模式。工具类程序,先跑起来比模式重要。
1.3 OCR 方案对比:腾讯云、百度、Tesseract、PaddleOCR
OCR 引擎是整个方案的灵魂,选型时我认真对比了几种方案。
| 方案 | 部署方式 | 识别率 | 开发成本 | 费用 |
|---|---|---|---|---|
| 腾讯云通用印刷体 OCR | 云端 API | 印刷体识别率高,中文支持好 | 低,官方 SDK 完善 | 有免费额度,超出后按量计费 |
| 百度 OCR | 云端 API | 印刷体表现也很好 | 低 | 有免费额度,接口文档成熟 |
| Tesseract | 本地开源 | 对清晰印刷体可以,复杂排版较弱 | 中,要处理语言包和预处理 | 免费 |
| PaddleOCR | 本地开源 | 精度不错,尤其中文场景 | 中高,环境配置稍复杂 | 免费 |
我个人最终选了腾讯云,原因有三点:一是官方 .NET SDK 比较完善,照着文档就能接入;二是通用印刷体识别对中文和数字的识别率稳定;三是免费额度对个人整理图片基本够用。
Tesseract 我也试过,最大的问题是它“任性”:对白底黑字的清晰扫描件表现不错,但遇到图片背景复杂、文字带颜色或者有轻微倾斜,识别结果就变得离谱。PaddleOCR 本地部署效果确实好,但需要 Python 环境或者 VC++ 运行库,做 GUI 集成时打包发布比较折腾。如果纯粹想快速解决问题,云端 OCR 是最省力的选择。
2. 准备工作:账号密钥、WPF 工程与整体结构
2.1 开通腾讯云 OCR 与获取密钥
使用腾讯云 OCR 需要有一个腾讯云账号,然后在控制台搜索“文字识别”,开通通用印刷体识别服务。开通后需要在访问管理里创建一对 API 密钥,也就是 SecretId 和 SecretKey,这两个字符串类似账号密码,调用接口时用来做身份签名。
创建密钥时有几个建议:
- 建议使用子账号密钥,并只授予 OCR 相关权限,避免主账号密钥泄露带来的风险;
- 不要把这些密钥硬编码在代码里。工具自己用无所谓,但如果要发给别人,至少做成配置文件;
- 地域参数一般填 ap-guangzhou,腾讯云 OCR 接口签名时都需要带地域信息。
密钥这东西,一旦泄露别人就能用你的额度调用接口,产生费用。所以本地存放时要谨慎,不要随手传到 GitHub 仓库。
2.2 创建 WPF 工程与 NuGet 依赖
开发环境建议使用 Visual Studio 2022,.NET 版本直接用 .NET 6 或 .NET 8。在 VS 里新建 WPF 项目,目标框架选 net6.0-windows 或 net8.0-windows 都行。命令行也可以创建:
dotnet new wpf -n BatchImageOcrRename需要安装的 NuGet 包有两个:腾讯云 OCR 的官方 SDK 包和一个 JSON 解析包。
dotnet add package TencentCloudSDK.Ocr dotnet add package Newtonsoft.JsonTencentCloudSDK.Ocr 这个包会依赖 TencentCloudSDK.Common,安装的时候会自动带上来,不需要手动处理。
这里提醒一下,SDK 包的版本号在持续更新,安装时建议使用最新稳定版。如果项目要发给别人运行,还需要确保目标机器装了对应版本的 .NET 桌面运行时,或者直接发布成自包含单文件,这样对方机器不需要额外装环境。
2.3 目录结构与核心类的职责
一个小工具不需要复杂的架构,但至少要让每个类只做一件事。我实际用的项目结构是这样:
BatchImageOcrRename/ ├── Models/ │ └── ImageItem.cs // 图片信息,包含路径、预览、状态、识别结果 ├── Services/ │ ├── TencentOcrService.cs // 腾讯云 OCR 接口封装 │ ├── ImageCropService.cs // 图片加载、裁剪、Base64 转换 │ └── RenameService.cs // 文件名清洗、重命名、冲突处理 ├── MainWindow.xaml // 主界面 ├── MainWindow.xaml.cs // 窗口逻辑和事件处理 └── App.xaml解耦的收益在真机调试时会体现得很明显。比如最开始 I 用的是 Tesseract,后来换腾讯云时,只需要改 TencentOcrService 的内部实现,界面和重命名逻辑完全不用动。
ImageItem 这个模型用来承载文件列表里每一行的状态,代码大致是这样:
public class ImageItem : INotifyPropertyChanged { public string FilePath { get; set; } public string OldName => Path.GetFileName(FilePath); private string _newName; public string NewName { get => _newName; set { _newName = value; OnPropertyChanged(); } } private string _status; public string Status { get => _status; set { _status = value; OnPropertyChanged(); } } public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged([CallerMemberName] string name = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); }3. 界面与交互:框选区域是核心体验
3.1 主窗口布局:三块区域怎么排
这个工具的界面不需要花哨,但布局要合理。我采用了三栏式结构,从上到下、从左到右依次是:
- 左侧:文件夹路径选择、加载按钮、图片列表,图片列表上显示文件名和识别状态;
- 右侧上半部分:图片预览区域,用户在这里用鼠标拖拽框选识别区域;
- 右侧下半部分:命名规则设置、识别结果预览、开始处理按钮和处理日志。
XAML 布局大致如下:
<Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="320"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <!-- 左侧文件列表 --> <DockPanel Grid.Column="0" Margin="10"> <StackPanel DockPanel.Dock="Top"> <TextBox x:Name="FolderPathTextBox" Margin="0,0,0,8"/> <Button x:Name="LoadButton" Content="加载图片" Click="LoadButton_Click" Margin="0,0,0,8"/> </StackPanel> <ListBox x:Name="ImageListBox" DisplayMemberPath="OldName"/> </DockPanel> <!-- 右侧预览和设置 --> <Grid Grid.Column="1" Margin="10"> <Grid.RowDefinitions> <RowDefinition Height="*"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <Border Grid.Row="0" BorderBrush="Gray" BorderThickness="1"> <Grid x:Name="PreviewHost"> <Image x:Name="PreviewImage" Stretch="Uniform"/> <Canvas x:Name="OverlayCanvas" Background="Transparent"/> </Grid> </Border> <StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,10,0,0"> <Button x:Name="StartButton" Content="开始识别并改名" Click="StartButton_Click" Width="140" Margin="0,0,10,0"/> <TextBlock x:Name="LogTextBlock" VerticalAlignment="Center"/> </StackPanel> </Grid> </Grid>需要注意的一个细节是预览图片容器。图片使用Stretch="Uniform"时可能留有空白边,这时候如果直接拿鼠标坐标对应图片像素坐标,位置会偏。必须换算。
3.2 实现鼠标框选并把坐标映射到原图
鼠标框选区域是整个工具交互里的核心功能。实现思路并不复杂:在图片上放一个透明 Canvas,监听鼠标按下、移动、松开三个事件,动态画一个矩形,最后把这个矩形从屏幕坐标换算成图片原始像素坐标。
先看矩形的绘制代码:
private Point _startPoint; private Rectangle _selectionRect; private void OverlayCanvas_MouseDown(object sender, MouseButtonEventArgs e) { _startPoint = e.GetPosition(OverlayCanvas); _selectionRect = new Rectangle { Stroke = Brushes.Red, StrokeThickness = 1.5, StrokeDashArray = new DoubleCollection { 4, 2 } }; Canvas.SetLeft(_selectionRect, _startPoint.X); Canvas.SetTop(_selectionRect, _startPoint.Y); OverlayCanvas.Children.Add(_selectionRect); } private void OverlayCanvas_MouseMove(object sender, MouseEventArgs e) { if (_selectionRect == null) return; var current = e.GetPosition(OverlayCanvas); double x = Math.Min(_startPoint.X, current.X); double y = Math.Min(_startPoint.Y, current.Y); double w = Math.Abs(_startPoint.X - current.X); double h = Math.Abs(_startPoint.Y - current.Y); _selectionRect.Width = w; _selectionRect.Height = h; Canvas.SetLeft(_selectionRect, x); Canvas.SetTop(_selectionRect, y); } private void OverlayCanvas_MouseUp(object sender, MouseButtonEventArgs e) { if (_selectionRect == null) return; // 把画布坐标换算成图片原始像素坐标 var pos = new Rect( Canvas.GetLeft(_selectionRect), Canvas.GetTop(_selectionRect), _selectionRect.Width, _selectionRect.Height ); double scaleX = PreviewImage.Source.Width / PreviewImage.ActualWidth; double scaleY = PreviewImage.Source.Height / PreviewImage.ActualHeight; _cropRect = new Rect( pos.X * scaleX, pos.Y * scaleY, pos.Width * scaleX, pos.Height * scaleY ); _selectionRect = null; Log($"已框选区域:X={_cropRect.X:F0}, Y={_cropRect.Y:F0}, W={_cropRect.Width:F0}, H={_cropRect.Height:F0}"); }这里的PreviewImage.Source.Width是图片原始像素宽,PreviewImage.ActualWidth是控件上实际显示的宽,两者相除就是缩放比例。如果图片被 Uniform 缩放后有留白,更精确的做法是把图片的 ActualWidth 替换成图片在容器中实际占用的显示宽度,这部分可以结合Arrange时的事件来算,但小型工具可以粗略处理,只要截图区域有一定冗余,OCR 依然能识别。
坐标换算这块是新人最容易踩的坑,很多人的第一版工具识别结果总是不对,就是因为这里没有做比例换算。框选的时候看着框住了文字,实际传给 OCR 的裁剪区域却是错误位置。
3.3 保存/复用识别区域坐标
一次性工具可以直接把区域坐标写死,但更实用的做法是让区域坐标可保存、可复用。因为批量处理同类型图片时,每张图片需要识别的区域位置基本固定,只有内容不同。
我建议把坐标直接序列化成 JSON,放在程序目录下:
public class RegionConfig { public double X { get; set; } public double Y { get; set; } public double Width { get; set; } public double Height { get; set; } }点击“保存区域”按钮,就把当前框选的区域写入 region.json;程序启动时如果发现这个文件,自动加载并绘制区域。实测下来,对于同一批次的扫描件,这个功能可以省掉重复框选的麻烦。
4. 核心逻辑:腾讯云 OCR 接入与批量重命名实现
4.1 官方 SDK 调用通用印刷体识别
腾讯云 OCR 的 .NET SDK 封装了签名鉴权过程,不需要自己写签名算法。接入步骤实际上是三步:创建客户端、构造请求、发起识别。
先看一个最简单的单张图片识别代码:
using TencentCloud.Common; using TencentCloud.Ocr.V20181119; using TencentCloud.Ocr.V20181119.Models; public class TencentOcrService { private readonly OcrClient _client; public TencentOcrService(string secretId, string secretKey, string region = "ap-guangzhou") { Credential credential = new Credential { SecretId = secretId, SecretKey = secretKey }; _client = new OcrClient(credential, region); } public async Task<string> RecognizeAsync(string imageBase64) { var request = new GeneralBasicOCRRequest(); request.ImageBase64 = imageBase64; var response = await _client.GeneralBasicOCR(request); if (response.TextDetections != null && response.TextDetections.Length > 0) { return string.Join(" ", response.TextDetections.Select(d => d.DetectedText)); } return string.Empty; } }GeneralBasicOCR是通用印刷体识别接口,适合识别印刷出来的文字,比如打印的合同、发票、屏幕截图里的文字。响应里的TextDetections是一个数组,每一项包含识别出的文本和位置信息。如果你框选的区域里有多个文本行,可以通过换行或者空格拼接,命名时再统一清洗。
证书部分有个细节需要注意:如果你在真实服务里使用,建议通过Environment变量或者配置文件读取密钥,不要直接写在代码里。我这个工具是自用,密钥写在 appsettings.json 里,并且加了忽略提交的处理。
4.2 裁剪图片区域并转 Base64
调用 OCR 之前需要把框选的区域从原始图片中裁剪出来。使用 System.Drawing 库就可以完成,这个库在 Windows 下做图片处理足够用。
private string CropToBase64(string imagePath, Rect cropRect) { using (var bitmap = new Bitmap(imagePath)) { // 防止区域越界 int x = Math.Max(0, (int)cropRect.X); int y = Math.Max(0, (int)cropRect.Y); int width = Math.Min(bitmap.Width - x, (int)cropRect.Width); int height = Math.Min(bitmap.Height - y, (int)cropRect.Height); width = Math.Max(1, width); height = Math.Max(1, height); using (var cropBitmap = bitmap.Clone(new Rectangle(x, y, width, height), bitmap.PixelFormat)) using (var ms = new MemoryStream()) { cropBitmap.Save(ms, ImageFormat.Jpeg); return Convert.ToBase64String(ms.ToArray()); } } }这里包含了几个实际操作中会踩到的坑:
- 坐标越界是必然会出现的情况,框选区域可能超出图片边界,裁剪前必须 clamp;
Clone之后得到的新 Bitmap 要记得 Dispose,否则文件句柄被占用,批量处理上百张图片后会出现“文件被占用”错误;- 保存成 MemoryStream 再转 Base64,是为了避免把临时文件写到磁盘。直接用文件路径也可以让 SDK 内部读取,但先用 Base64 可以更明确地控制图片格式和质量。
如果原始图片太大,比如扫描件有 10MB,直接转 Base64 会超过接口限制。腾讯云通用印刷体识别要求图片 Base64 后不超过 7MB,且图片尺寸不能太大。对于超大图片,可以先压缩再识别:
using (var resized = new Bitmap(cropBitmap, new Size(targetWidth, targetHeight))) { resized.Save(ms, ImageFormat.Jpeg); }压缩比例按原图宽高等比缩小,确保长边不超过 4000 像素,基本就不会触发接口限制。
4.3 识别文本清洗与文件名冲突处理
OCR 识别出来的文本不能直接用做文件名,需要先清洗。Windows 文件名不允许包含这些字符:
\ / : * ? " < > |此外,OCR 结果里可能混入空格、换行、特殊符号,还有全角半角混用的情况。我的清洗函数如下:
public static string SanitizeFileName(string raw) { if (string.IsNullOrWhiteSpace(raw)) return "未识别"; string invalidChars = new string(Path.GetInvalidFileNameChars()); string result = new string(raw .Where(c => !invalidChars.Contains(c) && c != '\r' && c != '\n') .ToArray()); // 去掉多余空格 result = result.Trim(); result = Regex.Replace(result, @"\s+", "_"); // 去掉文件名里容易引起混淆的前后点 result = result.Trim('.'); if (string.IsNullOrWhiteSpace(result)) return "未识别"; // 限制长度,NTFS 单文件名最大 255 字符 if (result.Length > 80) result = result.Substring(0, 80); return result; }长度限制我故意设置得比系统上限小很多,设为 80 个字符。原因很实际:文件名太长在资源管理器里根本看不全,完全失去命名的意义。如果你要保留更多信息,可以到 120 左右,再多我就不建议了。
重名冲突处理也很关键。批量处理时,两张图可能识别出一模一样的文字。如果直接用识别结果改名,后面的文件会覆盖前面的文件,这是毁灭性的。我采用追加序号的方式:
public static string GetUniqueFilePath(string dir, string fileName) { string baseName = Path.GetFileNameWithoutExtension(fileName); string ext = Path.GetExtension(fileName); string candidate = Path.Combine(dir, fileName); int index = 1; while (File.Exists(candidate)) { string suffix = $"_{index}"; candidate = Path.Combine(dir, $"{baseName}{suffix}{ext}"); index++; } return candidate; }这一步一定不能省。哪怕你确认这批图片识别结果不会重名,也建议保留这个逻辑,因为总会有意外。
4.4 批量处理的异步流程与并发控制
批量处理的核心逻辑是遍历文件列表,对每个文件依次执行:加载图片 → 裁剪 → Base64 → 调用 OCR → 清洗 → 重命名。
这里有两个性能要点:
第一,异步处理。调用云端 OCR 是网络 IO 操作,如果同步执行,UI 线程会被卡住,窗口会变成“未响应”。处理函数要写成 async,并配合 await 调用:
private async Task ProcessOneAsync(ImageItem item) { item.Status = "识别中..."; try { string base64 = CropToBase64(item.FilePath, _cropRect); string recognized = await _ocrService.RecognizeAsync(base64); string cleanName = RenameService.SanitizeFileName(recognized); item.NewName = cleanName + ".jpg"; string dir = Path.GetDirectoryName(item.FilePath); string target = RenameService.GetUniqueFilePath(dir, item.NewName); File.Move(item.FilePath, target); item.FilePath = target; item.Status = "完成"; } catch (Exception ex) { item.Status = "失败: " + ex.Message; } }第二,并发控制。批量上百张图片时,逐张串行虽然稳定但比较慢。可以引入 SemaphoreSlim 控制并发数,避免同时发起几十个请求导致接口限流或本机内存暴涨:
private static SemaphoreSlim _semaphore = new SemaphoreSlim(3); private async Task ProcessOneAsync(ImageItem item) { await _semaphore.WaitAsync(); try { // 同上 } finally { _semaphore.Release(); } }并发数控制在 3 到 5 之间比较合适。云端 OCR 接口有 QPS 限制,并发太高容易触发限流,并发太低又浪费网络带宽。实测 3 个并发处理 200 张图片,每张耗时约 1 秒,总耗时比串行快了一半以上。
批量循环里还需要一个“跳过”机制。如果处理到一半程序崩了,或者某个文件识别失败,重跑时应该可以跳过已经改名的图片。最简单的做法是看文件名:如果文件名不再是原始模式(比如不再是 IMG_ 开头),就跳过。更严谨的做法是记录处理日志,失败的文件集中展示,方便二次处理。
5. 常见问题与排查技巧实录
5.1 接口报错速查表
实际开发中总会遇到各种接口报错,这里整理了最常见的几个和排查思路。
| 错误信息 | 原因 | 解决办法 |
|---|---|---|
| AuthFailure.SignatureFailure | 密钥错误或签名失败 | 检查 SecretId 和 SecretKey 是否复制完整、是否有空格 |
| AuthFailure.SecretIdNotFound | SecretId 不存在 | 去访问管理控制台确认密钥状态 |
| FailedOperation.ImageDecodeFailed | 图片无法解码 | 确认 Base64 字符串前没有 data:image/jpg;base64, 前缀,且图片不是损坏文件 |
| FailedOperation.ImageTooLarge | 图片超过限制 | 压缩图片,或裁剪后控制尺寸 |
| ResourceNotFound | 服务未开通或地域错误 | 确认账号已开通对应 OCR 服务,检查 region 参数 |
| RequestLimitExceeded | 超出 QPS 限制 | 降低并发数,增加重试等待时间 |
有一个特别容易被坑的点:从浏览器里复制的图片或者接口文档示例里拿到的 Base64,字符串前面往往带着data:image/jpg;base64,这一大段前缀。直接把这个字符串塞给 OCR 接口,一定报解码失败。在转 Base64 的时候需要把这个前缀去掉。
if (imageBase64.Contains(",")) { imageBase64 = imageBase64.Substring(imageBase64.IndexOf(",") + 1); }5.2 识别不准的优化方向
如果 OCR 识别出来的文字经常出错,先别急着换服务,按下面几步排查,很多问题都能解决。
一,检查框选区域是否精准。区域范围太小,文字不完整,识别率会显著下降;区域范围太大,把旁边的无关文字也框进去了,识别结果会多出很多噪声。我的经验是框选时上下左右留出 5 到 10 像素的冗余,让文字整体处于区域中央。
二,注意图片方向。横版图片里的文字如果旋转了 90 度或者 180 度,识别率会大打折扣。腾讯云通用印刷体识别接口可以配合旋转校正参数,或者自己在程序里加一个旋转 90 度的按钮,手动纠偏后再识别。
三,图片太暗或者对比度低时,可以先做灰度化和提高对比度。System.Drawing 里处理灰度图比较繁琐,但可以用最简单的 ColorMatrix 实现。对于白底黑字的扫描件,灰度化之后识别率有明显提升。
四,针对同一批图片,可以保存多个识别区域,然后拼接结果。比如一张图片左上角是合同编号,右下角是日期,定义两个区域,分别识别后再用下划线拼接成最终文件名。这个扩展非常实用,我在第二版工具里加了这个功能。
5.3 工具实用性与扩展建议
这个工具虽然是围绕“图片改名”这个需求开发的,但把 OCR 识别出来的文本用于任何场景都成立。
比如扩展成“批量提取图片指定区域文字到 Excel”,只需要把重命名逻辑替换成写表格;也可以扩展成“识别快递单号并自动归类”,识别出单号后自动把文件移动到对应文件夹。核心的裁剪和 OCR 模块不用动,改的是下游的用途。
还有一个建议:正式批量处理前,先拿三五张图片试跑一遍,肉眼确认识别结果符合预期,再点“全部处理”。这个习惯能帮你避免成批文件被错误名字污染。错误命名的文件要再改回来,代价比多花一分钟试跑大得多。
我最早用 Tesseract 本地识别做了一版,后来换到腾讯云 OCR 后,最大的感受是识别精度上了一个台阶,而且省掉了大量图像预处理的琐碎工作。云 API 虽然要联网,但对于整理图片这种场景完全够用。如果你要处理的是涉密或者敏感文件,建议换 PaddleOCR 做全本地处理,代码结构上只需要替换 TencentOcrService 这一个类,其他模块完全不需要动。
最后再分享一个小技巧:识别结果清洗的时候,可以针对你的业务场景加一个“关键词白名单”或“关键词优先级”。比如你只关心文件名里的日期,那就在清洗时优先提取 20xx 开头的八位数字,而不是把所有文字都塞进文件名。这个小改动会让最终的文件名干净很多,也更好用。