PowerShell+注册表:一键导出完整的Windows软件清单
以前帮人处理电脑问题最烦的一件事就是看软件列表。打开“控制面板 - 卸载程序”一个个找名字、对比版本号偶尔还会遇到列表不完整、控制面板卡半天弹不出来的情况。后来要给一批电脑做软件资产盘点挨个开控制面板截图根本不现实。研究了一圈才发现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 {NName;E{$_.DisplayName}}, {NVersion;E{$_.DisplayVersion}}, {NPublisher;E{$_.Publisher}}, {NInstallDate;E{$_.InstallDate}}, {NSizeKB;E{$_.EstimatedSize}}, {NLocation;E{$_.InstallLocation}}, {NUninstall;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需要换算成 MBif ($_.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, {NInstallDate;E{ if ($_.InstallDate -match ^\d{8}$) { [datetime]::ParseExact($_.InstallDate,yyyyMMdd,$null) } }}, {NSizeMB;E{ if ($_.EstimatedSize) { [math]::Round($_.EstimatedSize/1024,2) } }}, QuietUninstallString, UninstallString | Sort-Object InstallDate -Descending | Format-Table -AutoSize如果要做资产管理就把它加导出 CSV 和远程收集。如果只是应急看一台机跑完 Format-Table 就够了。这些内容基本覆盖了 Windows 软件安装信息获取的绝大多数场景。几次批量盘点操作下来这条注册表路线是最稳、最快、信息最全的至少到目前为止我没找到比它更靠谱的替代方案。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →