baidu ocr 识别增值税专用发票图片的 PowerShell 脚本
参考
根据纳税人查验的票种,其输入的校验项目也不相同,其中:
(1)增值税专用发票、增值税电子专用发票:发票代码、发票号码、开票日期和开具金额(不含税);
(2)增值税普通发票、增值税电子普通发票(含通行费发票)、增值税普通发票(卷票):发票代码、发票号码、开票日期和校验码后六位;
(3)机动车销售统一发票:发票代码、发票号码、开票日期和不含税价;
(4)二手车销售统一发票发票:发票代码、发票号码、开票日期和车价合计;
(5)电子发票(增值税专用发票)、电子发票(普通发票):发票号码(20位)、开票日期、价税合计;
(6)电子发票服务平台开具的纸质增值税专用发票:发票代码、发票号码、开票日期和开具金额(不含税);
(7)电子发票服务平台开具的纸质增值税普通发票:发票代码、发票号码、开票日期和校验码(密码区发票号码后六位);
(8)电子发票(铁路电子客票):发票号码(20位)、开票日期和票价;
(9)电子发票(航空运输电子客票行程单):发票号码(20位)、开票日期、开票金额(价税合计)。
baidu_image_ocr.ps1
param(
[string]$ApiKey = "",
[string]$SecretKey = "",
[string]$AccessToken = "",
[string]$InputFolderPath = "./",
[string[]]$ImageExtensions = @("*.png", "*.jpg", "*.jpeg", "*.bmp"),
[bool]$OverwriteExistingJson = $true
)
#region Configuration
# 脚本开始时尝试设置控制台输出编码为 UTF-8
try {
[System.Console]::OutputEncoding = [System.Text.Encoding]::UTF8
Write-Host "控制台输出编码已设置为 UTF-8。" -ForegroundColor Green
} catch {
Write-Warning "设置控制台输出编码为 UTF-8 失败"
}
$ErrorActionPreference = "Continue"
# 兼容旧变量名 (如果用户没传参,使用默认值)
if (-not $inputFolderPath) { $inputFolderPath = $InputFolderPath }
if (-not $imageExtensions) { $imageExtensions = $ImageExtensions }
# DEBUG: 输出接收到的参数
Write-Host "DEBUG: ApiKey=['$ApiKey']" -ForegroundColor DarkGray
Write-Host "DEBUG: SecretKey=['$SecretKey']" -ForegroundColor DarkGray
Write-Host "DEBUG: AccessToken=['$AccessToken'] Length=$($AccessToken.Length)" -ForegroundColor DarkGray
# 图片处理配置
# 图片处理配置
$targetSizeKB = 4096 # 目标文件大小 (KB)
$minQuality = 30 # 最低质量阈值
$maxWidth = 3000 # 最大宽度像素
$maxHeight = 4000 # 最大高度像素
$initialQuality = 80 # 初始JPEG质量
$minJsonSizeKB = 1 # 最小JSON文件大小(KB),小于此大小认为识别失败
#endregion Configuration
#region Helper Functions
function Get-OcrValue {
param($InputObject)
if ($null -eq $InputObject) { return "" }
if ($InputObject -is [string]) { return $InputObject }
if ($InputObject -is [System.ValueType]) { return [string]$InputObject }
if ($InputObject.PSObject.Properties['word']) { return $InputObject.word }
return [string]$InputObject
}
function Get-BaiduAccessToken {
param(
[Parameter(Mandatory=$true)][string]$ApiKey,
[Parameter(Mandatory=$true)][string]$SecretKey
)
$url = "https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=$ApiKey&client_secret=$SecretKey"
try {
Write-Host "正在获取 Access Token..." -ForegroundColor Gray
$response = Invoke-RestMethod -Uri $url -Method Get
if ($response.access_token) {
Write-Host $response.access_token
return $response.access_token
} else {
Write-Error "获取Access Token失败: $($response.error_description)"
return $null
}
} catch {
Write-Error "获取Access Token网络请求失败: $($_.Exception.Message)"
return $null
}
}
function Is-ValidInvoiceCode {
param([string]$code)
if ([string]::IsNullOrWhiteSpace($code)) {
return $false
}
# 发票代码应该是10位或12位数字
$cleanCode = $code -replace '[^\d]', ''
$codeLength = $cleanCode.Length
if ($codeLength -eq 10 -or $codeLength -eq 12) {
return $true
}
return $false
}
function Is-SuccessfulJson {
param([string]$jsonPath)
if (-not (Test-Path $jsonPath)) { return $false }
$fileInfo = Get-Item $jsonPath
if ($fileInfo.Length -lt ($minJsonSizeKB * 1KB)) {
Write-Host " JSON文件过小 ($([math]::Round($fileInfo.Length/1KB, 2))KB),可能识别失败" -ForegroundColor Yellow
return $false
}
try {
$jsonContent = Get-Content -Path $jsonPath -Raw -ErrorAction Stop | ConvertFrom-Json
# 检查是否有错误码
if ($jsonContent.error_code) {
Write-Host " JSON包含错误码: $($jsonContent.error_code) - $($jsonContent.error_msg)" -ForegroundColor Yellow
return $false
}
# 检查是否有 words_result
if ($null -eq $jsonContent.words_result) {
Write-Host " JSON缺少 words_result 字段" -ForegroundColor Yellow
return $false
}
return $true
} catch {
Write-Host " JSON解析失败: $_" -ForegroundColor Red
return $false
}
}
function Compress-ImageToTargetSize {
param (
[string]$ImagePath,
[int]$TargetSizeKB,
[int]$MaxWidth,
[int]$MaxHeight,
[int]$Quality
)
try {
# 加载图片
$image = [System.Drawing.Image]::FromFile($ImagePath)
$originalWidth = $image.Width
$originalHeight = $image.Height
$fileSize = (Get-Item $ImagePath).Length
$fileSizeKB = $fileSize / 1KB
Write-Host " 处理图片: $ImagePath" -ForegroundColor Cyan
Write-Host " 原始图片: ${originalWidth}x${originalHeight}, 大小: $([math]::Round($fileSizeKB, 2)) KB" -ForegroundColor Gray
# 如果文件大小和尺寸都在范围内,直接使用原图
if ($fileSizeKB -le $TargetSizeKB -and $originalWidth -le $MaxWidth -and $originalHeight -le $MaxHeight) {
Write-Host " ? 原图符合要求,跳过压缩" -ForegroundColor Green
$image.Dispose()
return [System.IO.File]::ReadAllBytes($ImagePath)
}
# 计算压缩后的尺寸
if ($originalWidth -gt $MaxWidth -or $originalHeight -gt $MaxHeight) {
$ratio = [Math]::Min($MaxWidth / $originalWidth, $MaxHeight / $originalHeight)
$newWidth = [int]($originalWidth * $ratio)
$newHeight = [int]($originalHeight * $ratio)
} else {
$newWidth = $originalWidth
$newHeight = $originalHeight
}
$attempt = 1
$maxAttempts = 3
$compressedBytes = $null
while ($attempt -le $maxAttempts) {
$memoryStream = New-Object System.IO.MemoryStream
$newImage = New-Object System.Drawing.Bitmap($newWidth, $newHeight)
$graphics = [System.Drawing.Graphics]::FromImage($newImage)
$graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
$graphics.DrawImage($image, 0, 0, $newWidth, $newHeight)
$graphics.Dispose()
# 设置JPEG压缩质量
$jpegEncoderInfo = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() | Where-Object { $_.MimeType -eq "image/jpeg" }
$encoderParameters = New-Object System.Drawing.Imaging.EncoderParameters(1)
$encoderParameters.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter([System.Drawing.Imaging.Encoder]::Quality, $Quality)
$newImage.Save($memoryStream, $jpegEncoderInfo, $encoderParameters)
$tempBytes = $memoryStream.ToArray()
$compressedSizeKB = $tempBytes.Length / 1KB
Write-Host " 压缩尝试 $attempt, 质量: $Quality%, 尺寸: ${newWidth}x${newHeight}" -ForegroundColor Gray
Write-Host " 压缩后大小: $([math]::Round($compressedSizeKB, 2)) KB" -ForegroundColor Gray
if ($compressedSizeKB -le $TargetSizeKB) {
$compressedBytes = $tempBytes
$newImage.Dispose()
$memoryStream.Dispose()
Write-Host " ? 压缩成功" -ForegroundColor Green
break
}
# 如果还是太大,降低质量和尺寸继续尝试
$Quality -= 20
$newWidth = [int]($newWidth * 0.8)
$newHeight = [int]($newHeight * 0.8)
$newImage.Dispose()
$memoryStream.Dispose()
$attempt++
}
$image.Dispose()
return $compressedBytes
} catch {
Write-Error " 图片压缩失败: $_"
if ($image) { $image.Dispose() }
return $null
}
}
function Invoke-BaiduOcrForFile {
param (
[string]$ImagePath,
[string]$AccessToken
)
# 压缩图片
$imageBytes = Compress-ImageToTargetSize -ImagePath $ImagePath -TargetSizeKB $targetSizeKB -MaxWidth $maxWidth -MaxHeight $maxHeight -Quality $initialQuality
if (-not $imageBytes) {
Write-Error " 无法处理图片,跳过API调用"
return $null
}
$imageBase64 = [Convert]::ToBase64String($imageBytes)
# 移除可能存在的换行符
$imageBase64 = $imageBase64 -replace '\s+', ''
if ([string]::IsNullOrEmpty($imageBase64)) {
Write-Error " Base64转换失败"
return $null
}
Write-Host " Base64长度: $($imageBase64.Length) 字符" -ForegroundColor Gray
# URL 编码 (使用 System.Net.WebUtility 以获得更好的兼容性)
$urlEncodedImageBase64 = [System.Net.WebUtility]::UrlEncode($imageBase64)
# API调用
$apiUrl = "https://aip.baidubce.com/rest/2.0/ocr/v1/vat_invoice?access_token=$AccessToken"
$body = "image=$urlEncodedImageBase64"
Write-Host " 发送API请求..." -ForegroundColor Gray
try {
$response = Invoke-RestMethod -Uri $apiUrl -Method Post -Body $body -ContentType "application/x-www-form-urlencoded"
# 检查业务逻辑错误
if ($response.error_code) {
# 处理特定错误码
if ($response.error_code -eq 216201 -or $response.error_code -eq 216100 -or $response.error_code -eq 216101 -or $response.error_code -eq 282103) {
Write-Warning " API返回错误 (码=$($response.error_code)): $($response.error_msg)。尝试重新编码/重试..."
}
Write-Error " OCR错误: 码=$($response.error_code), 信息=$($response.error_msg)"
return $null
}
return $response
} catch {
Write-Error " API请求失败: $_"
return $null
}
}
function Get-ExistingJsonData {
param([string]$jsonPath)
try {
$json = Get-Content -Path $jsonPath -Raw -ErrorAction Stop | ConvertFrom-Json
$wr = $json.words_result
# 使用 Get-OcrValue 提取值
$invoiceCode = Get-OcrValue -InputObject $wr.InvoiceCode
$invoiceNum = Get-OcrValue -InputObject $wr.InvoiceNum
$invoiceDate = Get-OcrValue -InputObject $wr.InvoiceDate
# 修正金额提取逻辑
# AmountInFiguers -> 含税金额
# TotalAmount -> 不含税金额
$amountTaxIncluded = Get-OcrValue -InputObject $wr.AmountInFiguers
$amountTaxExcluded = Get-OcrValue -InputObject $wr.TotalAmount
return @{
InvoiceCode = $invoiceCode
InvoiceNumber = $invoiceNum
InvoiceDate = $invoiceDate
TaxIncludedAmount = $amountTaxIncluded
TaxExcludedAmount = $amountTaxExcluded
}
} catch {
Write-Warning " 读取现有JSON失败: $_"
return $null
}
}
function Get-InvoiceSummary {
param($ocrResult)
if (-not $ocrResult -or -not $ocrResult.words_result) {
return $null
}
$wr = $ocrResult.words_result
# 使用 Get-OcrValue 提取值
$invoiceCode = Get-OcrValue -InputObject $wr.InvoiceCode
$invoiceNum = Get-OcrValue -InputObject $wr.InvoiceNum
$invoiceDate = Get-OcrValue -InputObject $wr.InvoiceDate
# 修正金额提取逻辑
$amountTaxIncluded = Get-OcrValue -InputObject $wr.AmountInFiguers
$amountTaxExcluded = Get-OcrValue -InputObject $wr.TotalAmount
return @{
InvoiceCode = $invoiceCode
InvoiceNumber = $invoiceNum
InvoiceDate = $invoiceDate
TaxIncludedAmount = $amountTaxIncluded
TaxExcludedAmount = $amountTaxExcluded
}
}
#endregion Helper Functions
#region Main Script Logic
# 检查Access Token
# 如果 AccessToken 为空,或者用户提供了 ApiKey (优先使用 ApiKey 获取新 Token)
if ([string]::IsNullOrWhiteSpace($AccessToken) -or (-not [string]::IsNullOrWhiteSpace($ApiKey))) {
# 尝试使用 ApiKey 和 SecretKey 获取
if (-not [string]::IsNullOrWhiteSpace($ApiKey) -and -not [string]::IsNullOrWhiteSpace($SecretKey)) {
Write-Host "尝试使用 API Key 获取 Access Token..." -ForegroundColor Yellow
$newToken = Get-BaiduAccessToken -ApiKey $ApiKey -SecretKey $SecretKey
if ($newToken) {
$AccessToken = $newToken
Write-Host "成功获取 Access Token" -ForegroundColor Green
} else {
if ([string]::IsNullOrWhiteSpace($AccessToken)) {
Write-Error "无法获取 Access Token,且未提供备用 Token。"
exit 1
} else {
Write-Warning "获取新 Token 失败,将尝试使用现有的 Token。"
}
}
} elseif ([string]::IsNullOrWhiteSpace($AccessToken)) {
Write-Error "错误:必须提供 Access Token,或者同时提供 ApiKey 和 SecretKey"
exit 1
}
}
$displayToken = if ($AccessToken.Length -gt 20) { $AccessToken.Substring(0, 20) + "..." } else { $AccessToken }
Write-Host "Access Token: $displayToken" -ForegroundColor Gray
# 检查输入文件夹
if (-not (Test-Path $inputFolderPath -PathType Container)) {
Write-Error "错误:输入文件夹 '$inputFolderPath' 不存在"
exit 1
}
# 查找图片文件
$imageFiles = Get-ChildItem -Path $inputFolderPath -Include $imageExtensions -Recurse -File
if (-not $imageFiles) {
Write-Warning "在文件夹 '$inputFolderPath' 中没有找到图片文件"
exit
}
$totalFiles = $imageFiles.Count
$processedCount = 0
$successCount = 0
$errorCount = 0
$skippedCount = 0
$reprocessCount = 0
$invoiceSummaries = @()
Write-Host "找到 $totalFiles 个图片文件开始处理`n" -ForegroundColor Green
foreach ($imageFile in $imageFiles) {
$processedCount++
$progressPercent = [math]::Round(($processedCount / $totalFiles) * 100, 1)
Write-Host "--------------------------------------------------" -ForegroundColor DarkGray
Write-Host "[$progressPercent%] 处理 $processedCount/$totalFiles`: $($imageFile.Name)" -ForegroundColor Cyan
$jsonPath = [System.IO.Path]::ChangeExtension($imageFile.FullName, ".json")
$invoiceSummary = $null
$needApiCall = $true
# 检查是否存在JSON文件
if (Test-Path $jsonPath -PathType Leaf) {
Write-Host " 发现已有JSON文件" -ForegroundColor Yellow
# 检查JSON是否成功
if (Is-SuccessfulJson -jsonPath $jsonPath) {
Write-Host " JSON识别成功,提取数据..." -ForegroundColor Green
# 提取已有数据
$existingData = Get-ExistingJsonData -jsonPath $jsonPath
if ($existingData) {
# 验证发票代码
Write-Host " 检查发票码: '$($existingData.InvoiceCode)' -> '$($existingData.InvoiceCode)' ($(if($existingData.InvoiceCode){$existingData.InvoiceCode.Length}else{0}) 位)" -ForegroundColor Gray
if (Is-ValidInvoiceCode -code $existingData.InvoiceCode) {
Write-Host " ? 发票码位数正确,跳过API调用" -ForegroundColor Green
$invoiceSummary = $existingData
$needApiCall = $false
$skippedCount++
$successCount++
} else {
Write-Host " 发票码位数不对,需要重新识别" -ForegroundColor Red
$reprocessCount++
}
} else {
Write-Host " JSON数据提取失败,需要重新识别" -ForegroundColor Red
}
} else {
Write-Host " JSON识别失败,需要重新识别" -ForegroundColor Red
}
}
# 如果需要API调用
if ($needApiCall) {
$ocrResult = Invoke-BaiduOcrForFile -ImagePath $imageFile.FullName -AccessToken $accessToken
if ($ocrResult) {
Write-Host " ? OCR识别成功" -ForegroundColor Green
# 保存JSON
$jsonContent = $ocrResult | ConvertTo-Json -Depth 10 -Compress
$jsonContent | Out-File $jsonPath -Encoding UTF8
# 提取摘要
$invoiceSummary = Get-InvoiceSummary -ocrResult $ocrResult
if ($invoiceSummary) {
Write-Host " 检查发票码: '$($invoiceSummary.InvoiceCode)' -> '$($invoiceSummary.InvoiceCode)' ($(if($invoiceSummary.InvoiceCode){$invoiceSummary.InvoiceCode.Length}else{0}) 位)" -ForegroundColor Gray
if (Is-ValidInvoiceCode -code $invoiceSummary.InvoiceCode) {
Write-Host " ? 识别成功,发票码正确" -ForegroundColor Green
$successCount++
} else {
Write-Warning " 识别成功,但发票码位数可能不正确"
# 仍然计入成功,因为API调用成功了
$successCount++
}
} else {
Write-Warning " 识别成功,但无法提取关键信息"
$errorCount++
}
} else {
Write-Error " 识别失败"
$errorCount++
}
}
# 输出摘要信息
if ($invoiceSummary) {
Write-Host " 发票代码: $($invoiceSummary.InvoiceCode)"
Write-Host " 发票号码: $($invoiceSummary.InvoiceNumber)"
Write-Host " 开票日期: $($invoiceSummary.InvoiceDate)"
Write-Host " 含税金额: $($invoiceSummary.TaxIncludedAmount)"
Write-Host " 不含税额: $($invoiceSummary.TaxExcludedAmount)"
$invoiceSummaries += [PSCustomObject]@{
FileName = $imageFile.Name
InvoiceCode = $invoiceSummary.InvoiceCode
InvoiceNumber = $invoiceSummary.InvoiceNumber
InvoiceDate = $invoiceSummary.InvoiceDate
TaxIncludedAmount = $invoiceSummary.TaxIncludedAmount
TaxExcludedAmount = $invoiceSummary.TaxExcludedAmount
Status = "Success"
}
} else {
$invoiceSummaries += [PSCustomObject]@{
FileName = $imageFile.Name
InvoiceCode = ""
InvoiceNumber = ""
InvoiceDate = ""
TaxIncludedAmount = ""
TaxExcludedAmount = ""
Status = "Failed"
}
}
}
# 导出汇总CSV
$csvPath = Join-Path $inputFolderPath "invoice_summary.csv"
$invoiceSummaries | Export-Csv -Path $csvPath -NoTypeInformation -Encoding UTF8
Write-Host "`n? 汇总报告已保存到: $csvPath" -ForegroundColor Green
Write-Host "--------------------------------------------------" -ForegroundColor DarkGray
Write-Host "处理完成!" -ForegroundColor Green
Write-Host "总计文件: $totalFiles"
Write-Host "成功识别: $successCount ($([math]::Round($successCount/$totalFiles*100))%)"
Write-Host "跳过处理: $skippedCount (已有成功结果)"
Write-Host "重新处理: $reprocessCount (发票码位数不对)"
Write-Host "识别失败: $errorCount ($([math]::Round($errorCount/$totalFiles*100))%)"
Write-Host "节省API调用: $skippedCount 次"
Write-Host "==================================================" -ForegroundColor Green
#endregion Main Script Logic
用法比如
./baidu_image_ocr.ps1 -ApiKey "<your_api_key>" -SecretKey "<your_secret_key>" -AccessToken=""
或
./baidu_image_ocr.ps1 -ApiKey "" -SecretKey "" -AccessToken="<your_access_token>"
浙公网安备 33010602011771号