PowerShell 技能连载 - 替换文本中的指定字符

如果您只是需要替换文本中字符出现的所有位置,这是很简单的。以下可以将文本中所有的“l”变为大写:

"Hello World".Replace('l', 'L')

然而有些时候,您需要替换特定位置的某几个字符。我们假设您的文本是一个比特掩码,并且您需要对某些比特置位或清除。以上代码是不能用的,因为它一口气改变了所有的位:

PS> "110100011110110".Replace('1', '0')

000000000000000

而且您也不能通过索引来改变字符。您可以读取一个字符(例如检查某一个位是否为“1”),但您无法改变它的值:

PS> "110100011110110"[-1] -eq '1'
False

PS> "110100011110110"[-2] -eq '1'
True

PS> "110100011110110"[-2] = '0'
无法对 System.String 类型的对象进行索引。

要改变一个字符串中的某些字符,请将它转换为一个 StringBuilder

PS> $sb = New-Object System.Text.StringBuilder("1101100011110110")

PS> $sb[-1]
0

PS> $sb[-1] -eq '1'
False

PS> $sb[-2] -eq '1'
True

PS> $sb[-2] = '0'

PS> $sb[-2] -eq '1'
False

PS> $sb.ToString()
110100011110100

以下是将二进制转换为十进制格式的方法:

PS> $sb.ToString()
110100011110100

PS> [System.Convert]::ToInt64($sb.ToString(), 2)
26868
评论

用 PowerShell 脚本来导出美味书签

前言

美味书签Delicious 在线书签服务的中国本地化版本。由于各方面原因,美味书签实现的功能有限,远远达不到 Delicious 的功能。所以我希望将美味书签中的使用记录迁移回 Delicious。

经过一年使用,我在美味书签中已经积累了 5000+ 条书签记录。由于美味书签不支持书签导出功能,所以将美味书签中的书签导出至 Delicious 是一件需要动手动脑的事。幸好我们有 PowerShell 脚本,可以助我们完成这项单调枯燥的事。

这是一个系列文章,一共分为 3 部分:

  1. 用 PowerShell 脚本来导出美味书签
  2. 用 PowerShell 脚本来清除 Delicious 账户下的所有书签
  3. 用 PowerShell 脚本将书签批量导入 Delicious

原理分析

模拟美味书签的登录过程

美味书签的登录页面地址为 http://meiweisq.com/login 。我们可以使用 Invoke-WebRequest 获取登录页面,同时把会话信息记录到 $rb 变量中。

相应的 PowerShell 代码如下:

$response = Invoke-WebRequest -Uri $homeUrl -Method Default -SessionVariable rb -ContentType application/html

得到的响应中其中包含多个表单。通过查看网页源代码,我们可以确定 Action 为“/login”的那个表单是我们所要的:

相应的 PowerShell 代码为:

$loginForm = ($response.Forms | where { $_.Action -eq '/login' })[0]

我们在 Chrome 浏览器中登录一下,通过“开发者工具”的“Network”选项卡查看提交的数据:

根据提交的数据,我们可以编写 PowerShell 代码来提交表单,模拟登录按钮的动作。注意传入会话变量 $rb,以在后续的过程中保持会话身份,否则下次提交又会提示需要登录:

$loginForm.Fields['email'] = $email
$loginForm.Fields['password'] = $password
$loginForm.Fields['type'] = '登录'
$loginForm.Fields['return-url'] = '/home'
$loginForm.Fields['remember'] = 'off'
$response = Invoke-WebRequest -Uri $loginAction -WebSession $rb -Method POST -Body $loginForm

取得书签总数

在登录后的页面底部有“1 - 30 共 5126 个书签”的字样,其中 305126 两个数字是我们关心的。我们用正则表达式 1 - (\d+) 共 (\d+) 个书签 从整个网页中提取书签的总数量。在 PowerShell 使用正则表达式:

$response.Content -cmatch '1 - (\d+) 共 (\d+) 个书签'
$page1Count = $Matches[1]
$totalCount = $Matches[2]
echo "1 - $page1Count 共 $totalCount 个书签"

