先说结论
"把纸质单据 PDF 自动转成 Excel"这类需求,在 .NET 桌面端完全可以用纯离线方案做到够用级别,不需要云 API,也不需要训练模型。最近交付的一个真实项目:客户每周 6 小时的手工录入,压到了 20 分钟。本文把技术选型和关键代码整理出来,链路可以直接复用。
处理链路
javascript
PDF 文件夹 → 自动分类 → 文本版直接抽取 / 扫描件渲染 PNG 后 OCR
→ 字段规则抽取(JSON 模板)→ 结果复核表 → ClosedXML 导出 Excel
第一步:判断 PDF 是文本版还是扫描件
用 PdfPig(NuGet:UglyToad.PdfPig,Apache 2.0):
csharp
using UglyToad.PdfPig;
static (bool IsTextPdf, string Text) TryExtractText(string pdfPath)
{
using var doc = PdfDocument.Open(pdfPath);
var sb = new System.Text.StringBuilder();
foreach (var page in doc.GetPages())
sb.AppendLine(page.Text);
var text = sb.ToString();
int letterCount = text.Count(char.IsLetterOrDigit);
return (letterCount > 50, text); // 有效字符太少 = 扫描件
}
这个判断看着粗糙,实测很稳:扫描件里偶尔有水印字符,但有效字符数会远低于阈值。
第二步:扫描件渲染 + OCR
渲染用 PDFium(PdfiumViewer 封装 + 原生运行时包),300 DPI 输出 PNG:
csharp
using PdfiumViewer;
using var pdf = PdfDocument.Load(pdfPath);
for (int i = 0; i < pdf.PageCount; i++)
{
using var img = pdf.Render(i, 2000, 2800, 300, 300, forPrinting: false);
img.Save(Path.Combine(tempDir, $"page_{i}.png"),
System.Drawing.Imaging.ImageFormat.Png);
}
OCR 做了双引擎可切换:
引擎 A:Windows.Media.Ocr(系统内置,免费,支持中文)。坑点:
- TFM 要用
net8.0-windows10.0.19041.0以上才有 WinRT 投影; - 机器要装中文语言包;
- 输入图像有最大边长限制,用
OcrEngine.MaxImageDimension属性动态缩放,别硬编码。
csharp
using Windows.Globalization;
using Windows.Graphics.Imaging;
using Windows.Media.Ocr;
static async Task<string> OcrWithWindowsAsync(string imagePath)
{
var engine = OcrEngine.TryCreateFromLanguage(new Language("zh-Hans-CN"))
?? OcrEngine.TryCreateFromUserProfileLanguages()
?? throw new InvalidOperationException("本机无中文 OCR 引擎");
await using var fs = File.OpenRead(imagePath);
var decoder = await BitmapDecoder.CreateAsync(fs.AsRandomAccessStream());
var transform = new BitmapTransform();
double maxDim = engine.MaxImageDimension;
if (decoder.OrientedPixelWidth > maxDim || decoder.OrientedPixelHeight > maxDim)
{
double scale = maxDim / Math.Max(decoder.OrientedPixelWidth, decoder.OrientedPixelHeight);
transform.ScaledWidth = (uint)(decoder.OrientedPixelWidth * scale);
transform.ScaledHeight = (uint)(decoder.OrientedPixelHeight * scale);
}
var bmp = await decoder.GetSoftwareBitmapAsync(
BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied,
transform, ExifOrientationMode.IgnoreExifOrientation,
ColorManagementMode.DoNotColorManage);
var result = await engine.RecognizeAsync(bmp);
return string.Join(Environment.NewLine, result.Lines.Select(l => l.Text));
}
引擎 B:Tesseract 5 (NuGet:Tesseract),离线打包 chi_sim.traineddata:
csharp
using Tesseract;
static string OcrWithTesseract(string imagePath)
{
using var engine = new TesseractEngine(@".\tessdata", "chi_sim", EngineMode.Default);
using var pix = Pix.LoadFromFile(imagePath);
using var page = engine.Process(pix);
return page.GetText();
}
选型判断:环境可控用 Windows OCR 零成本起步;机器环境杂、或某些版式效果差,Tesseract 兜底。单据里有金额和往来单位,客户普遍不接受数据出内网------这也是不上云 OCR 的核心理由,不只是省钱。
第三步:并发与 UI
OCR 是 CPU 密集操作,Parallel.ForEachAsync + SemaphoreSlim 限流,进度用 IProgress<T> 回 UI 线程:
csharp
var semaphore = new SemaphoreSlim(Math.Max(2, Environment.ProcessorCount / 2));
var progress = new Progress<string>(msg => StatusText.Text = msg);
await Parallel.ForEachAsync(pdfFiles, ct, async (file, token) =>
{
await semaphore.WaitAsync(token);
try
{
var (isText, text) = TryExtractText(file);
if (!isText) text = await RenderAndOcrAsync(file, token);
lock (_results) _results.Add(ExtractFields(file, text));
((IProgress<string>)progress).Report($"完成:{Path.GetFileName(file)}");
}
finally { semaphore.Release(); }
});
第四步:字段抽取用规则模板,别碰模型
金额、日期、单号的抽取:每家厂商一份 JSON 模板(正则 + 锚点 + 校验),新厂商加模板即可。比训练模型便宜两个数量级,而且"配模板"本身就是可以持续收费的服务动作。
导出用 ClosedXML(MIT,避开 EPPlus 的商用授权配置):
csharp
using ClosedXML.Excel;
using var wb = new XLWorkbook();
var wsx = wb.Worksheets.Add("汇总");
wsx.Cell(1, 1).InsertTable(_records);
wsx.Columns().AdjustToContents();
wb.SaveAs(outPath);
工程边界(诚实版)
- 清晰印刷体:够用;手写单、倾斜、低分辨率翻拍:两个引擎都翻车,必须留人工复核通道;
- 关键字段必须有格式校验(金额小数点、单号正则),可疑行标黄让人确认。
延伸
同一套字段抽取逻辑放 .NET WebAPI 后端,就能接微信小程序(拍照上传识别)或 MAUI APP(现场扫描)。桌面端、小程序、APP 三端的定制开发我都在做,这类"重复录入自动化"的活儿尤其多------觉得有用的话点个关注,后面会写这套工具的授权机制设计(年费模式怎么落地)。