从 Veryfi 迁移到 IronOCR
本指南将引导.NET开发人员使用本地 OCR 库IronOCR替换 非常 的云文档处理 API。 它涵盖了包交换、命名空间清理,以及四个完整的代码迁移示例,重点关注围绕 非常 构建的最常见模式:客户端初始化、基于区域的字段提取、使用结构化数据进行费用分类以及 webhook 替换。 无需事先阅读对比文章。
为什么要从 非常 迁移?
财务文件在 非常 的管道中单向流动:从您的基础设施流向他们的基础设施。 这一建筑结构上的因素驱动了大多数人口迁移。 以下是促使团队做出改变的具体痛点。
每次文件调用都会将敏感的财务数据传输到第三方服务器。收据包含银行卡后四位数字和供应商信息。 发票上会包含银行账号、路由号码和供应商税务识别号。 银行对账单包含完整的交易记录。 借助Veryfi,每个api.veryfi.com,在Veryfi的基础设施上处理,并返回JSON。 一旦发送 HTTP 请求,您对该数据的控制权就结束了。
需要四个凭证,并且必须在每个环境中保持同步。apiKey——四个独立的秘密,需存储在配置中,按计划轮换,注入CI/CD管道,并进行暴露审计。 单个凭证泄露会导致整个应用程序中处理的每个文档的身份验证失效。 IronOCR需要一个许可证密钥字符串。
按单件计费,费用无上限。收据每张约 0.05 至 0.15 美元,发票每张 0.10 至 0.25 美元,银行对账单每张 0.15 至 0.30 美元。每月 5 万份单件,按计量计费,每月费用为 5,000 至 15,000 美元,第二年和第三年费用不变。 IronOCRProfessional许可证售价 2,999 美元,可永久无限量处理文档——与每月 5,000 美元的 非常 支出相比,不到三周即可收回成本。
API是异步的,因为底层工作是远程的。ProcessDocumentAsync不是异步的,因为处理需要较长的计算时间; 它是异步的,因为文档必须传输到服务器,排在其他请求后面,完成推理,并通过网络返回响应。 延迟是不确定的。 HTTP 429 速率限制需要重试逻辑。 HTTP 402 支付失败会导致批量处理完全停止。 非常 基础设施上的 HTTP 500 错误会导致您的工作流程中断。
Veryfi 的文档范围仅限于费用凭证。训练后的模型能够可靠地返回收据、发票、支票、银行对账单、W-2 表格和名片的结构化字段。 除了上述列表之外——一般商业文件、合同、医疗记录、运输单据、定制内部表格——结果会降低或需要付费的定制模型培训。 采用 非常 进行费用自动化的组织通常会在 6-12 个月内发现其他团队需要对 非常 无法处理的文档进行 OCR 识别。
Veryfi的专有JSON架构将所有提取逻辑与单个供应商绑定。读取response.LineItems的每行代码仅适用于Veryfi。 更换供应商(或切换到本地 OCR)意味着从头开始重写所有提取逻辑。
基本问题
// Veryfi: financial data leaves your infrastructure on every call
var client = new VeryfiClient(clientId, clientSecret, username, apiKey); // 4 secrets
var bytes = File.ReadAllBytes("invoice-with-routing-number.pdf");
var response = await client.ProcessDocumentAsync(bytes); // bank details transmitted
var routingNumber = response.BankAccount?.RoutingNumber; // arrived via 非常 cloud' Veryfi: financial data leaves your infrastructure on every call
Dim client As New VeryfiClient(clientId, clientSecret, username, apiKey) ' 4 secrets
Dim bytes As Byte() = File.ReadAllBytes("invoice-with-routing-number.pdf")
Dim response = Await client.ProcessDocumentAsync(bytes) ' bank details transmitted
Dim routingNumber = response.BankAccount?.RoutingNumber ' arrived via 非常 cloud// IronOCR: routing numbers never leave your server
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY"; // 1 key
var ocr = new IronTesseract();
using var input = new OcrInput();
input.LoadPdf("invoice-with-routing-number.pdf"); // processed locally
var result = ocr.Read(input);
var routingNumber = Regex.Match(result.Text, @"Routing\s*#?\s*:?\s*(\d{9})").Groups[1].Value;IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY" ' 1 key
Dim ocr = New IronTesseract()
Using input = New OcrInput()
input.LoadPdf("invoice-with-routing-number.pdf") ' processed locally
Dim result = ocr.Read(input)
Dim routingNumber = Regex.Match(result.Text, "Routing\s*#?\s*:?\s*(\d{9})").Groups(1).Value
End UsingIronOCR与 Veryfi:功能对比
下表列出了两款产品的功能,以支持技术评估。
| 特征 | 非常 | IronOCR |
|---|---|---|
| 加工地点 | 非常 云服务器 | 您的基础设施 |
| 部署模型 | 仅限云 API | 本地部署、Docker、Azure、AWS、Linux |
| 离线支持 | 否 | 是 |
| 需要互联网连接 | 是的(每份文件) | 否 |
| 数据离开基础设施 | 是的(每次通话) | 绝不 |
| 符合 HIPAA 标准,无需 BAA | 否 | 是 |
| 气隙环境支持 | 不可能 | 完全支持 |
| 定价模式 | 每份文件(0.05美元至0.30美元) | 永久许可证($999–$2,399) |
| 所需资质 | 4(客户端 ID、客户端密钥、用户名、API 密钥) | 1 个许可证密钥 |
| 同步 API | 否(仅限异步) | 是 |
| 限速 | 是的(HTTP 429) | None |
| 文件范围 | 收据、发票、支票、银行对账单、W-2表格、名片 | 任何文档类型 |
| 自定义文档类型 | 需要付费模特培训 | 通过正则表达式/模式提取实现任何布局 |
| PDF 输入 | 是的(字节上传) | 是的(本地的) |
| 可搜索的 PDF 输出 | 否 | 是(result.SaveAsSearchablePdf()) |
| 基于区域的OCR | 否 | 是(CropRectangle) |
| 条形码读取 | 否 | 是的(相同的OCR识别结果) |
| 结构化结果访问 | 预解析的 JSON 字段 | 页、段落、行、带坐标的单词 |
| 置信度评分 | 按字段(专有) | 逐字和总体(result.Confidence) |
| 支持 125 种以上的语言 | 有限的 | 是的(NuGet语言包) |
| 线程安全的并行处理 | HTTP 并发限制适用 | 完整(每线程一个IronTesseract) |
| 不使用模拟对象的单元测试 | 需要 HTTP 模拟 | 直接本地测试 |
快速入门:Veryfi 到IronOCR 的迁移
步骤 1:替换 NuGet 软件包
移除 非常 SDK:
dotnet remove package Veryfi
从NuGet安装IronOCR :
步骤 2:更新命名空间
将 非常 命名空间替换为IronOCR命名空间:
// Before (Veryfi)
using Veryfi;
using Veryfi.Models;
// After (IronOCR)
using IronOcr;
using System.Text.RegularExpressions;' Before (Veryfi)
Imports Veryfi
Imports Veryfi.Models
' After (IronOCR)
Imports IronOcr
Imports System.Text.RegularExpressions步骤 3:初始化许可证
在应用程序启动时,在任何 OCR 调用之前,添加此行代码:
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY"代码迁移示例
文档处理客户端替换
Veryfi服务围绕VeryfiClient的构造函数注入构建。 四凭证构造器是依赖注入的天然缝隙,但它会创建四个必须管理和轮换的密钥。 用IronOCR替换它,可以将凭据合并为一个许可证密钥,并将处理引擎的实例化移到服务类本身中。
Veryfi 方法:
using Veryfi;
using Microsoft.Extensions.Configuration;
public class ExpenseDocumentService
{
private readonly VeryfiClient _client;
// Four credentials injected — four secrets to manage, store, rotate
public ExpenseDocumentService(IConfiguration config)
{
_client = new VeryfiClient(
config["Veryfi:ClientId"], // secret 1
config["Veryfi:ClientSecret"], // secret 2
config["Veryfi:Username"], // secret 3
config["Veryfi:ApiKey"] // secret 4
);
}
public async Task<string> GetVendorNameAsync(string documentPath)
{
var bytes = File.ReadAllBytes(documentPath);
// Document uploaded to 非常 on this call
var response = await _client.ProcessDocumentAsync(bytes);
return response.Vendor?.Name;
}
public async Task<decimal?> GetTotalAsync(string documentPath)
{
var bytes = File.ReadAllBytes(documentPath);
var response = await _client.ProcessDocumentAsync(bytes);
return response.Total;
}
}Imports Veryfi
Imports Microsoft.Extensions.Configuration
Public Class ExpenseDocumentService
Private ReadOnly _client As VeryfiClient
' Four credentials injected — four secrets to manage, store, rotate
Public Sub New(config As IConfiguration)
_client = New VeryfiClient(config("Veryfi:ClientId"), ' secret 1
config("Veryfi:ClientSecret"), ' secret 2
config("Veryfi:Username"), ' secret 3
config("Veryfi:ApiKey")) ' secret 4
End Sub
Public Async Function GetVendorNameAsync(documentPath As String) As Task(Of String)
Dim bytes = File.ReadAllBytes(documentPath)
' Document uploaded to 非常 on this call
Dim response = Await _client.ProcessDocumentAsync(bytes)
Return response.Vendor?.Name
End Function
Public Async Function GetTotalAsync(documentPath As String) As Task(Of Decimal?)
Dim bytes = File.ReadAllBytes(documentPath)
Dim response = Await _client.ProcessDocumentAsync(bytes)
Return response.Total
End Function
End ClassIronOCR方法:
using IronOcr;
using System.Text.RegularExpressions;
public class ExpenseDocumentService
{
private readonly IronTesseract _ocr;
// One license key — set once at startup, not per-instance
public ExpenseDocumentService()
{
_ocr = new IronTesseract();
}
public string GetVendorName(string documentPath)
{
// All processing local — document bytes never leave this server
var result = _ocr.Read(documentPath);
// Vendor is typically the first non-whitespace line on a receipt
return result.Pages[0].Paragraphs
.OrderBy(p => p.Y)
.Select(p => p.Text.Trim())
.FirstOrDefault(t => t.Length > 3);
}
public decimal? GetTotal(string documentPath)
{
var result = _ocr.Read(documentPath);
var match = Regex.Match(result.Text,
@"(?:Total|Grand Total|Amount Due):?\s*\$?\s*([\d,]+\.\d{2})",
RegexOptions.IgnoreCase);
return match.Success
? decimal.Parse(match.Groups[1].Value.Replace(",", ""))
: (decimal?)null;
}
}Imports IronOcr
Imports System.Text.RegularExpressions
Public Class ExpenseDocumentService
Private ReadOnly _ocr As IronTesseract
' One license key — set once at startup, not per-instance
Public Sub New()
_ocr = New IronTesseract()
End Sub
Public Function GetVendorName(documentPath As String) As String
' All processing local — document bytes never leave this server
Dim result = _ocr.Read(documentPath)
' Vendor is typically the first non-whitespace line on a receipt
Return result.Pages(0).Paragraphs _
.OrderBy(Function(p) p.Y) _
.Select(Function(p) p.Text.Trim()) _
.FirstOrDefault(Function(t) t.Length > 3)
End Function
Public Function GetTotal(documentPath As String) As Decimal?
Dim result = _ocr.Read(documentPath)
Dim match = Regex.Match(result.Text, "(?:Total|Grand Total|Amount Due):?\s*\$?\s*([\d,]+\.\d{2})", RegexOptions.IgnoreCase)
Return If(match.Success, Decimal.Parse(match.Groups(1).Value.Replace(",", "")), CType(Nothing, Decimal?))
End Function
End Class构造函数的改变消除了每个环境中四个配置项:appsettings.json,Docker机密,Azure密钥保管库引用和CI/CD管道变量。 IronTesseract实例可在同一个线程上的多次调用中重用。 有关ASP.NET Core依赖注入容器中的单例注册模式,请参阅IronTesseract 设置指南。
基于区域的OCR收据字段提取
Veryfi 通过在整个文档图像上运行其训练好的 ML 模型来提取收据字段,并返回预先构建的 JSON 响应。 IronOCR的等效功能是使用CropRectangle的区域OCR,针对收据图像的特定区域(供应商的头部区域,总计的底部区域),而不是运行全页扫描并在输出中搜索模式。 对于已知布局,这种方法速度更快;对于感兴趣区域定义明确的情况,这种方法精度更高。
Veryfi 方法:
using Veryfi;
public class ReceiptFieldExtractor
{
private readonly VeryfiClient _client;
public ReceiptFieldExtractor(VeryfiClient client)
{
_client = client;
}
public async Task<(string Vendor, decimal? Total, decimal? Tax)>
ExtractReceiptFieldsAsync(string imagePath)
{
var bytes = File.ReadAllBytes(imagePath);
// Full document uploaded — Veryfi's ML returns structured fields
var response = await _client.ProcessDocumentAsync(bytes);
return (
Vendor: response.Vendor?.Name,
Total: response.Total,
Tax: response.Tax
);
}
}Imports Veryfi
Public Class ReceiptFieldExtractor
Private ReadOnly _client As VeryfiClient
Public Sub New(client As VeryfiClient)
_client = client
End Sub
Public Async Function ExtractReceiptFieldsAsync(imagePath As String) As Task(Of (Vendor As String, Total As Decimal?, Tax As Decimal?))
Dim bytes = File.ReadAllBytes(imagePath)
' Full document uploaded — Veryfi's ML returns structured fields
Dim response = Await _client.ProcessDocumentAsync(bytes)
Return (Vendor:=response.Vendor?.Name,
Total:=response.Total,
Tax:=response.Tax)
End Function
End ClassIronOCR方法:
using IronOcr;
using System.Text.RegularExpressions;
public class ReceiptFieldExtractor
{
private readonly IronTesseract _ocr = new IronTesseract();
public (string Vendor, decimal? Total, decimal? Tax)
ExtractReceiptFields(string imagePath)
{
// Region 1: Header zone — vendor name typically in top 15% of receipt
var headerRegion = new CropRectangle(0, 0, 800, 150);
using var headerInput = new OcrInput();
headerInput.LoadImage(imagePath, headerRegion);
headerInput.Deskew();
var headerResult = _ocr.Read(headerInput);
// Region 2: Footer zone — totals typically in bottom 20% of receipt
var footerRegion = new CropRectangle(0, 650, 800, 200);
using var footerInput = new OcrInput();
footerInput.LoadImage(imagePath, footerRegion);
footerInput.DeNoise();
var footerResult = _ocr.Read(footerInput);
var vendor = headerResult.Pages[0].Paragraphs
.OrderBy(p => p.Y)
.Select(p => p.Text.Trim())
.FirstOrDefault(t => t.Length > 3);
var footerText = footerResult.Text;
var totalMatch = Regex.Match(footerText,
@"(?:Total|Grand Total):?\s*\$?\s*([\d,]+\.\d{2})",
RegexOptions.IgnoreCase);
var taxMatch = Regex.Match(footerText,
@"(?:Tax|Sales Tax|VAT):?\s*\$?\s*([\d,]+\.\d{2})",
RegexOptions.IgnoreCase);
return (
Vendor: vendor,
Total: totalMatch.Success
? decimal.Parse(totalMatch.Groups[1].Value.Replace(",", ""))
: (decimal?)null,
Tax: taxMatch.Success
? decimal.Parse(taxMatch.Groups[1].Value.Replace(",", ""))
: (decimal?)null
);
}
}Imports IronOcr
Imports System.Text.RegularExpressions
Public Class ReceiptFieldExtractor
Private ReadOnly _ocr As New IronTesseract()
Public Function ExtractReceiptFields(imagePath As String) As (Vendor As String, Total As Decimal?, Tax As Decimal?)
' Region 1: Header zone — vendor name typically in top 15% of receipt
Dim headerRegion = New CropRectangle(0, 0, 800, 150)
Using headerInput = New OcrInput()
headerInput.LoadImage(imagePath, headerRegion)
headerInput.Deskew()
Dim headerResult = _ocr.Read(headerInput)
' Region 2: Footer zone — totals typically in bottom 20% of receipt
Dim footerRegion = New CropRectangle(0, 650, 800, 200)
Using footerInput = New OcrInput()
footerInput.LoadImage(imagePath, footerRegion)
footerInput.DeNoise()
Dim footerResult = _ocr.Read(footerInput)
Dim vendor = headerResult.Pages(0).Paragraphs _
.OrderBy(Function(p) p.Y) _
.Select(Function(p) p.Text.Trim()) _
.FirstOrDefault(Function(t) t.Length > 3)
Dim footerText = footerResult.Text
Dim totalMatch = Regex.Match(footerText,
"(?:Total|Grand Total):?\s*\$?\s*([\d,]+\.\d{2})",
RegexOptions.IgnoreCase)
Dim taxMatch = Regex.Match(footerText,
"(?:Tax|Sales Tax|VAT):?\s*\$?\s*([\d,]+\.\d{2})",
RegexOptions.IgnoreCase)
Return (
Vendor := vendor,
Total := If(totalMatch.Success, Decimal.Parse(totalMatch.Groups(1).Value.Replace(",", "")), CType(Nothing, Decimal?)),
Tax := If(taxMatch.Success, Decimal.Parse(taxMatch.Groups(1).Value.Replace(",", "")), CType(Nothing, Decimal?))
)
End Using
End Using
End Function
End Class(x, y, width, height)。 仅处理页眉和页脚区域比读取整个页面速度更快,并且可以避免收据正文中的行项目金额出现错误匹配。 基于区域的 OCR 指南涵盖了可变大小文档的坐标测量策略, 区域裁剪示例展示了完整的模式。
使用结构化段落数据进行费用分类
Veryfi返回Total已被解析。 IronOCR通过result.Lines提供了等效功能,它们会公开每个文本块及其X/Y坐标。 费用分类逻辑(决定某项费用是餐费、差旅费、用品费还是软件费)对同一文本都适用。 不同之处在于,使用IronOCR,您可以拥有、调整和扩展分类逻辑,而无需付费的机器学习重新训练周期。
Veryfi 方法:
using Veryfi;
public class ExpenseCategorizer
{
private readonly VeryfiClient _client;
public ExpenseCategorizer(VeryfiClient client)
{
_client = client;
}
public async Task<Dictionary<string, decimal>> CategorizeExpensesAsync(string receiptPath)
{
var bytes = File.ReadAllBytes(receiptPath);
var response = await _client.ProcessDocumentAsync(bytes);
var categories = new Dictionary<string, decimal>();
// Line items arrive pre-parsed from Veryfi's ML pipeline
foreach (var item in response.LineItems ?? Enumerable.Empty<dynamic>())
{
var category = response.Category ?? "Uncategorized";
var amount = (decimal)(item.Total ?? 0m);
if (!categories.ContainsKey(category))
categories[category] = 0m;
categories[category] += amount;
}
return categories;
}
}Imports Veryfi
Public Class ExpenseCategorizer
Private ReadOnly _client As VeryfiClient
Public Sub New(client As VeryfiClient)
_client = client
End Sub
Public Async Function CategorizeExpensesAsync(receiptPath As String) As Task(Of Dictionary(Of String, Decimal))
Dim bytes = File.ReadAllBytes(receiptPath)
Dim response = Await _client.ProcessDocumentAsync(bytes)
Dim categories = New Dictionary(Of String, Decimal)()
' Line items arrive pre-parsed from Veryfi's ML pipeline
For Each item In If(response.LineItems, Enumerable.Empty(Of Dynamic)())
Dim category = If(response.Category, "Uncategorized")
Dim amount = CType(If(item.Total, 0D), Decimal)
If Not categories.ContainsKey(category) Then
categories(category) = 0D
End If
categories(category) += amount
Next
Return categories
End Function
End ClassIronOCR方法:
using IronOcr;
using System.Text.RegularExpressions;
public class ExpenseCategorizer
{
private readonly IronTesseract _ocr = new IronTesseract();
// Keyword-based categorization — tune these for your expense policy
private static readonly Dictionary<string, string[]> CategoryKeywords = new()
{
["Meals & Entertainment"] = new[] { "restaurant", "cafe", "coffee", "lunch", "dinner", "food", "bar" },
["Travel"] = new[] { "airline", "hotel", "uber", "lyft", "taxi", "parking", "gas", "fuel" },
["Office Supplies"] = new[] { "staples", "office depot", "paper", "ink", "toner", "supplies" },
["Software & Subscriptions"] = new[] { "adobe", "microsoft", "github", "aws", "azure", "slack" }
};
public Dictionary<string, decimal> CategorizeExpenses(string receiptPath)
{
var result = _ocr.Read(receiptPath);
// Use paragraph coordinates to isolate line items
// Line items typically appear in the middle vertical band of the receipt
var lineItemParagraphs = result.Pages[0].Paragraphs
.Where(p => p.Y > 150 && p.Y < 650) // skip header/footer regions
.OrderBy(p => p.Y)
.ToList();
var categories = new Dictionary<string, decimal>();
var pricePattern = new Regex(@"\$?([\d,]+\.\d{2})$");
var vendorText = result.Text.ToLower();
// Determine top-level category from vendor name
var topCategory = "Uncategorized";
foreach (var (cat, keywords) in CategoryKeywords)
{
if (keywords.Any(kw => vendorText.Contains(kw)))
{
topCategory = cat;
break;
}
}
// Extract individual line item amounts
foreach (var para in lineItemParagraphs)
{
var priceMatch = pricePattern.Match(para.Text.Trim());
if (!priceMatch.Success)
continue;
if (!decimal.TryParse(priceMatch.Groups[1].Value.Replace(",", ""), out var amount))
continue;
// Classify individual items where keywords appear in the description
var itemCategory = topCategory;
var descriptionText = para.Text.ToLower();
foreach (var (cat, keywords) in CategoryKeywords)
{
if (keywords.Any(kw => descriptionText.Contains(kw)))
{
itemCategory = cat;
break;
}
}
if (!categories.ContainsKey(itemCategory))
categories[itemCategory] = 0m;
categories[itemCategory] += amount;
}
return categories;
}
}Imports IronOcr
Imports System.Text.RegularExpressions
Public Class ExpenseCategorizer
Private ReadOnly _ocr As New IronTesseract()
' Keyword-based categorization — tune these for your expense policy
Private Shared ReadOnly CategoryKeywords As New Dictionary(Of String, String()) From {
{"Meals & Entertainment", New String() {"restaurant", "cafe", "coffee", "lunch", "dinner", "food", "bar"}},
{"Travel", New String() {"airline", "hotel", "uber", "lyft", "taxi", "parking", "gas", "fuel"}},
{"Office Supplies", New String() {"staples", "office depot", "paper", "ink", "toner", "supplies"}},
{"Software & Subscriptions", New String() {"adobe", "microsoft", "github", "aws", "azure", "slack"}}
}
Public Function CategorizeExpenses(receiptPath As String) As Dictionary(Of String, Decimal)
Dim result = _ocr.Read(receiptPath)
' Use paragraph coordinates to isolate line items
' Line items typically appear in the middle vertical band of the receipt
Dim lineItemParagraphs = result.Pages(0).Paragraphs _
.Where(Function(p) p.Y > 150 AndAlso p.Y < 650) ' skip header/footer regions
.OrderBy(Function(p) p.Y).ToList()
Dim categories = New Dictionary(Of String, Decimal)()
Dim pricePattern = New Regex("\$?([\d,]+\.\d{2})$")
Dim vendorText = result.Text.ToLower()
' Determine top-level category from vendor name
Dim topCategory = "Uncategorized"
For Each kvp In CategoryKeywords
Dim cat = kvp.Key
Dim keywords = kvp.Value
If keywords.Any(Function(kw) vendorText.Contains(kw)) Then
topCategory = cat
Exit For
End If
Next
' Extract individual line item amounts
For Each para In lineItemParagraphs
Dim priceMatch = pricePattern.Match(para.Text.Trim())
If Not priceMatch.Success Then
Continue For
End If
Dim amount As Decimal
If Not Decimal.TryParse(priceMatch.Groups(1).Value.Replace(",", ""), amount) Then
Continue For
End If
' Classify individual items where keywords appear in the description
Dim itemCategory = topCategory
Dim descriptionText = para.Text.ToLower()
For Each kvp In CategoryKeywords
Dim cat = kvp.Key
Dim keywords = kvp.Value
If keywords.Any(Function(kw) descriptionText.Contains(kw)) Then
itemCategory = cat
Exit For
End If
Next
If Not categories.ContainsKey(itemCategory) Then
categories(itemCategory) = 0D
End If
categories(itemCategory) += amount
Next
Return categories
End Function
End ClassY坐标,使其能够轻松隔离标准收据布局上行项目出现的垂直区域。 结构化数据访问指南解释了Characters的完整层次结构及其坐标属性。 对于扫描质量较差的收据(例如纸张皱巴巴的、低对比度的热敏打印),图像质量校正指南涵盖了在分类逻辑运行之前提高准确性的预处理过滤器。
Webhook 消除和同步批量替换
在文档量较大的情况下,Veryfi 建议使用基于 webhook 的通知而不是轮询。 该模式需要一个可公开访问的 HTTPS 端点、一个用于签名验证的 webhook 密钥、一个用于保存结果直到 webhook 触发的队列,以及用于处理未成功投递的重试逻辑。 对于云 OCR 相对于本地处理速度较慢这一问题而言,这最终只是一种权宜之计,而这套基础设施意义重大。 IronOCR工艺同步进行。 不存在需要通过 webhook 来弥合的异步差距。
Veryfi 方法:
using Veryfi;
using Microsoft.AspNetCore.Mvc;
// 非常 webhook receiver — required for high-volume reliable processing
[ApiController]
[Route("webhooks")]
public class VeryfiWebhookController : ControllerBase
{
private readonly IDocumentResultQueue _queue;
public VeryfiWebhookController(IDocumentResultQueue queue)
{
_queue = queue;
}
[HttpPost("veryfi")]
public IActionResult ReceiveWebhook([FromBody] VeryfiWebhookPayload payload,
[FromHeader(Name = "X-Veryfi-Token")] string token)
{
// Validate webhook signature — prevents spoofed payloads
if (!IsValidSignature(token, payload))
return Unauthorized();
// Enqueue result for async downstream consumption
_queue.Enqueue(new DocumentResult
{
DocumentId = payload.Id,
Vendor = payload.Data?.Vendor?.Name,
Total = payload.Data?.Total
});
return Ok();
}
private bool IsValidSignature(string token, VeryfiWebhookPayload payload) =>
// HMAC validation against webhook secret — infrastructure requirement
token == ComputeHmac(payload, Environment.GetEnvironmentVariable("VERYFI_WEBHOOK_SECRET"));
}
// Document batch submission — fire and forget, results arrive via webhook
public class VeryfiDocumentBatchSubmitter
{
private readonly VeryfiClient _client;
public async Task SubmitBatchAsync(string[] documentPaths)
{
foreach (var path in documentPaths)
{
var bytes = File.ReadAllBytes(path);
// Submit — result arrives asynchronously via webhook, not here
await _client.ProcessDocumentAsync(bytes);
}
}
}Imports Veryfi
Imports Microsoft.AspNetCore.Mvc
' 非常 webhook receiver — required for high-volume reliable processing
<ApiController>
<Route("webhooks")>
Public Class VeryfiWebhookController
Inherits ControllerBase
Private ReadOnly _queue As IDocumentResultQueue
Public Sub New(queue As IDocumentResultQueue)
_queue = queue
End Sub
<HttpPost("veryfi")>
Public Function ReceiveWebhook(<FromBody> payload As VeryfiWebhookPayload,
<FromHeader(Name:="X-Veryfi-Token")> token As String) As IActionResult
' Validate webhook signature — prevents spoofed payloads
If Not IsValidSignature(token, payload) Then
Return Unauthorized()
End If
' Enqueue result for async downstream consumption
_queue.Enqueue(New DocumentResult With {
.DocumentId = payload.Id,
.Vendor = payload.Data?.Vendor?.Name,
.Total = payload.Data?.Total
})
Return Ok()
End Function
Private Function IsValidSignature(token As String, payload As VeryfiWebhookPayload) As Boolean
' HMAC validation against webhook secret — infrastructure requirement
Return token = ComputeHmac(payload, Environment.GetEnvironmentVariable("VERYFI_WEBHOOK_SECRET"))
End Function
End Class
' Document batch submission — fire and forget, results arrive via webhook
Public Class VeryfiDocumentBatchSubmitter
Private ReadOnly _client As VeryfiClient
Public Async Function SubmitBatchAsync(documentPaths As String()) As Task
For Each path In documentPaths
Dim bytes = File.ReadAllBytes(path)
' Submit — result arrives asynchronously via webhook, not here
Await _client.ProcessDocumentAsync(bytes)
Next
End Function
End ClassIronOCR方法:
using IronOcr;
using System.Text.RegularExpressions;
using System.Collections.Concurrent;
// 否 webhook controller needed — results are synchronous and local
public class DocumentBatchProcessor
{
// IronTesseract is thread-safe when one instance is created per thread
public List<DocumentResult> ProcessBatch(string[] documentPaths)
{
var results = new ConcurrentBag<DocumentResult>();
Parallel.ForEach(documentPaths, documentPath =>
{
// One IronTesseract per thread — thread-safe pattern
var ocr = new IronTesseract();
var result = ocr.Read(documentPath);
results.Add(new DocumentResult
{
FilePath = documentPath,
Vendor = ExtractVendor(result),
Total = ExtractTotal(result.Text),
Confidence = result.Confidence,
// Result is available immediately — no queue, no webhook
ProcessedAt = DateTime.UtcNow
});
});
return results.OrderBy(r => r.FilePath).ToList();
}
private string ExtractVendor(OcrResult result)
{
// Vendor: first substantive paragraph ordered by vertical position
return result.Pages[0].Paragraphs
.OrderBy(p => p.Y)
.Select(p => p.Text.Trim())
.FirstOrDefault(t => !string.IsNullOrWhiteSpace(t) && t.Length > 3);
}
private decimal? ExtractTotal(string text)
{
var match = Regex.Match(text,
@"(?:Total|Grand Total|Amount Due):?\s*\$?\s*([\d,]+\.\d{2})",
RegexOptions.IgnoreCase);
return match.Success
? decimal.Parse(match.Groups[1].Value.Replace(",", ""))
: (decimal?)null;
}
}
public class DocumentResult
{
public string FilePath { get; set; }
public string Vendor { get; set; }
public decimal? Total { get; set; }
public double Confidence { get; set; }
public DateTime ProcessedAt { get; set; }
}Imports IronOcr
Imports System.Text.RegularExpressions
Imports System.Collections.Concurrent
' 否 webhook controller needed — results are synchronous and local
Public Class DocumentBatchProcessor
' IronTesseract is thread-safe when one instance is created per thread
Public Function ProcessBatch(documentPaths As String()) As List(Of DocumentResult)
Dim results = New ConcurrentBag(Of DocumentResult)()
Parallel.ForEach(documentPaths, Sub(documentPath)
' One IronTesseract per thread — thread-safe pattern
Dim ocr = New IronTesseract()
Dim result = ocr.Read(documentPath)
results.Add(New DocumentResult With {
.FilePath = documentPath,
.Vendor = ExtractVendor(result),
.Total = ExtractTotal(result.Text),
.Confidence = result.Confidence,
' Result is available immediately — no queue, no webhook
.ProcessedAt = DateTime.UtcNow
})
End Sub)
Return results.OrderBy(Function(r) r.FilePath).ToList()
End Function
Private Function ExtractVendor(result As OcrResult) As String
' Vendor: first substantive paragraph ordered by vertical position
Return result.Pages(0).Paragraphs _
.OrderBy(Function(p) p.Y) _
.Select(Function(p) p.Text.Trim()) _
.FirstOrDefault(Function(t) Not String.IsNullOrWhiteSpace(t) AndAlso t.Length > 3)
End Function
Private Function ExtractTotal(text As String) As Decimal?
Dim match = Regex.Match(text,
"(?:Total|Grand Total|Amount Due):?\s*\$?\s*([\d,]+\.\d{2})",
RegexOptions.IgnoreCase)
Return If(match.Success,
Decimal.Parse(match.Groups(1).Value.Replace(",", "")),
CType(Nothing, Decimal?))
End Function
End Class
Public Class DocumentResult
Public Property FilePath As String
Public Property Vendor As String
Public Property Total As Decimal?
Public Property Confidence As Double
Public Property ProcessedAt As DateTime
End Class移除 webhook 层可以消除 HTTPS 端点、webhook 密钥轮换要求、结果队列、HMAC 验证逻辑和重试配置。 整个下游管道系统的存在仅仅是因为 非常 的结果是从远程服务器异步到达的。 使用IronOCR,Parallel.ForEach全部替代。 多线程示例详细展示了每线程IronTesseract模式,异步OCR指南涵盖了Task.Run集成以提高UI响应能力。 速度优化指南涵盖了针对批量工作负载实现最大吞吐量的实例配置。
非常 API 到IronOCR映射参考
| 非常 | IronOCR当量 |
|---|---|
new VeryfiClient(clientId, clientSecret, username, apiKey) | new IronTesseract() + IronOcr.License.LicenseKey = "key" |
_client.ProcessDocumentAsync(bytes) | ocr.Read(ocrInput) |
_client.ProcessDocumentAsync(bytes, categories: new[] { "invoices" }) | input.LoadPdf(路径); ocr.Read(input) |
_client.ProcessDocumentAsync(bytes, categories: new[] { "bank_statements" }) | input.LoadPdf(路径); ocr.Read(input) |
response.Vendor?.Name | 第一段由result.Pages[0].Paragraphs排序 |
response.Total | Regex.Match(result.Text, @"Total:?\s*\$?([\d,]+\.\d{2})") |
response.Tax | Regex.Match(result.Text, @"Tax:?\s*\$?([\d,]+\.\d{2})") |
response.Date | Regex.Match(result.Text, @"\d{1,2}/\d{1,2}/\d{4}") |
response.LineItems | result.Pages[0].Paragraphs根据Y坐标范围过滤 |
response.InvoiceNumber | Regex.Match(result.Text, @"Invoice\s*#?\s*:?\s*(\w+[-\w]*)") |
response.BankAccount?.AccountNumber | Regex.Match(result.Text, @"Account\s*#?\s*:?\s*(\d{4,})") |
response.BankAccount?.RoutingNumber | Regex.Match(result.Text, @"Routing\s*#?\s*:?\s*(\d{9})") |
response.ConfidenceScore | word.Confidence(逐字) |
response.Payment?.Last4 | Regex.Match(result.Text, @"\*{4}\s*(\d{4})") |
VeryfiApiException(401/402/429/500) | 标准.NET异常——本地处理不涉及 HTTP 错误代码 |
| 上传前进行 Base64 编码 | 不需要——ocr.Read(filePath)直接接受文件路径 |
response.Category | 自定义关键字与result.Text匹配 |
| Webhook有效载荷反序列化 | 不需要——ocr.Read()同步返回结果 |
ProcessDocumentAsync带有重试/退避 | 无需限制——本地处理无速率限制 |
常见迁移问题和解决方案
问题 1:缺少预解析字段
Veryfi:response.LineItems以预训练ML模型的结构字段形式到达。 客户端无需任何提取逻辑。
解决方案:为应用程序使用的每个字段编写正则表达式模式。 迁移工作通常需要 8 到 24 小时,具体取决于您要处理的不同文档布局的数量。 对于常见的收据和发票模式,发票 OCR 教程和收据扫描教程提供了完整的提取模式实现。
// Map each 非常 field to a Regex extraction
private static readonly Dictionary<string, string> FieldPatterns = new()
{
["InvoiceNumber"] = @"Invoice\s*#?\s*:?\s*(\w+[-\w]*)",
["PurchaseOrder"] = @"(?:PO|P\.O\.|Purchase Order)\s*#?\s*:?\s*(\w+)",
["DueDate"] = @"Due\s*(?:Date)?:?\s*(\d{1,2}/\d{1,2}/\d{4})",
["PaymentTerms"] = @"(?:Terms|Net)\s*:?\s*(\w+\s*\d+)"
};
public string ExtractField(string text, string fieldName)
{
if (!FieldPatterns.TryGetValue(fieldName, out var pattern))
return null;
var match = Regex.Match(text, pattern, RegexOptions.IgnoreCase);
return match.Success ? match.Groups[1].Value.Trim() : null;
}' Map each 非常 field to a Regex extraction
Private Shared ReadOnly FieldPatterns As New Dictionary(Of String, String) From {
{"InvoiceNumber", "Invoice\s*#?\s*:?\s*(\w+[-\w]*)"},
{"PurchaseOrder", "(?:PO|P\.O\.|Purchase Order)\s*#?\s*:?\s*(\w+)"},
{"DueDate", "Due\s*(?:Date)?:?\s*(\d{1,2}/\d{1,2}/\d{4})"},
{"PaymentTerms", "(?:Terms|Net)\s*:?\s*(\w+\s*\d+)"}
}
Public Function ExtractField(text As String, fieldName As String) As String
Dim pattern As String
If Not FieldPatterns.TryGetValue(fieldName, pattern) Then
Return Nothing
End If
Dim match As Match = Regex.Match(text, pattern, RegexOptions.IgnoreCase)
Return If(match.Success, match.Groups(1).Value.Trim(), Nothing)
End Function问题 2:代码库中各处的异步方法签名
Veryfi: ProcessDocumentAsync在Veryfi SDK级别是异步的。 团队通常通过每个调用方法向上传播async Task<t>签名。
解决方案:IronOCR的Read()是同步的。 现有Task.Run包装来保留。 这样既避免了对整个代码库进行大规模签名更改,又消除了对云的依赖。
// Preserve async signature during transition — no codebase-wide refactor needed
public async Task<string> GetVendorNameAsync(string documentPath)
{
return await Task.Run(() =>
{
var result = _ocr.Read(documentPath);
return result.Pages[0].Paragraphs
.OrderBy(p => p.Y)
.Select(p => p.Text.Trim())
.FirstOrDefault(t => t.Length > 3);
});
}Public Async Function GetVendorNameAsync(documentPath As String) As Task(Of String)
Return Await Task.Run(Function()
Dim result = _ocr.Read(documentPath)
Return result.Pages(0).Paragraphs _
.OrderBy(Function(p) p.Y) _
.Select(Function(p) p.Text.Trim()) _
.FirstOrDefault(Function(t) t.Length > 3)
End Function)
End Function问题 3:凭据配置分散在各个环境中
Veryfi: 四个凭证(appsettings.json、Docker Compose文件中的环境变量块、GitHub Actions机密、Azure密钥保管库引用和CI/CD管道配置中。
解决方案:从每个环境中搜索并删除所有四个凭据条目。 添加一个单独的IRONOCR_LICENSE_KEY环境变量。 启动时加载。
# Find all 非常 credential references
grep -r "Veryfi:ClientId\|Veryfi:ClientSecret\|Veryfi:Username\|Veryfi:ApiKey" \
--include="*.json" --include="*.yml" --include="*.yaml" --include="*.env" .
// Load from environment at startup
IronOcr.License.LicenseKey = Environment.GetEnvironmentVariable("IRONOCR_LICENSE_KEY")
?? throw new InvalidOperationException("IRONOCR_LICENSE_KEY not set");' Load from environment at startup
IronOcr.License.LicenseKey = If(Environment.GetEnvironmentVariable("IRONOCR_LICENSE_KEY"), Throw New InvalidOperationException("IRONOCR_LICENSE_KEY not set"))问题 4:之前未发现的扫描质量问题
Veryfi:云处理包括在机器学习推理运行之前进行服务器端图像增强。 低质量的收据扫描件(纸张皱巴巴的、热敏打印褪色的、手机照片歪斜的)在现场提取之前会被默默地纠正。
解决方案:显式应用IronOCR的预处理流程。 Contrast()覆盖了大多数现实世界中的收据扫描质量问题。
using var input = new OcrInput();
input.LoadImage("receipt-phone-photo.jpg");
input.Deskew(); // correct rotation from angled phone capture
input.DeNoise(); // remove compression artifacts
input.Contrast(); // improve faded thermal print
input.Sharpen(); // recover edge detail
var result = _ocr.Read(input);Using input As New OcrInput()
input.LoadImage("receipt-phone-photo.jpg")
input.Deskew() ' correct rotation from angled phone capture
input.DeNoise() ' remove compression artifacts
input.Contrast() ' improve faded thermal print
input.Sharpen() ' recover edge detail
Dim result = _ocr.Read(input)
End Using图像质量校正指南和图像滤镜教程涵盖了针对特定扫描劣化模式应应用哪些滤镜。
问题 5:大批量处理吞吐量
Veryfi:速率限制会降低文档提交速度。 HTTP 429 响应需要指数退避逻辑。 吞吐量受 非常 每个套餐的速率限制,而不是受您的硬件限制。
解决方案:IronOCR的性能仅受限于 CPU 核心数。 对每个线程使用一个Parallel.ForEach一起使用。 在 8 核服务器上,吞吐量与核心数量大致呈线性关系。
// One IronTesseract per thread — do not share instances across threads
Parallel.ForEach(
documentPaths,
new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount },
path =>
{
var ocr = new IronTesseract();
var result = ocr.Read(path);
SaveResult(path, result.Text, result.Confidence);
});Parallel.ForEach(
documentPaths,
New ParallelOptions With { .MaxDegreeOfParallelism = Environment.ProcessorCount },
Sub(path)
Dim ocr As New IronTesseract()
Dim result = ocr.Read(path)
SaveResult(path, result.Text, result.Confidence)
End Sub)问题 6:专有 JSON Schema 仅限 非常 使用
Veryfi: 所有提取代码均读取自Veryfi的响应架构:response.BankAccount?.RoutingNumber。 这段代码仅适用于 非常 的 SDK。 非常 API 更新中任何字段名称的更改都会导致应用程序代码出错。
解决方案: IronOCR提取使用标准.NET System.Text.RegularExpressions.Regex处理纯文本。 这些模式具有可移植性,无需模拟任何 SDK 即可进行测试,并且完全由您控制。 单元测试在没有任何网络连接的情况下运行。
// Extraction logic that is fully portable and unit-testable
[Fact]
public void ExtractsRoutingNumberFromInvoiceText()
{
const string sampleText = "Routing Number: 021000021\nAccount: 1234567890";
var match = Regex.Match(sampleText, @"Routing\s*(?:Number)?:?\s*(\d{9})",
RegexOptions.IgnoreCase);
Assert.True(match.Success);
Assert.Equal("021000021", match.Groups[1].Value);
}<Fact>
Public Sub ExtractsRoutingNumberFromInvoiceText()
Const sampleText As String = "Routing Number: 021000021" & vbLf & "Account: 1234567890"
Dim match = Regex.Match(sampleText, "Routing\s*(?:Number)?:?\s*(\d{9})", RegexOptions.IgnoreCase)
Assert.True(match.Success)
Assert.Equal("021000021", match.Groups(1).Value)
End Sub非常 迁移清单
迁移前
在修改任何代码之前,请先审核代码库,清点所有 非常 的使用情况:
# Find all 非常 using statements
grep -rn "using Veryfi" --include="*.cs" .
# Find all VeryfiClient instantiations
grep -rn "VeryfiClient\|ProcessDocumentAsync" --include="*.cs" .
# Find all 非常 response field accesses
grep -rn "response\.Vendor\|response\.Total\|response\.LineItems\|response\.BankAccount" --include="*.cs" .
# Find all credential configuration references
grep -r "Veryfi:ClientId\|Veryfi:ClientSecret\|Veryfi:Username\|Veryfi:ApiKey" \
--include="*.json" --include="*.yml" --include="*.yaml" --include="*.env" .
# Find all webhook-related code
grep -rn "VeryfiWebhook\|X-Veryfi-Token\|webhook" --include="*.cs" .
记录ProcessDocumentAsync调用站点的总数,每个调用站点访问的响应字段列表,以及包含Veryfi凭证的环境列表。
代码迁移
- 从解决方案中的所有项目中删除
VeryfiNuGet包。 - 将
IronOcrNuGet包安装到以前引用Veryfi的所有项目。 - 在应用程序启动时添加
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";(在任何OCR调用之前)。 - 将所有
using IronOcr;。 - 将
IronTesseract字段初始化。 - 从每个
appsettings.*.json和机密配置文件中删除所有四个Veryfi凭证条目。 - 将
ocr.Read(ocrInput)。 - 用段落排序的文本提取从
response.Vendor?.Name访问。 - 用针对
response.Total,response.Tax,response.InvoiceNumber及其他结构字段访问。 - 用Y坐标过滤的
response.LineItems迭代。 - 删除 webhook 控制器类并移除 webhook 端点注册。
- 从所有环境中移除 webhook 秘密环境变量。
- 添加
Contrast())以处理扫描图像输入。 - 用每线程一个
Parallel.ForEach替换单线程顺序循环。 - 将
IRONOCR_LICENSE_KEY添加到所有环境变量配置和CI/CD机密存储中。
后迁移
- 验证迁移部署后,HTTP 流量日志中是否出现 非常 网络调用。
- 确认提取的供应商名称与 20-50 张收据样本集中的预期值相符。
- 确认提取的总数与同一样本集的预期值在 0.01 美元的容差范围内一致。
- 验证文档语料库中每种发票格式的发票号码提取是否成功。
- 测试批量处理吞吐量与 非常 基准吞吐量的对比,以确认解除速率限制。
- 在没有任何网络连接的情况下运行完整的测试Suite,以确认完全不依赖云。
- 确认
result.Confidence评分超过80%,以确保文档扫描干净; 低于 80% 表示需要添加预处理步骤。 - 确认所有四个 非常 凭证都已从每个环境(开发、测试、生产)中删除。
- 确认 webhook 端点返回 404 或已从路由表中删除。
- 测试在低质量收据扫描件(皱巴巴的、褪色的、倾斜的)上启用预处理管道后的性能。
迁移到IronOCR的主要优势
本地处理的财务文件不会被第三方泄露。迁移后,从发票中提取的银行账号、从支票中解析的路由号码以及从银行对账单中读取的交易记录都将在您的硬件上进行处理。 任何第三方安全事件、子处理器数据访问或 非常 基础设施漏洞都无法暴露从未离开过您服务器的文档。
迁移部署当天,每份文档的成本将降至零。每月处理 5 万份文档后,每月 5,000 至 15,000 美元的 非常 费用将不再存在。 IronOCRProfessional一次性许可证费用为 2,999 美元,可在第一个月的第一周内收回。 销量越高,节省的成本就越大,无需任何批量折扣谈判或合同续签。
处理吞吐量取决于硬件配置,而非供应商的速率限制。HTTP 429 响应、套餐级吞吐量上限以及季节性超额费用都是云 API 的架构固有缺陷。使用IronOCR,增加 CPU 核心数即可成比例地提升吞吐量。 一批 10,000 张收据将按照您的时间安排进行处理,而不是按照 非常 的速率限制时间表进行处理。
所有文档类型均可使用同一 API 进行处理。当人力资源部门需要处理入职表格、法务部门需要提取合同文本或运营部门需要提取发货单据数据时,组织不再需要第二个 OCR 工具。 ocr.Read()处理所有这些问题。 从图像中读取文本教程和专门的文档指南涵盖了IronOCR处理的所有文档格式。
提取逻辑成为代码库中不可或缺的一部分。正则表达式模式纳入源代码控制,可在拉取请求中进行审查,无需模拟任何 SDK 即可在单元测试中进行测试,并可根据生产反馈进行调整。 当 非常 的预训练模型返回错误的供应商名称时,无需进行任何调整。 当IronOCR的提取模式返回错误的供应商名称时,只需修改一行正则表达式并编写单元测试即可修复。IronOCR的许可页面涵盖了各种级别选项,包括面向偏好按年付费而非永久购买的团队的 SaaS 订阅方案。
IronOCR占用空间缩小至一个可在任何地方运行的NuGet包。IronOCR以单个包的形式安装,无需外部依赖项、无需本地二进制文件管理,也无需配置 tessdata 文件夹。 同一个软件包引用可以在 Windows、Linux、macOS、Docker、Azure 应用服务和 AWS Lambda 上解析,而无需平台条件代码。 对于 非常 的网络出口要求可能阻碍部署的容器化环境,请参阅Docker 部署指南和Linux 部署指南。