根据 $page1Count$totalCount,就可以计算总页数了:

$pageCount = [math]::Ceiling($totalCount / $bookmarksPerPage)

遍历每一页

知道了总页数,自然想到用 for 循环来遍历它们。我们观察每一页的规律,发现页码是通过 URL 的 page 参数指定的。我们用 PowerShell 来拼接 URL 字符串:

$uri = 'http://meiweisq.com/home?page=' + $page

对于每一页,继续用 Invoke-WebRequest 来获取它的内容:

$response = Invoke-WebRequest -Uri $uri -Method Default -WebSession $rb

分析书签

在每一页中,含有不超过 30 个书签,其中包含了书签的标题、URL、标签、时间等信息。

接下来是一些 DOM 的分析,需要一点耐心。我们先把它输出为 .html 文件,以便分析:

$response.Content > current_page.html

从 Chrome 的开发者工具中,可以观察到 DOM 的结构。和我们有关系的是 class 为 links、link、tags、tag这些元素。我们用 jQuery 的语法来表达它们,整理成一个表格如下:

选择器 含义
div.links 本页所有书签的集合
div.links > div.link 一个书签
div.links > div.link a.link-title 书签标题、URL
div.links > div.link a.link-time 时间
div.links > div.link ul.tags > tag 标签

请注意一下,在 Invoke-WebRequest 的结果(COM 对象)中做 DOM 查询,是有点慢的,不像 WEB 中的 jQuery 那么高效。在我们需要做一定的优化,以缩短大量的查询的总时间。我的优化原则如下:

  1. 能用 id 过滤的,不用 tag。
  2. 如果需要查询一个节点的子节点,则把前者保存到临时变量中,不要每次都从根对象(document)开始查询。

以下是 DOM 查询的相关代码:

$html = $response.ParsedHtml
$linksDiv = ($html.getElementsByTagName('div') | where { $_.classname -eq 'links' })[0]
$linksDiv.getElementsByTagName('div') | where { $_.classname -cmatch '\blink[\s,$]' }

$linkTitle = $div.getElementsByTagName('a') | where { $_.className -eq 'link-title' }
$title = $linkTitle | select -exp innerText
$url = $linkTitle | select -exp href

$linkTime = $div.getElementsByTagName('p') | where { $_.className -eq 'link-time' } | select -exp innerText

$ul = $div.getElementsByTagName('ul') | where { $_.className -cmatch '\btags[\s,$]' }
$tags = $ul.getElementsByTagName('a') | where { $_.className -cmatch 'tag' }
$tagNames = $tags | foreach { $_.getAttribute('tag') }

Javascript 的时间转换

美味书签的时间以 yyyy/MM/dd 的形式表达,而 Delicious 导入/导出文件的时间以 Javascript 格式表达。它们之间的转换方法是,前者减去1970年1月1日0时整的时间差,得到的总秒数,即得到其 Javascript 的格式表达。PowerShell 实现代码如下:

$jsTime = ([datetime]::ParseExact($_.LinkTime, 'yyyy/MM/dd', $null) - [datetime]'1970-01-01').TotalSeconds

输出

经过上面的步骤,我们已将所有的书签以 PSObject 的形式存放在 $bookmarks 数组中。现在可以随心所欲地将 $bookmarks 输出为我们所希望的格式了:

这是输出为 CSV 格式的代码:

$bookmarks | Export-Csv ("meiweisq-export-{0:yyyyMMdd}.csv" -f [datetime]::Now) -Encoding UTF8 -NoTypeInformation

这是输出到 GUI 界面的代码:

$bookmarks | Out-GridView

另外,我们可以导出为 Delicious 的专用格式。由于格式比较简单,我们就不用 ConvertTo-HTML 之类的函数了。

源代码

$email = 'victorwoo@gmail.com'
$password = 'xxx'

$homeUrl = 'http://meiweisq.com/home'
$loginAction = 'http://meiweisq.com/login'

$bookmarksPerPage = 30
$countPerExport = 10

