#requires -version 5.1 <# .SYNOPSIS Active Directory のユーザーアカウント一覧を Excel (.xlsx) でデスクトップに出力します。 .DESCRIPTION Excel / ImportExcel モジュール / AD PowerShell モジュールは不要です。 Windows PowerShell 5.1 と、ドメインを照会できる Windows 環境で動作します。 C列は「姓名(姓 + 半角スペース + 名)」または「表示名」を選べます。 .EXAMPLE .\Export-ADUsersToExcel.ps1 .EXAMPLE .\Export-ADUsersToExcel.ps1 -NameMode Seimei .EXAMPLE .\Export-ADUsersToExcel.ps1 -NameMode DisplayName -Server dc02.example.local #> [CmdletBinding()] param( [ValidateSet('Seimei', 'DisplayName')] [string] $NameMode, # 接続先のドメインコントローラー。省略時は現在のドメインを利用。 [string] $Server ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' if ([string]::IsNullOrWhiteSpace($NameMode)) { Write-Host '' Write-Host 'C列に出力する項目を選んでください。' Write-Host ' 1:姓名(姓 + 半角スペース + 名)' Write-Host ' 2:表示名' do { $choice = (Read-Host '番号を入力(1 または 2)').Trim() } until ($choice -eq '1' -or $choice -eq '2') $NameMode = if ($choice -eq '1') { 'Seimei' } else { 'DisplayName' } } # 必要な機能は Windows / .NET の標準機能のみ。 Add-Type -AssemblyName System.DirectoryServices Add-Type -AssemblyName System.IO.Compression function Get-LdapValue { param( [System.DirectoryServices.ResultPropertyCollection] $Properties, [string] $Key ) $valueCollection = $Properties[$Key] if ($null -ne $valueCollection -and $valueCollection.Count -gt 0) { return [string] $valueCollection[0] } return '' } # ExcelのセルはXMLの文字列として格納するため、無効な制御文字だけ置換。 function Convert-ToSafeXmlText { param([AllowNull()][string] $Value) if ($null -eq $Value) { return '' } return [regex]::Replace($Value, '[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ') } function Add-ZipText { param( [System.IO.Compression.ZipArchive] $Zip, [string] $Path, [string] $Content ) $entry = $Zip.CreateEntry($Path, [System.IO.Compression.CompressionLevel]::Optimal) $stream = $entry.Open() $writer = [System.IO.StreamWriter]::new( $stream, [System.Text.UTF8Encoding]::new($false) ) try { $writer.Write($Content) } finally { $writer.Dispose() } } function Write-TextCell { param( [System.Xml.XmlWriter] $Writer, [string] $SheetNamespace, [string] $CellAddress, [AllowNull()][string] $Value, [int] $StyleId = 0 ) $Writer.WriteStartElement('c', $SheetNamespace) $Writer.WriteAttributeString('r', $CellAddress) $Writer.WriteAttributeString('t', 'inlineStr') if ($StyleId -ne 0) { $Writer.WriteAttributeString('s', [string] $StyleId) } $Writer.WriteStartElement('is', $SheetNamespace) $Writer.WriteStartElement('t', $SheetNamespace) # 文字列が数式と解釈されることはありません。 $Writer.WriteString((Convert-ToSafeXmlText $Value)) $Writer.WriteEndElement() # t $Writer.WriteEndElement() # is $Writer.WriteEndElement() # c } # ドメインのユーザーを全件取得(無効アカウントも対象)。 $rootDsePath = if ($Server) { "LDAP://$Server/RootDSE" } else { 'LDAP://RootDSE' } $rootDse = [System.DirectoryServices.DirectoryEntry]::new($rootDsePath) $searchRoot = $null $searcher = $null $results = $null try { $domainDn = [string] $rootDse.Properties['defaultNamingContext'].Value if ([string]::IsNullOrWhiteSpace($domainDn)) { throw 'ドメインの情報を取得できません。ADに接続できる環境で実行してください。' } $searchRootPath = if ($Server) { "LDAP://$Server/$domainDn" } else { "LDAP://$domainDn" } $searchRoot = [System.DirectoryServices.DirectoryEntry]::new($searchRootPath) $searcher = [System.DirectoryServices.DirectorySearcher]::new($searchRoot) $searcher.Filter = '(&(objectCategory=person)(objectClass=user)(sAMAccountName=*))' $searcher.SearchScope = [System.DirectoryServices.SearchScope]::Subtree $searcher.PageSize = 1000 # 1000件を超えるドメインにも対応 foreach ($property in @('sAMAccountName', 'sn', 'givenName', 'displayName', 'description')) { [void] $searcher.PropertiesToLoad.Add($property) } Write-Host 'AD のアカウント情報を取得しています...' $results = $searcher.FindAll() $users = @( foreach ($result in $results) { $props = $result.Properties [pscustomobject]@{ AccountName = Get-LdapValue $props 'samaccountname' Surname = Get-LdapValue $props 'sn' GivenName = Get-LdapValue $props 'givenname' DisplayName = Get-LdapValue $props 'displayname' Description = Get-LdapValue $props 'description' } } ) } finally { if ($null -ne $results) { $results.Dispose() } if ($null -ne $searcher) { $searcher.Dispose() } if ($null -ne $searchRoot) { $searchRoot.Dispose() } $rootDse.Dispose() } $users = @($users | Sort-Object -Property AccountName) if ($users.Count -gt 1048575) { throw 'Excelの1シートあたりの上限(見出しを除き1,048,575件)を超えました。' } $desktop = [Environment]::GetFolderPath([Environment+SpecialFolder]::DesktopDirectory) if ([string]::IsNullOrWhiteSpace($desktop)) { $desktop = Join-Path $env:USERPROFILE 'Desktop' } if (-not (Test-Path -LiteralPath $desktop -PathType Container)) { [void] (New-Item -ItemType Directory -Path $desktop -Force) } $stamp = Get-Date -Format 'yyyyMMdd_HHmmss' $outputPath = Join-Path $desktop "AD_Users_$stamp.xlsx" # 追加モジュールを使わず、Office Open XML形式の正規の.xlsxを作成。 $contentTypes = @' '@ $rootRelationships = @' '@ $workbookXml = @' '@ $workbookRelationships = @' '@ $stylesXml = @' '@ $archive = $null $fileStream = $null $sheetWriter = $null $finished = $false $sheetNs = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main' try { $fileStream = [System.IO.FileStream]::new( $outputPath, [System.IO.FileMode]::CreateNew, [System.IO.FileAccess]::Write ) $archive = [System.IO.Compression.ZipArchive]::new( $fileStream, [System.IO.Compression.ZipArchiveMode]::Create, $false ) Add-ZipText $archive '[Content_Types].xml' $contentTypes Add-ZipText $archive '_rels/.rels' $rootRelationships Add-ZipText $archive 'xl/workbook.xml' $workbookXml Add-ZipText $archive 'xl/_rels/workbook.xml.rels' $workbookRelationships Add-ZipText $archive 'xl/styles.xml' $stylesXml $sheetEntry = $archive.CreateEntry( 'xl/worksheets/sheet1.xml', [System.IO.Compression.CompressionLevel]::Optimal ) $settings = [System.Xml.XmlWriterSettings]::new() $settings.Encoding = [System.Text.UTF8Encoding]::new($false) $settings.CloseOutput = $true $sheetWriter = [System.Xml.XmlWriter]::Create($sheetEntry.Open(), $settings) $sheetWriter.WriteStartDocument() $sheetWriter.WriteStartElement('worksheet', $sheetNs) # 先頭行を固定。 $sheetWriter.WriteStartElement('sheetViews', $sheetNs) $sheetWriter.WriteStartElement('sheetView', $sheetNs) $sheetWriter.WriteAttributeString('workbookViewId', '0') $sheetWriter.WriteStartElement('pane', $sheetNs) $sheetWriter.WriteAttributeString('ySplit', '1') $sheetWriter.WriteAttributeString('topLeftCell', 'A2') $sheetWriter.WriteAttributeString('activePane', 'bottomLeft') $sheetWriter.WriteAttributeString('state', 'frozen') $sheetWriter.WriteEndElement() # pane $sheetWriter.WriteEndElement() # sheetView $sheetWriter.WriteEndElement() # sheetViews # 列幅: A=No. / B=アカウント / C=姓名or表示名 / D=説明。 $sheetWriter.WriteStartElement('cols', $sheetNs) $colWidths = @(9, 24, 30, 65) for ($idx = 0; $idx -lt 4; $idx++) { $sheetWriter.WriteStartElement('col', $sheetNs) $sheetWriter.WriteAttributeString('min', [string] ($idx + 1)) $sheetWriter.WriteAttributeString('max', [string] ($idx + 1)) $sheetWriter.WriteAttributeString('width', [string] $colWidths[$idx]) $sheetWriter.WriteAttributeString('customWidth', '1') $sheetWriter.WriteEndElement() } $sheetWriter.WriteEndElement() # cols $sheetWriter.WriteStartElement('sheetData', $sheetNs) $sheetWriter.WriteStartElement('row', $sheetNs) $sheetWriter.WriteAttributeString('r', '1') $colCHeader = if ($NameMode -eq 'Seimei') { '姓名' } else { '表示名' } Write-TextCell $sheetWriter $sheetNs 'A1' 'No.' 1 Write-TextCell $sheetWriter $sheetNs 'B1' 'アカウント名' 1 Write-TextCell $sheetWriter $sheetNs 'C1' $colCHeader 1 Write-TextCell $sheetWriter $sheetNs 'D1' '説明' 1 $sheetWriter.WriteEndElement() # row $number = 0 foreach ($user in $users) { $number++ $rowNum = $number + 1 if ($NameMode -eq 'Seimei') { $parts = @() if (-not [string]::IsNullOrWhiteSpace($user.Surname)) { $parts += $user.Surname.Trim() } if (-not [string]::IsNullOrWhiteSpace($user.GivenName)) { $parts += $user.GivenName.Trim() } $fullName = $parts -join ' ' # 半角スペース(U+0020) } else { $fullName = $user.DisplayName } $sheetWriter.WriteStartElement('row', $sheetNs) $sheetWriter.WriteAttributeString('r', [string] $rowNum) # A列は連番の数値として記録。 $sheetWriter.WriteStartElement('c', $sheetNs) $sheetWriter.WriteAttributeString('r', "A$rowNum") $sheetWriter.WriteStartElement('v', $sheetNs) $sheetWriter.WriteString([string] $number) $sheetWriter.WriteEndElement() # v $sheetWriter.WriteEndElement() # c Write-TextCell $sheetWriter $sheetNs "B$rowNum" $user.AccountName Write-TextCell $sheetWriter $sheetNs "C$rowNum" $fullName Write-TextCell $sheetWriter $sheetNs "D$rowNum" $user.Description $sheetWriter.WriteEndElement() # row } $sheetWriter.WriteEndElement() # sheetData # フィルターを追加。 $sheetWriter.WriteStartElement('autoFilter', $sheetNs) $sheetWriter.WriteAttributeString('ref', "A1:D$($users.Count + 1)") $sheetWriter.WriteEndElement() # autoFilter $sheetWriter.WriteEndElement() # worksheet $sheetWriter.WriteEndDocument() $sheetWriter.Dispose() $sheetWriter = $null $finished = $true } finally { if ($null -ne $sheetWriter) { $sheetWriter.Dispose() } if ($null -ne $archive) { $archive.Dispose() } if ($null -ne $fileStream) { $fileStream.Dispose() } if (-not $finished -and (Test-Path -LiteralPath $outputPath)) { Remove-Item -LiteralPath $outputPath -Force -ErrorAction SilentlyContinue } } Write-Host '' Write-Host "完了: $($users.Count) 件" -ForegroundColor Green Write-Host "保存先: $outputPath" -ForegroundColor Green Write-Host "C列: $(if ($NameMode -eq 'Seimei') { '姓名(姓 名)' } else { '表示名' })"