PAYATHON 2026

PowerShell 压缩 PDF 后无法删除源文件,如何正确释放文件句柄?

数据阿明

需要显式释放 PDF 读取流和 ZIP 条目写入流。原代码只释放了 ZipArchive 和 ZIP 文件流,没有释放 OpenRead() 返回的源文件流,这直接导致 PDF 持续被占用。

哪一行没有释放文件?

问题出在这一行:

[System.IO.File]::OpenRead($file.FullName).CopyTo($entry.Open())

它打开了两个流:

  • OpenRead($file.FullName) 返回读取 PDF 的 FileStream。
  • $entry.Open() 返回写入 ZIP 条目的流。

CopyTo() 只负责复制数据,不会替调用方关闭这两个流。代码没有把流保存到变量中,也没有显式调用它们的 Dispose()。

$archive.Dispose() 会完成归档并释放相关资源,但不会关闭独立打开的 PDF 读取流。PowerShell 也没有一个通用的“关闭所有文件”命令可以代替对这些对象的资源管理。

脚本结束后能够删除文件,这与句柄最终被释放的现象一致,但不应依赖脚本结束或垃圾回收来决定文件何时可删除。

如何修改脚本?

将每个流保存到变量中,并用 try/finally 释放。下面的代码假设 $sourceDir 已设置,可用来替换原来的压缩循环和最后扫描 ZIP 删除 PDF 的部分:

$files = @(Get-ChildItem -LiteralPath $sourceDir -Filter '*.pdf' -File)

if ($files.Count -eq 0) {
    Write-Host "No PDF files found in '$sourceDir'."
}

foreach ($file in $files) {
    $zipPath = Join-Path $sourceDir ($file.BaseName + '.ZIP')

    try {
        $zipStream = [System.IO.File]::Open($zipPath, 'Create')
        try {
            $archive = [System.IO.Compression.ZipArchive]::new(
                $zipStream,
                [System.IO.Compression.ZipArchiveMode]::Create
            )
            try {
                $entry = $archive.CreateEntry($file.Name)

                $sourceStream = [System.IO.File]::OpenRead($file.FullName)
                try {
                    $entryStream = $entry.Open()
                    try {
                        $sourceStream.CopyTo($entryStream)
                    }
                    finally {
                        $entryStream.Dispose()
                    }
                }
                finally {
                    $sourceStream.Dispose()
                }
            }
            finally {
                $archive.Dispose()
            }
        }
        finally {
            $zipStream.Dispose()
        }

        Write-Host "Zipped '$($file.Name)' to '$zipPath'"

        # Delete the source only after copying and archive disposal succeed.
        Remove-Item -LiteralPath $file.FullName -ErrorAction Stop
        Write-Host "Deleted '$($file.Name)'"
    }
    catch {
        Write-Error "Failed to process '$($file.Name)': $_"
    }
}

删除操作放在当前文件的所有资源释放之后。如果复制或归档结束过程抛出异常,代码会进入 catch,不会继续删除该 PDF。嵌套的 finally 也能确保内层操作失败时,外层资源仍会尝试释放。

为什么不继续扫描 ZIP 来删除 PDF?

原代码以目录中存在同名 ZIP 为依据删除 PDF,但这不能证明本次压缩成功:ZIP 可能来自之前的运行,也可能是本次异常后留下的不完整文件。

根据当前文件的处理结果执行删除,可以避免因这些 ZIP 误删源文件。示例中的成功条件是复制和归档结束过程没有报告异常;它没有重新解压或校验内容。如果源 PDF 很重要,可以在删除前增加归档读取检查或解压后的内容比对。

另外,File.Open($zipPath, 'Create') 会覆盖已有的同名 ZIP,与原代码的行为一致。如果需要保留已有文件,应先检查目标是否存在,或改用 CreateNew,让同名冲突直接报错。

修改后仍然提示占用怎么办?

先确认原来的链式 OpenRead(...).CopyTo(...) 已被替换,脚本其他位置也没有留下未释放的读取流。之前运行所遗留的句柄不会因为修改了后续代码就自动关闭。

如果错误仍然出现,再检查 PDF 阅读器、资源管理器预览或其他进程是否打开了文件。Remove-Item -Force 无法绕过仍然有效的文件占用锁,必须先释放持有文件的句柄。

备注:内容仅供参考。