function Get-DeliciousHtml($bookmarks) {
    $pre = @"
<!DOCTYPE NETSCAPE-Bookmark-file-1>
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
<!-- This is an automatically generated file.
It will be read and overwritten.
Do Not Edit! -->
<TITLE>Bookmarks</TITLE>
<H1>Bookmarks</H1>
<DL><p>

"@

    $post = @"
</DL><p>
"@

    $bookmarkTemplate = @"
<DT><A HREF="{0}" ADD_DATE="{1}" PRIVATE="{2}" TAGS="{3}">{4}</A>
<DD>{5}

"@
    $result = $pre
    $bookmarks | foreach {
        $jsTime = ([datetime]::ParseExact($_.LinkTime, 'yyyy/MM/dd', $null) - [datetime]'1970-01-01').TotalSeconds
        $tags = [string]::Join(',', $_.Tags -split ', ')
        $bookmarkString = $bookmarkTemplate -f $_.Url, $jsTime, 0, $tags, $_.Title, ''
        $result += $bookmarkString
    }
    $result += $post
    return $result
}

$startTime = [datetime]::Now

echo 'Requesting home'
$response = Invoke-WebRequest -Uri $homeUrl -Method Default -SessionVariable rb -ContentType application/html
if ($response.StatusCode -ne 200) {
    Write-Warning "[$response.StatusCode] $homeUrl"
    return
}
$response.Content > mwsq_login.html

echo 'Logining'
$loginForm = ($response.Forms | where { $_.Action -eq '/login' })[0]
$loginForm.Fields['email'] = $email
$loginForm.Fields['password'] = $password
$loginForm.Fields['type'] = '登录'
$loginForm.Fields['return-url'] = '/home'
$loginForm.Fields['remember'] = 'off'

$response = Invoke-WebRequest -Uri $loginAction -WebSession $rb -Method POST -Body $loginForm
if ($response.StatusCode -ne 200) {
    Write-Warning "[$response.StatusCode] $loginAction"
    return
}
$response.Content > mwsq_home.html

if ($response.Content -cnotmatch '1 - (\d+) 共 (\d+) 个书签') {
    Write-Warning '找不到书签个数'
    return
}

$page1Count = $Matches[1]
$totalCount = $Matches[2]
echo "1 - $page1Count 共 $totalCount 个书签"
$pageCount = [math]::Ceiling($totalCount / $bookmarksPerPage)
echo "共 $pageCount 页"
echo ''

$bookmarks = @()
for ($page = 1; $page -le $pageCount; $page++) {
    $uri = 'http://meiweisq.com/home?page=' + $page
    echo "Requesting $uri"

    $isSuccess = $false
    while (!$isSuccess) {
        try {
            $response = Invoke-WebRequest -Uri $uri -Method Default -WebSession $rb
            if ($response.StatusCode -ne 200) {
                Write-Warning "[$response.StatusCode] $loginAction"
                continue
            }
            $isSuccess = $true
        } catch { }
    }

    $response.Content > current_page.html
    $html = $response.ParsedHtml
    $linksDiv = ($html.getElementsByTagName('div') | where { $_.classname -eq 'links' })[0]
    $linksDiv.getElementsByTagName('div') | where { $_.classname -cmatch '\blink[\s,$]' } | foreach {
        $message = "Bookmark: {0} / {1}, Page: {2} / {3}, Elapsed: {4}" -f @(
            $($bookmarks.Length + 1),
            $totalCount,
            $page
            $pageCount,
            ([datetime]::Now - $startTime).ToString()
        )
        Write-Progress -Activity 'Getting bookmarks' -PercentComplete (100 * ($bookmarks.Length + 1) / $totalCount) -CurrentOperation $message
        echo "$($bookmarks.Length + 1) of $totalCount"
        $div = $_
        $linkTitle = $div.getElementsByTagName('a') | where { $_.className -eq 'link-title' }

        $title = $linkTitle | select -exp innerText
        $title = $title.Trim()
        echo $title

        $url = $linkTitle | select -exp href
        echo $url

        $linkTime = $div.getElementsByTagName('p') | where { $_.className -eq 'link-time' } | select -exp innerText
        $linkTime = $linkTime.Trim()
        echo $linkTime

        $ul = $div.getElementsByTagName('ul') | where { $_.className -cmatch '\btags[\s,$]' }
        $tags = $ul.getElementsByTagName('a') | where { $_.className -cmatch 'tag' }
        $tagNames = $tags | foreach { $_.getAttribute('tag') }
        if ($tagNames -eq $null) {
            $tagNames = @()
        }


        echo "[$([string]::Join(' | ', $tagNames))]"
        echo ''

        $bookmark = [PSObject]@{
            Title = $title
            Url = $url
            LinkTime = $linkTime
            Tags = [string]::Join(', ', $tagNames)
        }

        $bookmarks += New-Object -TypeName PSObject -Property $bookmark
    }
}

