You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何开发自动提取PDF表单数据写入SQL的后台定时服务

基于PowerShell的无人值守采购订单导入方案

你选PowerShell做全流程实现是非常合适的,整套方案不需要额外开发重型桌面/服务程序,部署维护成本极低,完全可以实现无弹窗静默后台运行,核心实现逻辑如下:

前置依赖准备

所有依赖都可以离线部署,不需要服务器连外网:

  • 数据提取:针对带表单域的标准PDF采购单(绝大多数企业用的可填写PDF订单都是AcroForm格式),直接用iText社区版的DLL读取表单域值,不需要OCR,识别准确率100%;如果是扫描版非结构化PDF,搭配Tesseract OCR做固定区域识别即可
  • 数据库操作:直接用PowerShell官方的SqlServer模块,支持批量高速写入SQL Server,不需要逐行插入
  • 后台托管:不需要自己写循环调度逻辑,用Windows任务计划程序或NSSM即可实现开机自启、崩溃自恢复、无界面静默运行,比自研服务稳定性更高

核心脚本实现

首次环境配置

以管理员身份打开PowerShell,执行以下命令安装SQL模块:

Install-Module -Name SqlServer -Force -AllowClobber -Scope LocalMachine

将iText相关的依赖DLL放到脚本同级的lib目录下,不需要全局注册。

主脚本结构

把配置项统一放在脚本开头,方便后续维护调整:

# -------------------------- 配置项 --------------------------
$pendingPath = "D:\PO_System\Wait_Import"    # 待处理PDF存放目录
$archivedPath = "D:\PO_System\Imported"     # 已处理PDF归档目录
$errorPath = "D:\PO_System\Parse_Error"     # 解析失败文件存放目录
$logPath = "D:\PO_System\Run_Log.log"       # 运行日志路径
$sqlConnString = "Server=你的SQL实例地址;Database=采购订单库;Integrated Security=True;Encrypt=False;" # 优先用Windows集成认证,避免硬编码账号密码
$tableName = "dbo.purchase_order"           # 采购订单存储表名
# -----------------------------------------------------------

# 加载PDF处理依赖
Add-Type -Path ".\lib\itext.kernel.dll"
Add-Type -Path ".\lib\itext.forms.dll"

# 日志写入函数
function Write-Log {
    param([string]$Content)
    Add-Content -Path $logPath -Value "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] $Content"
}

# PDF表单数据提取函数
function Read-PoFormData {
    param([string]$PdfFullPath)
    $reader = [iText.Kernel.Pdf.PdfReader]::new($PdfFullPath)
    $doc = [iText.Kernel.Pdf.PdfDocument]::new($reader)
    $form = [iText.Forms.PdfAcroForm]::getAcroForm($doc, $true)
    $fields = $form.getFormFields()
    # 以下字段名请根据你实际PDF的表单域名称调整,首次运行可以遍历$fields的键名打印所有域确认
    $poInfo = [PSCustomObject]@{
        po_no = $fields.Get("PO_Number").GetValueAsString().Trim()
        vendor = $fields.Get("Vendor_FullName").GetValueAsString().Trim()
        total_amount = [decimal]$fields.Get("PO_Total").GetValueAsString().Trim()
        po_date = [datetime]$fields.Get("PO_Date").GetValueAsString().Trim()
        source_file = Split-Path $PdfFullPath -Leaf
        import_time = Get-Date
    }
    $doc.Close()
    $reader.Close()
    return $poInfo
}

# 初始化目录
if (-not (Test-Path $archivedPath)) { New-Item -Path $archivedPath -ItemType Directory | Out-Null }
if (-not (Test-Path $errorPath)) { New-Item -Path $errorPath -ItemType Directory | Out-Null }

# 遍历待处理PDF
$poList = New-Object System.Collections.Generic.List[Object]
Get-ChildItem -Path $pendingPath -Filter "*.pdf" -File | Where-Object { $_.Name -notlike "~$*" } | ForEach-Object {
    try {
        # 检查文件是否被占用,避免读取正在拷贝/编辑的不完整文件
        $fs = $_.Open([System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None)
        $fs.Close()
        # 提取数据
        $data = Read-PoFormData -PdfFullPath $_.FullName
        $poList.Add($data)
        # 归档已处理文件
        Move-Item -Path $_.FullName -Destination (Join-Path $archivedPath $_.Name) -Force
        Write-Log "文件 $($_.Name) 解析完成"
    }
    catch {
        Write-Log "文件 $($_.Name) 处理失败:$($_.Exception.Message)"
        Move-Item -Path $_.FullName -Destination (Join-Path $errorPath $_.Name) -Force
    }
}

# 批量写入SQL Server
if ($poList.Count -gt 0) {
    try {
        $bulk = [System.Data.SqlClient.SqlBulkCopy]::new($sqlConnString)
        $bulk.DestinationTableName = $tableName
        # 字段映射:左边是PowerShell对象属性名,右边是SQL表列名
        $bulk.ColumnMappings.Add("po_no", "po_number") | Out-Null
        $bulk.ColumnMappings.Add("vendor", "vendor_name") | Out-Null
        $bulk.ColumnMappings.Add("total_amount", "total_amount") | Out-Null
        $bulk.ColumnMappings.Add("po_date", "order_date") | Out-Null
        $bulk.ColumnMappings.Add("source_file", "source_file_name") | Out-Null
        $bulk.ColumnMappings.Add("import_time", "import_time") | Out-Null
        # 转DataTable写入
        $dataTable = [System.Data.DataTable]::new()
        $poList | ForEach-Object {
            $row = $dataTable.NewRow()
            foreach ($prop in $_.PSObject.Properties) {
                if (-not $dataTable.Columns.Contains($prop.Name)) {
                    $dataTable.Columns.Add($prop.Name, $prop.Value.GetType()) | Out-Null
                }
                $row[$prop.Name] = $prop.Value
            }
            $dataTable.Rows.Add($row)
        }
        $bulk.WriteToServer($dataTable)
        $bulk.Close()
        Write-Log "数据写入完成,共导入 $($poList.Count) 条采购订单"
    }
    catch {
        Write-Log "数据库写入失败:$($_.Exception.Message)"
    }
}

静默后台运行配置

不要在脚本内写无限循环+Start-Sleep做定时调度,这种方式异常退出后无法自动恢复,资源占用也更高,直接用系统原生能力实现服务级运行效果:

  • 每日定时跑的场景:用Windows任务计划程序创建任务
    • 安全选项选择「不管用户是否登录都要运行」,取消勾选「只有用户登录时才运行」,运行时不会弹出PowerShell窗口
    • 触发器设置为你需要的每日执行时间,也可以设置为每10/30分钟执行一次,满足近实时导入需求
    • 操作选择启动程序,程序路径填powershell.exe,参数填-ExecutionPolicy Bypass -WindowStyle Hidden -File "你的脚本全路径.ps1"
    • 设置任务失败自动重启规则:失败后每5分钟重试,最多重试3次,避免偶发异常导致任务中断
  • 7*24小时长驻运行的场景:用NSSM把脚本注册为原生Windows Service,支持开机自启、崩溃自动拉起、资源占用监控,和自研Service程序没有体验差异

落地注意事项

  • 首次调试时先把所有表单域打印出来确认名称,避免字段对应错误
  • 数据库连接优先用Windows集成认证,不要把数据库账号密码硬编码在脚本里,降低安全风险
  • 如果你的采购单是扫描版PDF,把Read-PoFormData函数替换为Tesseract OCR识别逻辑即可,固定模板下提前预设识别区域,识别准确率可以满足业务需求
  • 可以在写入前增加重复校验逻辑,按PO单号+源文件名判断是否已经导入,避免重复插入数据
  • 所有操作必须留日志,出问题可以直接定位是文件解析错、文件被占用还是数据库连接问题

内容的提问来源于stack exchange,提问作者Une Jolie fleur

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 19:12:43