Windows注册表批量删除

卸载软件后,有时仍会在注册表中留下带有相同关键词的残留项。下面使用 PowerShell 先查找并导出注册表项,再在确认列表后备份并删除。

注册表操作可能导致系统或软件异常。请只删除确定属于目标软件的项,并在操作前创建系统还原点或其他可靠备份。脚本默认先预览、再备份、最后删除,不建议跳过确认步骤。

1. 查找注册表项

下面的脚本只搜索键名,不会删除内容。它会同时尝试 64 位和 32 位注册表视图,并将结果保存为 TXT 和 CSV 文件。

保存为 Find-WincompressRegistryKeys.ps1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
param(
    [string]$Keyword = 'wincompress',
    [string]$OutTxt = ".\wincompress_registry_keys.txt",
    [string]$OutCsv = ".\wincompress_registry_keys.csv"
)

$IsAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(
    [Security.Principal.WindowsBuiltInRole]::Administrator
)
if (-not $IsAdmin) { Write-Warning "Run PowerShell as Administrator to reduce missed keys." }

$Is64OS = [Environment]::Is64BitOperatingSystem
$roots = @('HKLM','HKCU','HKCR','HKU','HKCC')
$results = New-Object System.Collections.Generic.List[string]

function Search-Keys {
    param([string]$Root, [string]$Keyword, [string]$RegView)
    $args = @('query', $Root, '/f', $Keyword, '/k', '/s')
    if ($RegView) { $args += "/reg:$RegView" }
    $out = & reg.exe @args 2>$null
    if ($LASTEXITCODE -eq 0 -and $out) {
        foreach ($line in $out) {
            if ($line -match '^HKEY_') { $results.Add($line.Trim()) }
        }
    }
}

foreach ($root in $roots) {
    Search-Keys -Root $root -Keyword $Keyword -RegView '64'
    if ($Is64OS) { Search-Keys -Root $root -Keyword $Keyword -RegView '32' }
}

$uniq = $results | Sort-Object -Unique
if ($uniq.Count -eq 0) {
    Write-Host "No registry key name contains '$Keyword'."
} else {
    $uniq | Set-Content -Path $OutTxt -Encoding UTF8
    $uniq | ForEach-Object { [pscustomobject]@{ RegistryKey = $_ } } |
        Export-Csv -NoTypeInformation -Encoding UTF8 -Path $OutCsv
    Write-Host "Found $($uniq.Count) keys."
    Write-Host "Output files:`n$OutTxt`n$OutCsv"
}

2. 备份并删除

确认列表中的每一项都可以删除后,保存下面的脚本为 Remove-WincompressRegistryKeys.ps1。脚本默认导出 .reg 备份,并支持 -WhatIf 预演。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
[CmdletBinding(SupportsShouldProcess=$true)]
param(
    [Parameter(Mandatory=$true)]
    [string]$InputList,
    [string]$BackupDir = ".\Backup_wincompress",
    [switch]$NoBackup
)

if (-not (Test-Path -LiteralPath $InputList)) { throw "List not found: $InputList" }
if (-not $NoBackup) { New-Item -ItemType Directory -Force -Path $BackupDir | Out-Null }

$Is64OS = [Environment]::Is64BitOperatingSystem
$stamp = Get-Date -Format "yyyyMMdd_HHmmss"
$log = ".\remove_wincompress_$stamp.log"
"Start: $(Get-Date)" | Out-File -FilePath $log -Encoding UTF8

$keys = Get-Content -LiteralPath $InputList |
    Where-Object { $_ -and ($_ -match '^HKEY_') } |
    ForEach-Object { $_.Trim() } |
    Sort-Object -Unique

function SafeName([string]$path) { return ($path -replace '[\\/:*?""<>|]','_') }

$success = 0
$failed = 0
foreach ($key in $keys) {
    $base = Join-Path $BackupDir (SafeName $key)

    if (-not $NoBackup) {
        & reg.exe export "$key" "$base.x64.reg" /y /reg:64 2>$null | Out-Null
        if ($LASTEXITCODE -ne 0) { "Backup FAIL (x64): $key" | Tee-Object -FilePath $log -Append | Out-Null }
        if ($Is64OS) {
            & reg.exe export "$key" "$base.x86.reg" /y /reg:32 2>$null | Out-Null
            if ($LASTEXITCODE -ne 0) { "Backup FAIL (x86): $key" | Tee-Object -FilePath $log -Append | Out-Null }
        }
    }

    if ($PSCmdlet.ShouldProcess("$key [x64]", 'Delete')) {
        & reg.exe delete "$key" /f /reg:64 2>$null | Out-Null
        if ($LASTEXITCODE -eq 0) { $success++; "Delete OK (x64): $key" | Tee-Object -FilePath $log -Append | Out-Null }
        else { $failed++; "Delete FAIL (x64): $key" | Tee-Object -FilePath $log -Append | Out-Null }
    }
    if ($Is64OS -and $PSCmdlet.ShouldProcess("$key [x86]", 'Delete')) {
        & reg.exe delete "$key" /f /reg:32 2>$null | Out-Null
        if ($LASTEXITCODE -eq 0) { $success++; "Delete OK (x86): $key" | Tee-Object -FilePath $log -Append | Out-Null }
        else { $failed++; "Delete FAIL (x86): $key" | Tee-Object -FilePath $log -Append | Out-Null }
    }
}