echo 'Exporting html thant you can import into del.icio.us'
$index = 0
while ($index -lt $totalCount) {
    $currentCountInExport = [math]::Min($countPerExport, $totalCount - $index)
    $endIndex = $index + $currentCountInExport

    $deliciousHtml = Get-DeliciousHtml ($bookmarks | select -Skip $index -First $currentCountInExport)
    $deliciousHtml | sc -Encoding UTF8 ("meiweisq-export-{0:yyyyMMdd}-{1}-{2}.html" -f [datetime]::Now, $index, $endIndex)
    $index += $currentCountInExport
}

$deliciousHtml = Get-DeliciousHtml $bookmarks
$deliciousHtml | sc -Encoding UTF8 ("meiweisq-export-{0:yyyyMMdd}-all.html" -f [datetime]::Now)

echo 'Exporting CSV.'
$bookmarks | Export-Csv ("meiweisq-export-{0:yyyyMMdd}.csv" -f [datetime]::Now) -Encoding UTF8 -NoTypeInformation

echo 'Exporting GUI.'
$bookmarks | Out-GridView

echo 'All done.'

您也可以点击这里下载源代码。

评论

用 PowerShell 脚本将书签批量导入 Delicious

前言

美味书签Delicious 在线书签服务的中国本地化版本。由于各方面原因,美味书签实现的功能有限,远远达不到 Delicious 的功能。所以我希望将美味书签中的使用记录迁移回 Delicious。

经过一年使用,我在美味书签中已经积累了 5000+ 条书签记录。由于美味书签不支持书签导出功能,所以将美味书签中的书签导出至 Delicious 是一件需要动手动脑的事。幸好我们有 PowerShell 脚本,可以助我们完成这项单调枯燥的事。

这是一个系列文章,一共分为 3 部分:

  1. 用 PowerShell 脚本来导出美味书签
  2. [用 PowerShell 脚本来清除 Delicious 账户下的所有书签][用 PowerShell 脚本来批量删除 Delicious 账户下的所有书签]
  3. 用 PowerShell 脚本将书签批量导入 Delicious

原理分析

Delicious API

通过阅读 Delicious API,可以知道我们只需要这样一条 API /v1/posts/add?,它的参数为:

  • &url={URL} (required) — The url of the item.
  • &description={...} (required) — The description of the item.
  • &extended={...} (optional) — Botes for the item.
  • &tags={...} (optional) — Tags for the item (comma delimited).
  • &dt={CCYY-MM-DDThh:mm:ssZ} (optional) — Datestamp of the item (format “CCYY-MM-DDThh:mm:ssZ”). Requires a LITERAL “T” and “Z” like in ISO8601 at http://www.cl.cam.ac.uk/~mgk25/iso-time.html for Example: 1984-09-01T14:21:31Z
  • &replace=no (optional) — Don’t replace post if given url has already been posted.
  • &shared=no (optional) — Make the item private

关于身份验证,请参考本系列的另一篇文章 用 PowerShell 脚本来清除 Delicious 账户下的所有书签

URL 编码

我们需要提交的书签中,description 字段和 tags 字段是有可能出现 URL 中不允许的字符的,例如 ?&,以及中文字符等。我们需要将它们进行 URL 编码以后,才可以拼接到 URL 字符串中。在 PowerShell 中进行 URL 编码的方法如下:

Add-Type -AssemblyName 'System.Web'
[System.Web.HttpUtility]::UrlEncode('中文')

其中第一行是为了加载 System.Web 程序集。还可以用以下两种方法来实现:

[void][system.Reflection.Assembly]::LoadWithPartialName("System.Web")

以及:

[Reflection.Assembly]::LoadFile('C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\System.Web.dll') | Out-Null

生成查询字符串

我们要在查询字符串中包含 API 文档中提到的那 7 个参数。用 string -f 的方式显得有点笨拙。于是我们编写这样一个函数:

function Get-QueryString ($params) {
    $keyValuePairs = ($params.Keys | % {
        write ('{0}={1}' -f $_, $params[$_])
    })
    return $keyValuePairs -join '&'
}

这个函数接收一个哈希表作为参数,也可以是 [ordered] (即OrderedDictionary)。函数中循环地取出所有键,将它们的值用 & 符号拼接在一起。

容错设计

若是 Invoke-WebRequest 命令抛出异常,或是 HTTP 响应码不为 200,或是 XML 中不是 <result code="done" /> 这样的返回,那么表示添加书签失败。我们可以把这些书签收集起来,输出到 failed_import.csv 文件中。然后下次可以再对这个文件进行导入。直到这个文件中没有记录为止。当然,您也可以将脚本改进一下,全自动地做完上述的事情。那么您一定是懒(勤劳)到家了 ;-)

源代码

$userName = 'vichamp'
$importFileName = 'meiweisq-export-20131030.csv'
#$importFileName = 'failed_import.csv'

Add-Type -AssemblyName 'System.Web'
#$password = ConvertTo-SecureString –String "xxx" –AsPlainText -Force

$credential = Get-Credential -UserName $userName -Message '请输入密码'

function Get-QueryString ($params) {
    $keyValuePairs = ($params.Keys | % {
        write ('{0}={1}' -f $_, $params[$_])
    })
    return $keyValuePairs -join '&'
}

$startTime = [datetime]::Now
$template = 'https://api.del.icio.us/v1/posts/add?{0}'

$bookmarks = Import-Csv $importFileName
$failedBookmarks = @()
$index = 0
$bookmarks | foreach {
    $params = @{}
    $params.Add('description', [System.Web.HttpUtility]::UrlEncode($_.Title))
    if ($false) {
        $params.Add('extended', [System.Web.HttpUtility]::UrlEncode(''))
    }
    $params.Add('tags', [System.Web.HttpUtility]::UrlEncode([string]::Join(',', $_.Tags -split ', ')))
    $params.Add('dt', ("{0}T00:00:00Z" -f ($_.LinkTime -creplace '/', '-')))
    $params.Add('replace', 'no')
    $params.Add('shared', 'yes')
    $params.Add('url', $_.Url)

    $queryString = Get-QueryString $params
    $url = $template -f $queryString

    $message = "Bookmark: {0} / {1}, Elapsed: {2}" -f @(
        $($index + 1),
        $bookmarks.Length,
        ([datetime]::Now - $startTime).ToString()
    )
    Write-Progress -Activity 'Adding bookmarks' -PercentComplete (100 * $index / $bookmarks.Length) -CurrentOperation $message
    #echo "Requesting $_.Url"

    $isSuccess = $false
    try {
        [xml]$response = Invoke-WebRequest -Uri $url -Credential $credential
        $isSuccess = $response.StatusCode -eq 200 -and $response.result.code -eq 'done'
    } catch { }

    if ($isSuccess) {
        Write-Output "[SUCC] $($_.Url)"
    } else {
        Write-Warning "[FAIL] $($_.Url)"
        $failedBookmarks += $_
    }

    $index++
}

$failedBookmarks | Export-Csv 'failed_import.csv' -Encoding UTF8 -NoTypeInformation

您也可以点击这里下载源代码。

评论

PowerShell 技能连载 - 怪异的文本格式化(以及解决方法)

试试以下的代码并且找到问题所在:

$desc = Get-Process -Id $pid | Select-Object -Property Description
"PowerShell process description: $desc"

