You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PowerShell清理Windows旧用户脚本卡顿问题排查求助

问题背景

我需要编写一款可在企业电脑运行的PowerShell脚本,实现Windows旧用户配置文件清理功能,具体需求包括:

  • 获取本地用户列表
  • 排除指定系统/业务账户
  • 删除90天未登录用户的配置文件(需同时清理对应注册表项)

我对PowerShell不太熟悉,先后写了初始版本和更新版本的脚本,但不清楚脚本运行卡顿的原因,希望帮忙排查。


初始脚本

$User = get-childitem -Path C:\Users -Name
#Current Date for comparison
$Cutoff = (Get-Date).AddDays(-90)
#Cut Public and Administrator accounts from the list
$Userlist = @()
foreach ($User in $Users) {
    if ($User -in "Public", "Administrator", "administrator", ".Net v4.5", ".Net v4.5 classic", ".Default", "itadmin", "it-admin", "cpsi") {
        # Skip the user if their name is in the exclusion list
        continue
    }

    try {
        $Userlist.Add($User)
    }
    catch {
        Write-Host "Could not add the user $User to the Userlist."
    }
}
#Gather all Login events in the $events variable
$Events = Get-WinEvent -FilterHashtable @{ LogName='Security'; Id='4624'; StartTime=(Get-Date).AddDays(-90)}
$LogList = @{}
$user -lt ($cutoff) 
#Loop through the previously gathered list is users
foreach ($User in $Userlist){
#Filter the Events for ones performed by the user and store it in $Logins
$Logins = $Events| Where {$_.Message -like "*$User*" -and $_.Message -like "*@tchospital.local*"}
#Output line ($Logins | Measure).Count will output the number of times the user login during the currently available security logs
$LoginCount = ($Logins | Measure).Count
if($LoginCount -eq 0){
$RemovedUserList =@($RemovedUserList + $User)
$UserFolder = "C:\Users\$User"
Get-WmiObject -Class Win32_UserProfile | Where LocalPath -eq $UserFolder | Remove-WmiObject}
else{
continue
}
}
$CutOut = Write-Output "Cutoff date is $Cutoff"
$UserOut = Write-Output "Found users are: $Userlist"
$Today = Get-Date
$RunDate= Write-Output "Rundate is: $Today"
if (!($RemovedUserList)){
$RemovedUser = Write-Output "No one has been removed"
}
else{
$RemovedUser = Write-Output "$RemovedUserList has been removed"
}

更新后脚本

# Get a list of user folders in C:\Users
$UserNames = Get-ChildItem -Path C:\Users -Name

# Set the cutoff date to 90 days ago
$Cutoff = (Get-Date).AddDays(-90)

# Initialize user lists
$Userlist = @()
$RemovedUserList = @()

# Loop through each user folder
foreach ($UserName in $UserNames) {
    # List of excluded user names
    $ExcludedUsers = @("Public", "Administrator", "administrator", ".Net v4.5", ".Net v4.5 classic", ".Default", "itadmin", "it-admin", "cpsi")

    # Check if the current user name is in the excluded list
    if ($UserName -in $ExcludedUsers) {
        Write-Host "Excluded user: $UserName"
        continue
    }

    # Add the user to the $Userlist if it's not excluded
    $Userlist += $UserName
}

# Get security events for logins in the last 90 days
$Events = Get-WinEvent -FilterHashtable @{ LogName='Security'; Id='4624'; StartTime=$Cutoff }

# Loop through each user in the $Userlist
foreach ($UserName in $Userlist) {
    $Logins = $Events | Where-Object { $_.Message -like "*$UserName*" -and $_.Message -like "*@tchospital.local*" }
    $LoginCount = ($Logins | Measure-Object).Count

    if ($LoginCount -eq 0) {
        $RemovedUserList += $UserName
        $UserFolder = "C:\Users\$UserName"

        try {
            Get-WmiObject -Class Win32_UserProfile | Where-Object LocalPath -eq $UserFolder | Remove-WmiObject -ErrorAction Stop
            Write-Host "Removed user profile for: $UserName"
        }
        catch {
            Write-Host "Failed to remove user profile for: $UserName"
            Write-Host "Error: $_"
        }
    }
}

# Output the results to files in the root of the C drive
$Cutoff | Out-File -Append -FilePath "C:\cutoff.txt"
$Userlist | Out-File -Append -FilePath "C:\userlist.txt"
$Today = Get-Date
$Today | Out-File -Append -FilePath "C:\runlog.txt"

if ($RemovedUserList.Count -eq 0) {
    "No users were removed" | Out-File -Append -FilePath "C:\runlog.txt"
}
else {
    "Skipped users: $($RemovedUserList -join ', ')" | Out-File -Append -FilePath "C:\runlog.txt"
}

卡顿原因排查

1. 安全事件处理效率极低

Get-WinEvent获取的90天内4624登录事件量可能极大,后续循环每个用户都要全量遍历事件集,用-like做全文模糊匹配:

  • 用户数量越多,重复扫描事件的次数越多
  • 模糊匹配Message字段是性能黑洞,事件量上万时卡顿会非常明显

2. WMI查询重复执行

每次删除用户时都调用Get-WmiObject -Class Win32_UserProfile全量查询配置文件,再过滤路径,重复调用会浪费大量系统资源。

3. 数组拼接方式低效

用$Userlist += $UserName和$RemovedUserList += $UserName拼接数组,每次都会创建新数组,用户数量多的时候会累积性能开销。

4. 初始脚本的语法错误

初始脚本存在变量名写错($User = get-childitem...却循环$Users)、无意义代码($user -lt ($cutoff))等问题,虽不直接导致卡顿,但会引发逻辑错误和不必要的处理。


优化后的脚本
# 配置参数
$ExcludedUsers = @("Public", "Administrator", ".Net v4.5", ".Net v4.5 classic", ".Default", "itadmin", "it-admin", "cpsi")
$Cutoff = (Get-Date).AddDays(-90)
$DomainSuffix = "@tchospital.local"

# 获取C:\Users下的用户文件夹,排除指定账户(统一转小写避免大小写问题)
$Userlist = Get-ChildItem -Path C:\Users -Name | Where-Object {
    $lowerName = $_.ToLower()
    !($ExcludedUsers | Where-Object { $_.ToLower() -eq $lowerName })
}

# 预加载所有用户配置文件,避免重复查询
$allUserProfiles = Get-WmiObject -Class Win32_UserProfile

# 高效处理登录事件:提取事件中用户名,构建哈希表存储最近登录时间
$loginEvents = Get-WinEvent -FilterHashtable @{
    LogName='Security'
    Id='4624'
    StartTime=$Cutoff
} -ErrorAction SilentlyContinue

# 构建用户最近登录时间哈希表(只保留目标域用户)
$userLastLogin = @{}
foreach ($event in $loginEvents) {
    # 直接从事件Properties提取用户名,比解析Message快
    $username = $event.Properties[5].Value
    if ($username.EndsWith($DomainSuffix)) {
        $userShortName = $username.Replace($DomainSuffix, "")
        $eventTime = $event.TimeCreated
        # 只保留最新的登录时间
        if (-not $userLastLogin.ContainsKey($userShortName) -or $eventTime -gt $userLastLogin[$userShortName]) {
            $userLastLogin[$userShortName] = $eventTime
        }
    }
}

# 遍历用户列表,处理未登录用户
$RemovedUserList = @()
foreach ($userName in $Userlist) {
    # 检查用户是否有90天内的登录记录
    if (-not $userLastLogin.ContainsKey($userName)) {
        $RemovedUserList += $userName
        $userFolder = "C:\Users\$userName"
        
        # 从预加载的配置文件中查找并删除
        $profileToRemove = $allUserProfiles | Where-Object { $_.LocalPath -eq $userFolder }
        if ($profileToRemove) {
            try {
                $profileToRemove.Remove($true) # $true表示同时删除注册表和文件夹
                Write-Host "成功删除用户配置文件: $userName"
            }
            catch {
                Write-Host "删除用户配置文件失败: $userName,错误信息: $_"
            }
        }
        else {
            Write-Host "未找到用户$userName的配置文件记录"
        }
    }
}

# 写入统一日志
$logPath = "C:\ProfileCleanupLog.txt"
"=== 清理日志 - $(Get-Date) ===" | Out-File -FilePath $logPath -Append
"截止日期: $Cutoff" | Out-File -FilePath $logPath -Append
"扫描用户列表: $($Userlist -join ', ')" | Out-File -FilePath $logPath -Append
if ($RemovedUserList.Count -eq 0) {
    "本次清理无用户被移除" | Out-File -FilePath $logPath -Append
}
else {
    "被移除用户: $($RemovedUserList -join ', ')" | Out-File -FilePath $logPath -Append
}
"============================" | Out-File -FilePath $logPath -Append

优化点说明

  • 事件处理优化:一次性提取事件中的用户名,用哈希表存储最新登录时间,避免多次遍历事件集
  • WMI预加载:只查询一次所有用户配置文件,后续直接过滤,减少WMI调用次数
  • 用户名匹配优化:统一转小写避免大小写问题,直接用事件Properties提取用户名,比解析Message快数倍
  • 删除逻辑优化:用Win32_UserProfile的Remove($true)方法,自动删除文件夹和注册表项,无需单独处理

内容的提问来源于stack exchange,提问作者Sloan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 17:04:54