"End: $(Get-Date)" | Out-File -FilePath $log -Append -Encoding UTF8
"Summary: success=$success, failed=$failed, see log: $log" | Write-Host

3. 使用方法

  1. 以管理员身份打开 PowerShell,并在本次会话中允许脚本运行:

    1
    
    Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
    
  2. 运行查找脚本,把结果输出到桌面:

    1
    2
    3
    
    .\Find-WincompressRegistryKeys.ps1 -Keyword 'wincompress' `
      -OutTxt "$env:USERPROFILE\Desktop\wincompress_keys.txt" `
      -OutCsv "$env:USERPROFILE\Desktop\wincompress_keys.csv"
    
  3. 打开 TXT 或 CSV 文件,确认所有键都属于要卸载的软件。

  4. 先进行预演,不会真正删除:

    1
    2
    
    .\Remove-WincompressRegistryKeys.ps1 `
      -InputList "$env:USERPROFILE\Desktop\wincompress_keys.txt" -WhatIf
    
  5. 确认无误后执行删除。默认会把两个注册表视图的备份保存到指定目录:

    1
    2
    3
    
    .\Remove-WincompressRegistryKeys.ps1 `
      -InputList "$env:USERPROFILE\Desktop\wincompress_keys.txt" `
      -BackupDir "$env:USERPROFILE\Desktop\Backup_wincompress"
    

脚本文件建议保存为 ASCII 或 UTF-8(无 BOM),以兼容旧版 Windows PowerShell。PowerShell 7 通常可以直接使用 UTF-8 文件。

4. 需要 SYSTEM 权限时

如果管理员权限仍不足,可以使用 Windows 自带的任务计划程序,以 SYSTEM 身份运行删除脚本。将下面的包装脚本与 Remove-WincompressRegistryKeys.ps1 放在同一目录,保存为 Invoke-RemoveWincompressAsSystem.ps1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
[CmdletBinding()]
param(
    [Parameter(Mandatory=$true)] [string]$InputList,
    [string]$BackupDir = "$env:USERPROFILE\Desktop\Backup_wincompress",
    [switch]$NoBackup
)

function Q([string]$s) { '"' + $s.Replace('"','""') + '"' }

$removeScript = Join-Path $PSScriptRoot 'Remove-WincompressRegistryKeys.ps1'
if (-not (Test-Path $removeScript)) { throw "Missing removal script." }
if (-not (Test-Path $InputList)) { throw "Input list not found." }
if (-not (Test-Path $BackupDir)) { New-Item -ItemType Directory -Force $BackupDir | Out-Null }

$ps = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"
$tokens = @('-NoProfile','-ExecutionPolicy','Bypass','-File',(Resolve-Path $removeScript).Path,
    '-InputList',(Resolve-Path $InputList).Path,'-BackupDir',(Resolve-Path $BackupDir).Path)
if ($NoBackup) { $tokens += '-NoBackup' }
$argString = ($tokens | ForEach-Object { if ($_ -like '-*') { $_ } else { Q $_ } }) -join ' '

$service = New-Object -ComObject 'Schedule.Service'
$service.Connect()
$root = $service.GetFolder('\')
$taskDefinition = $service.NewTask(0)
$taskDefinition.Principal.UserId = 'SYSTEM'
$taskDefinition.Principal.LogonType = 5
$taskDefinition.Principal.RunLevel = 1
$taskDefinition.Settings.Hidden = $true
$taskDefinition.Settings.AllowDemandStart = $true

$action = $taskDefinition.Actions.Create(0)
$action.Path = $ps
$action.Arguments = $argString
$action.WorkingDirectory = Split-Path -Parent (Resolve-Path $removeScript).Path

$name = 'Temp_RemoveWincompress_' + [Guid]::NewGuid().ToString('N')
$null = $root.RegisterTaskDefinition("\$name", $taskDefinition, 6, $null, $null, 5, $null)
$task = $root.GetTask("\$name")
$running = $task.Run($null)

$deadline = (Get-Date).AddMinutes(30)
while ((Get-Date) -lt $deadline) {
    try { $state = $running.State } catch { break }
    if ($state -eq 4 -or $state -eq 2 -or $state -eq 3) { Start-Sleep -Seconds 2 } else { break }
}
try { $root.DeleteTask("\$name", 0) } catch {}

以管理员身份运行包装脚本即可。若仍无法删除,注册表项可能由 TrustedInstaller 所有;不要直接强删系统组件,先确认来源并保留完整备份。也可以使用 Sysinternals PsExec 等工具以 SYSTEM 身份运行,但第三方工具需要自行核验来源和风险。

5. 恢复备份

需要恢复某个注册表项时,可以双击对应的 .reg 文件,或在管理员 PowerShell 中执行:

1
reg.exe import "备份文件路径.reg"