这段代码的目的是获取 PowerShell 宿主进程并且读取进程的描述信息,然后输出到字符串。它的结果看起来是怪异的:

PowerShell process description: @{Description=Windows PowerShell}

这是因为代码中选择了整个 Description 属性,而且结果不仅是描述字符串,而且包括了整个属性:

PS> $desc

Description
-----------
Windows PowerShell ISE

当您只选择一个属性时,请确保使用 -ExpandProperty 而不是 -Property。前者避免产生一个属性列,并且字符串看起来正常了:

PS> $desc = Get-Process -Id $pid | Select-Object -ExpandProperty Description
PS> "PowerShell process description: $desc"
PowerShell process description: Windows PowerShell ISE
评论

PowerShell 技能连载 - 在 PowerShell 中查找服务

Get-Service 可以列出计算机上的所有服务,但是返回的信息十分少。您无法很容易地看出一个服务做什么、它是一个 Microsoft 服务还是一个第三方服务,以及服务所对应的可执行程序。

通过合并一些信息,您可以获取许多更丰富的信息。以下是一个 Find-Service 函数,可以返回一系列丰富的信息:

function Find-Service
{
    param
    (
        $Name = '*',
        $DisplayName = '*',
        $Started
    )
    $pattern = '^.*\.exe\b'

    $Name = $Name.Replace('*','%')
    $DisplayName = $DisplayName.Replace('*','%')

    Get-WmiObject -Class Win32_Service -Filter "Name like '$Name' and DisplayName like '$DisplayName'"|
      ForEach-Object {

        if ($_.PathName -match $pattern)
        {
            $Path = $matches[0].Trim('"')
            $file = Get-Item -Path $Path
            $rv = $_ | Select-Object -Property Name, DisplayName, isMicrosoft, Started, StartMode, Description, CompanyName, ProductName, FileDescription, ServiceType, ExitCode, InstallDate, DesktopInteract, ErrorControl, ExecutablePath, PathName
            $rv.CompanyName = $file.VersionInfo.CompanyName
            $rv.ProductName = $file.VersionInfo.ProductName
            $rv.FileDescription = $file.VersionInfo.FileDescription
            $rv.ExecutablePath = $path
            $rv.isMicrosoft = $file.VersionInfo.CompanyName -like '*Microsoft*'
            $rv
        }
        else
        {
            Write-Warning ("Service {0} has no EXE attached. PathName='{1}'" -f $_.PathName)
        }
      }
}

Find-Service | Out-GridView
评论

PowerShell 技能连载 - 获取 1000 个以上 Active Directory 结果

当您使用 ADSISearcher 时,默认情况下,Active Directory 只返回前 1000 个搜索结果。这是一个防止意外的 LDAP 查询导致域控制器负荷过重的安全保护机制。

如果您需要完整的搜索结果,并且明确地知道它将超过 1000 条记录,请设置 PageSize 为 1000。通过这种方式,ADSISearcher 每一批返回 1000 个搜索结果元素。

以下查询将会返回您域中的所有用户账户(在运行这个查询之前,您也许需要联系一下您的域管理员):

$searcher = [ADSISearcher]"sAMAccountType=$(0x30000000)"

# get all results, do not stop at 1000 results
$searcher.PageSize = 1000

$searcher.FindAll() |
  ForEach-Object { $_.GetDirectoryEntry() } |
  Select-Object -Property * |
  Out-GridView
评论

PowerShell 技能连载 - 在智能感知中隐藏参数

从 PowerShell 4.0 开始,脚本作者可以决定隐藏某些参数,使之不在智能感知中出现。通过这种方式,可以在 ISE 的智能感知上下文菜单中隐藏不常用的参数。

function Test-Function
{
    param(
        $Name,
        [Parameter(DontShow)]
        [Switch]
        $IAmSecret
    )

    if ($IAmSecret)
    {
     "Doing secret things with $Name"
    }
    else
    {
      "Regular behavior with $Name"
    }
}

