以前帮人处理电脑问题,最烦的一件事就是看软件列表。打开“控制面板 -> 卸载程序”,一个个找名字、对比版本号,偶尔还会遇到列表不完整、控制面板卡半天弹不出来的情况。后来要给一批电脑做软件资产盘点,挨个开控制面板截图根本不现实。研究了一圈才发现,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 安装的软件,卸载键里只有DisplayName、UninstallString等几个值,查不到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 -AutoSize6.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 软件安装信息获取的绝大多数场景。几次批量盘点操作下来,这条注册表路线是最稳、最快、信息最全的,至少到目前为止,我没找到比它更靠谱的替代方案。