PowerShell+注册表:一键导出完整的Windows软件清单
2026/9/17 21:07:27 网站建设 项目流程

以前帮人处理电脑问题,最烦的一件事就是看软件列表。打开“控制面板 -> 卸载程序”,一个个找名字、对比版本号,偶尔还会遇到列表不完整、控制面板卡半天弹不出来的情况。后来要给一批电脑做软件资产盘点,挨个开控制面板截图根本不现实。研究了一圈才发现,Windows 所有已安装软件的详细信息,全部藏在注册表里,控制面板那层界面只是个“展示壳”。这篇文章就分享我整理 Windows 本地软件安装信息的完整思路:怎么读注册表、怎么做批量导出、怎么处理 32 位和 64 位软件混装时的坑,以及如何用 PowerShell 写一个对标“卸载程序控制面板”的软件清单工具。适合系统管理员、运维工程师、技术支持,以及想彻底搞懂自己电脑里到底装了什么的普通用户。

1. 控制面板的“卸载程序”界面到底在读什么

很多人以为控制面板里显示软件列表的是个什么独立数据库,实际完全不是。Windows 的“程序和功能”界面(也就是卸载程序控制面板)读取的就是注册表里几个特定的 Uninstall 键,把符合条件的子键读出来,翻译成一行行列表。搞懂这一点之后,你能解释很多日常见到的怪现象。

1.1 数据来源:注册表 Uninstall 键

真正存放已安装软件信息的,是以下几组注册表路径:

  • HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall\*
  • HKLM\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*
  • HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall\*

HKLM 表示“本机所有用户”级别的安装信息,HKCU 表示“当前用户”级别的安装信息。WOW6432Node 是 64 位 Windows 专门给 32 位软件预留的注册表视图位置。

每个子键代表一个软件。控制面板逐条显示的就是这些子键里某些带 DisplayName 的条目。我当初第一次手动打开注册表编辑器,顺着路径一层层点进去的时候,才恍然大悟——原来平时看得见摸得着的软件列表,底层就是这么朴素的数据结构。

1.2 常见误解:为什么控制面板列表经常“对不上”

一个常见问题是:明明我自己从网上下载的某个软件能正常运行,但控制面板里找不到它。这不一定是没装好,更可能是因为:

  • 软件是绿色版、便携版,没有写注册表 Uninstall 键。
  • 软件是 per-user 安装模式,写到了 HKCU 下面,另一个用户登录后就看不到。
  • 软件用了 MSIX/Appx 打包方式,走的是另一套管理机制,传统控制面板压根不展示。
  • 某些软件的卸载条目被清理工具误删了,或者被安全软件隔离了。

理解了数据来源,这些现象就都说得通了:控制面板不是通过扫描磁盘来找软件,而是纯粹查注册表。

1.3 三条读取路线的选型对比

读取软件信息有三条主流路线,我实际用下来各有优劣。

路线优点缺点适用场景
注册表直接读取速度快、信息最完整、无副作用需要自己拼路径、处理 32/64 位视图精确盘点、脚本全量导出
WMI 的 Win32_Product 查询查询写法简单速度极慢、会触发自动修复安装、影响性能基本不建议生产环境使用
PowerShell 的 CIM/Registry Provider语法优雅、可控性强、可远程需要 PowerShell 环境日常工作首选

顺带说一句,Win32_Product 这个类我踩过坑。网上很多教程让你直接用:

Get-WmiObject -Class Win32_Product

看着很省事,但实际跑起来会让人崩溃。这个类在查询时会对每个软件做一致性检查,大量软件会触发重新配置流程,轻则让机器卡半天,重则把某些软件的配置搞乱。我见过有朋友在一台装了上百个软件的电脑上跑这条命令,结果等了快 20 分钟,中间还有几个软件弹出了安装界面。这种查询方式后来我再也没在正式环境用过。

2. 最底层的注册表读取:64 位与 32 位视图的坑

既然注册表是源头,那第一步就是把注册表读明白。这中间最大的坑,就是 64 位 Windows 的双重视图机制。

2.1 WOW6432Node 重定向机制

64 位 Windows 为了兼容 32 位软件,在注册表里做了一个虚拟视图。32 位程序写入HKLM\Software\...的时候,系统会自动重定向到HKLM\Software\WOW6432Node\...。从 64 位程序的角度看,它读的才是真正的HKLM\Software\...

这个机制导致的结果是:一台 64 位 Windows 上,软件安装信息被拆成了两份。我的软件清单脚本必须同时读这两个位置,并且注意不要因为重复读取 32 位视图而错乱。如果只读一半,导出的清单就会缺很多软件,尤其是老牌国产软件和许多工具类小软件,它们有很大比例还是 32 位。

2.2 常用字段一览

控制面板上看到的那些列,比如名称、发布者、版本、安装日期、大小,其实都来自子键里的具体值。我整理了一份常用的字段表:

注册表值名称含义备注
DisplayName软件显示名称最重要的筛选条件
DisplayVersion软件版本号有些软件写的是空值
Publisher发布者/厂商不一定规范
InstallDate安装日期格式是 YYYYMMDD,需要转换
EstimatedSize软件占用大小单位是 KB,注意换算
InstallLocation安装位置很多软件不写
UninstallString卸载程序命令行可能有引号嵌套
QuietUninstallString静默卸载命令行有些软件没有
SystemComponent是否系统组件值为 1 时控制面板默认隐藏
ParentKeyName父组件标识用于判断是否为某个软件的补丁
ReleaseType发布类型常见值有 Windows Update、Hotfix 等
ModifyPath修复/修改程序路径控制面板的“更改”按钮读的就是它

2.3 用 reg query 快速验证数据

在写正式脚本之前,先用命令行手动验证一下注册表内部结构是非常必要的。打开 CMD,执行:

reg query "HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall"

你会看到一堆以 GUID 或软件名命名的子键。继续深入一个常见的键就能看到具体值:

reg query "HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall\7-Zip" /s

这样能直观地确认一个软件在注册表里到底写了哪些字段,哪些字段在你常见的软件里普遍存在,哪些只是个别软件自己加的。

2.4 读取权限边界

正常读这些注册表键是不需要管理员权限的,普通用户也能读到 HKLM 下面的 Uninstall 键。但是有少数软件会设置子键的权限,导致普通用户读不到某些值,这种情况属于少数。脚本跑出来的结果如果发现个别软件的字段缺失,可以先检查这一项是不是权限问题。我在做用户软件盘点时一般会用管理员权限跑脚本,既为了稳定读取,也方便同时读取其他用户的 HKCU 数据。

3. 推荐主力方案:PowerShell 脚本批量导出软件清单

用 PowerShell 遍历注册表,是个人觉得最可控、最稳的方案。既绕开了 Win32_Product 的性能灾难,又能拿到和控制面板一致甚至更全的字段。

3.1 核心遍历逻辑

思路很简单:把三个注册表位置分别枚举一遍,取所有子键,逐个读取字段,过滤掉没有 DisplayName 的键,然后输出对象。

直接上我目前在生产环境使用的脚本骨架:

$paths = @( "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*", "HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*", "HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" ) $softwareList = foreach ($path in $paths) { Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName } | Select-Object @{N='Name';E={$_.DisplayName}}, @{N='Version';E={$_.DisplayVersion}}, @{N='Publisher';E={$_.Publisher}}, @{N='InstallDate';E={$_.InstallDate}}, @{N='SizeKB';E={$_.EstimatedSize}}, @{N='Location';E={$_.InstallLocation}}, @{N='Uninstall';E={$_.UninstallString}} } $softwareList | Sort-Object Name | Format-Table -AutoSize

这段代码已经能输出比较完整的清单了。解释几个关键点:

  • -ErrorAction SilentlyContinue是必须加的。注册表里随时可能有不规范、字段类型异常的键,不加这个参数脚本很容易被个别垃圾键卡死。我实际跑的时候发现确实有某些软件卸载残留键缺少关键值,没有这个参数 PowerShell 会一并抛出大量红色报错。
  • Where-Object { $_.DisplayName }是筛选逻辑,去掉没有显示名称的键。如果不加,清单里会混入大量 Windows 更新补丁、系统驱动条目。当然,如果你就是想把所有条目都挖出来,这行可以去掉。
  • Select-Object里我把注册表值名映射成了更直观的自定义列名,这样输出更加可读,方便后续直接进表格工具处理。

3.2 安装日期与大小的单位换算

注册表里的InstallDate是 YYYYMMDD 格式,比如20250415就是 2025 年 4 月 15 日。这个格式直接看也能看懂,但如果要排序、筛选,最好转成标准日期:

$installDate = $_.InstallDate if ($installDate -match '^\d{8}$') { [datetime]::ParseExact($installDate, 'yyyyMMdd', $null) }

EstimatedSize 的单位是 KB,需要换算成 MB:

if ($_.EstimatedSize) { [math]::Round($_.EstimatedSize / 1024, 2) }