当您在 PowerShell 4.0 ISE 中运行这个函数时,只有 “Name” 参数会出现在智能感知上下文菜单中。然而,如果您事先知道这个隐藏参数,并且键入了第一个字母,然后按下 (TAB) 键,这个参数仍会显示:

在帮助窗口,隐藏参数总是可以显示,您可以通过类似这种方式打开:

评论

PowerShell 技能连载 - 快速查找 Active Directory 用户账户

LDAP 查询条件越明确,查询速度就越快,占用的资源就越少,并且查询结果越清晰。

例如,许多人使用 objectClass 来限制查询结果为某个指定的对象类型。若只需要查询用户账户,他们常常使用 "objectClass=user" 的写法。许多人不知道计算机账户也共享这个对象类型。让我们来验证这一点:

这个例子将会查找所有 SamAccountName 以 “a” 开头,并且 objectClass=”user” 的账户。

# get all users with a SamAccountName that starts with "a"
$searcher = [ADSISearcher]"(&(objectClass=User)(sAMAccountName=a*))"

# see how long this takes
$result = Measure-Command {
  $all = $searcher.FindAll()
  $found = $all.Count
}

$seconds = $result.TotalSeconds

"The search returned $found objects and took $sec seconds."

然后使用这行来代替上面的代码:

$searcher = [ADSISearcher]"(&(sAMAccountType=$(0x30000000))(sAMAccountName=a*))"

当您换成这行代码以后,查询速度显著提升了。并且结果更清晰。这是因为普通用户账户和计算机账户的 SamAccountType 不同:

  • SAM_NORMAL_USER_ACCOUNT 0x30000000
  • SAM_MACHINE_ACCOUNT 0x30000001

两者的 objectClass 都属于 “User”。

评论

PowerShell 技能连载 - 通过 SID 查找 Active Directory 账户

如果您已知账户的 SID 并且希望找到相应的 Active Directory 账户,那么 LDAP 查询并不适合这项工作。为了使它能工作,您需要将 SID 的格式改成符合 LDAP 规则的格式,这不是一个简单的过程。

以下是一个更简单的使用 LDAP 路径的办法。假设您使用 $SID 变量保存了一个 SID 字符串,并且您希望查找出和它关联的 Active Directory 账户。试试以下的代码:

$SID = '<enter SID here>'   # like S-1-5-21-1234567-...
$account = [ADSI]"LDAP://<SID=$SID>"
$account
$account.distinguishedName
评论

用 PowerShell 处理纯文本 - 2

应朋友要求,帮忙解决一例 PowerShell 问题:

有一个 CSV 文件,其中有一个 Photo 字段存的是 BASE64 编码的字符串,这个字符串包含换行符。在 Import-Csv 的时候,Photo 字段不会作为一个整体值,而是会变成每行一个。文件的内容是这样的:

StaffNum,LogonName,ObjName,Title,Office,Department,Photo
03138,wangjunhao,王俊豪,流程优化主管,宜山路,运营平台中心/项目部,/9j/4AAQSkZJRgABAgAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0a
HBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIy
...
PyoooGSr0qRCdtFFMBy9qkFFFJCYo60w85oopgNzjoBTto9KKKQj/9k=

为了解决这个 case,先归纳它的规律:

  1. BASE64 字符串的字符集为 0..9、a..z、A..Z,以及/和=。
  2. BASE64 字符串每一行不超过 76 个字符。
  3. 如果某一行从第一个字符到最后一个字符,都符合上述 2 条规律,说明前一行并没有结束。应当把当前行拼接到前一行中。

根据以上规律编写 PowerShell 代码:

$fileName = 'AllUsers.csv'

$currentLine = ''
gc $fileName | % -process {
        if ($_ -cmatch '^[a-zA-Z0-9/+=]{1,76}$') {
            # 如果符合 BASE64 特征,说明上一行未结束。
            $currentLine += $_
        } else {
            # 如果不符合 BASE64 特征,说明上一行是完整的。
            Write-Output $currentLine
            $currentLine = $_
        }
    } -end {
        $currentLine
    } |
ConvertFrom-Csv

完整的 .CSV 及 .PS1 文件请在这里下载

评论