不过要提醒一下,EstimatedSize 这个值本身是软件卸载时系统根据文件大小估算的,很多软件根本不写,也有软件写了但明显偏小。它只能作为参考,别指望它 100% 准确。

3.3 导出 CSV:注意中文乱码问题

我最初用标准方式导出 CSV 后,用 Excel 打开时中文全部乱码。原因很简单:PowerShell 5.1 的Export-Csv默认编码格式是 UTF-8 带 BOM,而 Excel 有时候识别不到 BOM 就按 ANSI 解析了。

解决方法是导出时强制使用 UTF8 编码。但直接字符串不可靠,可靠的方法:

$softwareList | Export-Csv -Path "C:\Temp\software_list.csv" -Encoding UTF8 -NoTypeInformation

如果你在 PowerShell 7 或更高版本中运行,默认编码已经是 UTF-8,基本不会乱码。对于仍在使用 Windows PowerShell 5.1 的环境,注意加-Encoding UTF8

3.4 远程批量收集的方向

如果是给一批机器做软件资产盘点,要扩容到多台机器。我个人实际用的是两条路线:

  • 每台机器上放脚本,跑完生成 CSV,通过共享目录收回来。
  • 用 WinRM 远程执行,写成 Invoke-Command 脚本块,把结果一次性集送到管理中心。

第二条路线单独说:

$computers = @("PC01", "PC02", "PC03") $result = Invoke-Command -ComputerName $computers -ScriptBlock { $paths = @( "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*", "HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*", "HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" ) foreach ($path in $paths) { Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName } | Select-Object PSComputerName, DisplayName, DisplayVersion, Publisher } } $result | Export-Csv -Path "C:\Temp\all_pc_software.csv" -Encoding UTF8 -NoTypeInformation

注意远程执行时,如果目标机器 WinRM 服务没开,或者当前用户没有远程管理权限,Invoke-Command 会直接报错。部署前先随便找一台测试机验证下Test-WSMan -ComputerName PC01能不能通。

4. 对标控制面板的进阶字段补全

只拿到基础字段还不够。如果要做一个真正“对标控制面板”的工具,还需要额外处理几个细节。

4.1 UninstallString 和 QuietUninstallString

控制面板里对每个软件提供“卸载”和“更改”操作,底层读的就是 UninstallString、ModifyPath。但很多应用安装程序(比如 NSIS 打包的)卸载命令里带引号,直接通过命令行调用会失败。处理方式是先取到值,再用命令行解析工具或正则把可执行文件和参数拆开。

一个实用的办法是直接用 PowerShell 调用,而不是手动解析路径:

$uninst = $_.UninstallString if ($uninst) { # 使用 Start-Process 来执行,避免复杂引号导致解析混乱 Start-Process cmd.exe -ArgumentList "/c `"$uninst`"" }

注意这只能作为辅助操作,实际卸载往往还需要 UAC 提权。真正要静默卸载,应该优先找 QuietUninstallString,没有的话再尝试给 UninstallString 拼接/S/silent等参数。不同打包工具(MSI、NSIS、Inno Setup、InstallShield)的静默参数完全不同,我做过一个函数来识别类型并拼接参数,跑起来省心很多。

4.2 拿 GUID:区分同一软件的多版本共存

注册表子键的名称经常是{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}这种 GUID,也有直接是软件名。判断一个软件是否存在、是否重复安装,GUID 是最可靠的标识。

PowerShell 里把子键名取出来:

(Get-Item $path).PSChildName

在导出清单时,可以把 PSChildName 作为单独一列输出,方便后续做精确比对:比如同一台机器上装了两个版本的某软件,DisplayName 可能一样,但 GUID 不同。

4.3 现代应用(MSIX/Appx)的获取

传统控制面板不显示应用商店安装的现代应用,这是很多人的困惑点。如果你研究过就会知道,MSIX/Appx 程序的注册表位置和环境变量完全不同,不是传统的 Uninstall 键。

要获取这些应用列表,用:

Get-AppxPackage | Select-Object Name, PackageFullName, Version, InstallLocation Get-AppxProvisionedPackage -Online | Select-Object DisplayName, PackageName

这里有个需要留意的区别:Get-AppxPackage获取的是当前用户已安装的应用,Get-AppxProvisionedPackage获取的是系统预置的所有应用。如果你要全面对标“已安装软件”,应该两者结合来看。

4.4 过滤系统组件和 Windows 更新条目

控制面板默认隐藏了系统组件和部分 Windows 更新,但注册表里它们是存在的。过滤逻辑有三个关键字段:

  • SystemComponent为 1 的键:系统组件,通常不显示。
  • ParentKeyName存在且非空的键:多为驱动程序或者补丁子条目。
  • ReleaseType包含 Update、Hotfix、Security Update 的键:这些本质上是 Windows 更新条目,一般也不该出现在软件清单里。

用这三个条件综合过滤,能更好地和“卸载程序控制面板”默认展示的内容对齐。但如果你做的是安全审计,反而建议不要过滤得太干净,因为这些隐藏条目往往能暴露系统更新的真实状态。

5. 踩坑实录:为什么导出的清单经常“缺胳膊少腿”

这一节专门讲讲我在实际执行过程中反复踩过的坑,每一条都是真金白银换来的经验。

5.1 控制面板报错“注册表信息不完整或损坏”

热度词里有个高频问题:“由于其配置信息(注册表中的)不完整或已损坏”。这个提示我在多个场景下见到过。遇到这个报错时,首先需要定位是哪个 Uninstall 子键坏了。

排查思路是这样:逐个导出各子键的 DisplayName 和 UninstallString,对照列表人工筛选。我有一次遇到某个软件的卸载条目被人为删掉了 DisplayName 字段,只剩一个空壳 GUID,导致软件既不出现在列表里,也无法卸载。复现这个现象后的经验是:在处理软件列表时,对只有 UninstallString 但没有 DisplayName 的键要格外留意,必要时候手动补全或清理。

这类问题通常发生在卸载残留场景:安装程序先写入了 Uninstall 键,但卸载时没删干净;或者安全软件清掉了某些字段;或者是注册表权限被修改。遇到报错别急着乱删注册表,先把 Uninstall 键整个导出备份,再做最小范围的修复。

5.2 32 位和 64 位软件出现重复记录

同时读HKLM:\Software\...HKLM:\Software\WOW6432Node\...之后,偶尔会看到明明是同一个软件却出现在两份清单里。这不一定脚本逻辑错了,可能是 32 位软件和 64 位版本都装了,也可能是安装程序写注册表时自身写了两个键。

最简单的处理办法是按 DisplayName + DisplayVersion 分组去重:

$softwareList | Sort-Object Name, Version -Unique

但真实环境中去重标准可以更复杂。我在一次企业软件盘点中,发现某一个安全客户端会在两个位置各写一份 Uninstall 键且版本号写法不同,用 Name 去重并不彻底。后来加了 GUID 对比,再人工审计一次,才彻底解决。

5.3 脚本运行时突然闪退

热词里反复出现“Windows 脚本命令闪退”这个搜索词。脚本闪退最常见的原因是 PowerShell 执行策略限制:

Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass

这一步只需要在当前进程中生效,不需要动系统策略,是最稳妥的做法。另外还有可能是脚本本身抛了未处理的终止性错误,比如路径不存在、注册表键值类型异常。所有读取操作都加上-ErrorAction SilentlyContinue,可以大幅降低闪退概率。

5.4 导出的 CSV 用 Excel 打开乱码

前面提到过编码问题,这里再补充完整。PowerShell 5.1 环境加了-Encoding UTF8之后,Excel 能正常识别,但某些老版本的表格软件(特别是 WPS 的低版本)仍有概率识别错乱。更保险的方案是生成 CSV 时把编码指定为GB2312

$content = $softwareList | ConvertTo-Csv -NoTypeInformation $content | Out-File -FilePath "C:\Temp\software_list.csv" -Encoding Default

但这样做的坏处是跨系统兼容性差。我的最终建议是:日常管理用 UTF8,必须给他人发文件时再根据对方环境调整。

5.5 MSI 安装的软件在注册表里字段不完整

很多通过 Windows Installer 安装的软件,卸载键里只有DisplayNameUninstallString等几个值,查不到InstallLocation。这是正常现象,不代表数据有问题。MSI 软件的真实安装目录记录在另一个数据库里:

Get-CimInstance -ClassName Win32_Product | Select-Object Name, InstallLocation, InstallDate

又回到 Win32_Product 了,但请注意:这里是配套使用而非之前说的全量遍历。如果你只需要查某几个 MSI 软件的位置,是可以接受的,但如果要扫描全量软件,依旧建议回到注册表方案。

6. 把脚本扩展成“半个控制面板”的实用小工具

有了前面的基础字段、过滤规则、去重逻辑,其实已经可以做一个对标控制面板的工具了。我自己把它封装成了一个带菜单的 PowerShell 脚本,执行后可以选交互式查看清单、导出 CSV、定位某个软件的卸载命令,或者远程批量采集。

6.1 函数封装:读取与过滤拆开

写脚本时建议把读取和过滤拆成两个函数。读取函数负责扫描注册表,返回所有原始对象;过滤函数负责剔除系统组件和空条目。这样做的最大好处是,后续你改动过滤规则时不需要动读取逻辑,维护成本低很多。

function Get-InstalledSoftware { param([switch]$IncludeSystemComponents) $paths = @( "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*", "HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*", "HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" ) $result = foreach ($path in $paths) { Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName } } if (-not $IncludeSystemComponents) { $result = $result | Where-Object { -not $_.SystemComponent -and -not $_.ParentKeyName -and $_.ReleaseType -notmatch 'Update|Hotfix' } } return $result }

6.2 交互式查看:按发布者筛选、按安装日期排序

对着一大坨 CSV 看始终不直观,写一个轻量交互子命令会舒服很多:

Get-InstalledSoftware | Where-Object { $_.Publisher -match "Microsoft" } | Sort-Object InstallDate -Descending | Format-Table Name, Version, InstallDate -AutoSize

6.3 给 CSM/SCCM 类工具喂数据

如果公司有 SCCM 或类似资产管理工具,不一定非要用 PowerShell 自建。但 PowerShell 的优势在于:可以在不依赖服务器、不需要目标机器安装 Agent 的情况下快速收集清单。对有临时盘点需求、想快速得到结果的场景,脚本是效率最高的方案。

6.4 输出 HTML 报告:和 Excel 说再见

我想更直观展示的时候,还会把清单输出成 HTML 报告,用颜色标出未设置发布者的条目,标记出可疑的安装目录。这个方法在交付给业务部门时特别有用。把 CSV/对象转表格直接生成 HTML:

$html = $softwareList | Select-Object Name, Version, Publisher, InstallDate | ConvertTo-Html -Fragment $html | Out-File "C:\Temp\software_report.html" -Encoding UTF8

配合简单的 CSS,视觉效果直接接近专业软件的界面。

7. 最后提醒几个容易忽略的底层细节

写这类脚本很依赖对 Windows 基础机制的理解,几个细节容易忽略,单独列一提。

7.1 注册表键值类型并不统一

同一个 DisplayName,有的软件存的是字符串,有的存的是展开字符串,还有少数存储损坏的键会直接报错。脚本必须容忍这些异常,不能假设所有键值类型一致。

7.2 某些软件的安装条目是动态生成的

部分软件的卸载键并不是安装时装上去的,而是在第一次启动时才写。比如一些从压缩包自解压运行的软件,首次运行后才会生成真正的卸载项。这类软件你扫注册表时能否看到,完全取决于有没有运行过。

7.3 “静默卸载”不是万能钥匙

很多人试图通过注册表拿到卸载命令后直接静默卸载,但我必须说:不同打包工具的静默参数差异极大,而且很多商业软件会在静默卸载时弹出交互确认框。把“完整软件清单”当成“可自动化卸载清单”是一个常见误区。我建议脚本里把 UninstallString、QuietUninstallString 都输出,方便人工判断,而不是直接自动化执行。

7.4 注册表备份是底线

任何涉及注册表写入、删除的操作,操作前必须备份对应键。在扩展“对标控制面板”功能时,如果加入了“远程卸载”“更改安装”这类动作,一定要先用测试机做完整演练。推荐使用reg export命令或者注册表编辑器的“导出”功能,把 Uninstall 整个分支导出来存成一个 .reg 文件。

reg export "HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall" C:\backup\uninstall_backup.reg /y

这条命令几十秒就能跑完,但真到出问题时,能救命。

8. 我个人的最终推荐用法

如果你只是日常想搞清楚电脑里装了什么、哪个软件占空间大、哪个软件是 32 位,最简单实用的一条命令是:

Get-ItemProperty "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*", "HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*", "HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName } | Select-Object DisplayName, DisplayVersion, Publisher, @{N='InstallDate';E={ if ($_.InstallDate -match '^\d{8}$') { [datetime]::ParseExact($_.InstallDate,'yyyyMMdd',$null) } }}, @{N='SizeMB';E={ if ($_.EstimatedSize) { [math]::Round($_.EstimatedSize/1024,2) } }}, QuietUninstallString, UninstallString | Sort-Object InstallDate -Descending | Format-Table -AutoSize

如果要做资产管理,就把它加导出 CSV 和远程收集。如果只是应急看一台机,跑完 Format-Table 就够了。这些内容基本覆盖了 Windows 软件安装信息获取的绝大多数场景。几次批量盘点操作下来,这条注册表路线是最稳、最快、信息最全的,至少到目前为止,我没找到比它更靠谱的替代方案。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询