用 PowerShell 重新打包 0day appz

从 0day 服务器下载下来的 appz 文件夹是这样的形态:

每个文件夹代表一个 appz 软件,打开是这个样子的:

里面是一系列 .zip 文件以及说明文件。这些 .zip 文件却不是使用 zip 的分卷压缩出来的,它们的内容如下:

要把这些 .zip 文件全部解压到同一个目录下,才可以得到一系列 rar 的分卷压缩文件。我们打开一个 .rar 文件,这才看到真正的内容:

软件数量大的时候,人工重复进行上述操作就不合适了。机械的劳动应该交给程序。我们可以设计一个 PowerShell 脚本,完成一系列功能:

  • 遍历 0day appz 的下载目录。
  • 解压所有 .zip 文件。
  • 解压 .rar 文件。
  • 将说明文件复制到一起。
  • 将最终的文件重打包为 .zip 文件。
  • 如果上述的解压有问题,则不打包,并输出错误日志。
  • 清理临时文件。
  • 清理成功的原始文件夹,保留失败的原始文件夹。

按照这个需求,我们可以编写如下 PowerShell 脚本:

$DebugPreference = 'Continue'

$incoming = 'd:\0day\incoming'
$temp1 = 'd:\0day\temp1'
$temp2 = 'd:\0day\temp2'
$output = 'd:\0day\output'

if (Test-Path $temp1) { del $temp1 -r }
if (Test-Path $temp2) { del $temp2 -r }

$apps = dir $incoming -Directory
$count = 0
$hasFailed = $false
$apps | foreach {
    $name = $_.Name
    Write-Progress -Activity 'Repacking apps' -PercentComplete ($count / $apps.Length * 100) -CurrentOperation $name
    echo "Repacking $name"

    md $temp1 | Out-Null
    md $temp2 | Out-Null

    # d:\0day\util\7z x -o"d:\0day\temp1" "d:\0day\incoming\VanDyke.SecureCRT.v7.2.2.491.Incl.Patch.And.Keymaker-ZWT\*.zip"
    $arguments = 'x', "-o""$temp1""", '-y', (Join-Path $_.FullName *.zip)
    .\7z $arguments | Out-Null

    if (!$?) {
        Write-Warning "Repacking $name failed."
        echo "$name" >> "$output\fail.log"

        del $temp1 -r
        del $temp2 -r

        $count++
        $hasFailed = $true
        return
    }

    # d:\0day\util\7z x -o"d:\0day\temp2" "d:\0day\temp1\*.rar" -y
    $arguments = 'x', "-o""$temp2""", '-y', "$temp1\*.rar"
    .\7z $arguments | Out-Null
    if (!$?) {
        Write-Warning "Repacking $name failed."
        echo "$name" >> "$output\fail.log"

        del $temp1 -r
        del $temp2 -r

        $count++
        $hasFailed = $true
        return
    }

    # copy d:\0day\temp1\*.diz d:\0day\temp2
    # copy d:\0day\temp1\*.nfo d:\0day\temp2

    dir $temp1 | where {
        $_.Extension -notmatch 'rar|r\d*'
    } | copy -Destination $temp2

    #d:\0day\util\7z a "d:\0day\output\VanDyke.SecureCRT.v7.2.2.491.Incl.Patch.And.Keymaker-ZWT.zip" "d:\0day\temp2\*.*" -r
    $arguments = 'a', "$output\$name.zip", "$temp2\*.*", '-r'
    .\7z $arguments | Out-Null
    if (!$?) {
        Write-Warning "Repacking $name failed."
        echo "$name" >> "$output\fail.log"

        del $temp1 -r
        del $temp2 -r

        $count++
        $hasFailed = $true
        return
    }

    del $temp1 -r
    del $temp2 -r

    Remove-Item -LiteralPath $_.FullName -r

    $count++
}

if ($hasFailed) {
    echo '' >> "$output\fail.log"
}

echo 'Press any key to continue...'
[Console]::ReadKey() | Out-Null

# del 'd:\0day\output\*.*' -r

您也可以在这里下载写好的脚本,包括完整的目录结构和 7z 软件包。请解压到 d:\ 中使用,或者自行调整脚本头部的路径。

用 PowerShell 处理纯文本 - 4

命题

QQ 群里的史瑞克朋友提出的一个命题:

$txt="192.168.1
192.168.2
192.168.3
172.19.3
192.16.1
192.16.2
192.16.11
192.16.3
10.0.4
192.16.29
192.16.9
192.16.99
192.16.100"

要求输出:

10.0.4
172.19.3
192.16.1-192.16.3
192.16.9
192.16.11
192.16.29
192.16.99-192.16.100
192.168.1-192.168.3

问题解析

* 将各个 IP 段补充为三位的格式
* 按字符串排序
* 遍历每一行,按照以下规则处理:
    * 如果和上一行连续,则上一段可能没有结束,更新 `$endIP`
    * 如果和上一行不连续
        * 若 `$startIP` 和 `$endIP` 相同,说明是单个 IP,将单个 IP 加入 $result
        * 若 `$startIP` 和 `$endIP` 不同,说明是一段 IP,将一段 IP 加入 $result
        * 更新 `$startIP` 和 `$endIP`
* 最后一行需要特殊处理

PowerShell 实现

$DebugPreference = "Continue"

$txt="192.168.1
192.168.2
192.168.3
172.19.3
192.16.1
192.16.2
192.16.11
192.16.3
10.0.4
192.16.29
192.16.9
192.16.99
192.16.100"
$txt += "`n999.999.999"

$startIP = @(0, 0, 0)
$endIP = @(0, 0, 0)
$result = @()

-split $txt | % {
    $fullSegments = ($_ -split "\." | % {
        "{0:D3}" -f [int]$_
    })
    $fullSegments -join "."
} | sort | % {
    Write-Debug "Processing $_"
    $segments = @($_ -split "\." | % {
        [int]$_
    })

    if ($endIP[0] -eq $segments[0] -and
        $endIP[1] -eq $segments[1] -and
        $endIP[2] + 1 -eq $segments[2]) {
        Write-Debug '和上一个IP连续'
        $endIP = $segments
    } else {
        Write-Debug '和上一个IP不连续'
        if (($startIP -join ".") -eq ($endIP -join ".")) {
            Write-Debug '单个IP'
            $result += $startIP -join "."
        } else {
            Write-Debug '一段IP'
            $result += ($startIP -join ".") + "-" + ($endIP -join ".")
        }

        $startIP = $segments
        $endIP = $segments
    }
}

$result | select -Skip 1

源代码请在这里下载

用 PowerShell 处理纯文本 - 3

命题:

怎样把字符 “ABC-EFGH-XYZ” 替换为 “012-3456-789”

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

  • 注意字符集和数字之间的对应关系,我们可以自定义一个字符集来表示。
  • 保留 - 号。
  • 不区分大小写,提高适应性。

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

$charSet = 'ABCEFGHXYZ'.ToCharArray()

$text = 'ABC-EFGH-XYZ'

$array = ($text.ToUpper().ToCharArray() | % {
    if ($_ -eq '-') {
        '-'
    } else {
        [string]([System.Array]::IndexOf($charSet, $_))
    }
})

$array -join ''

结果是:

012-3456-789

用 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 脚本来清除您 Delicious 账户下的所有书签

前言

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

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

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

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

原理分析

Delicious API

通过阅读 Delicious API,我们找出需要的 API 来:

API 功能
/v1/posts/all? 列出所有书签
/v1/posts/all?hashes 以哈希的形式列出所有书签
/v1/posts/delete? 删除一条书签

其中 /v1/posts/all?hashes 这条 API 暂时用不到。

身份验证

在 Delicious API 文档中提到了在 URL 中包含用户和密码的方式来验证身份:

$ curl https://user:passwd@api.delicious.com/v1/posts/get?tag=webdev&meta=yes

但在实际中这个方法行不通。我们还是通过 PowerShell 的 Get-Credential 命令来实现:

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

这段代码的执行效果是弹出一个身份验证框

当然,您也可以把身份信息硬编码的方式写在脚本中,在调试期可以提高效率。但在脚本发布时,可以采用 Get-Credential 这种优雅的方式来提示用户输入。

调用 API

调用 Delicious API 的方法十分简单,由于返回的是一个 XML 文档,我们可以显式地将 $listResponse 返回值的数据类型声明为 [xml]

[xml]$listResponse = Invoke-WebRequest -Uri 'https://api.del.icio.us/v1/posts/all?red=api' -Credential $credential

解析执行结果

我们可以在浏览器中试着敲入 https://api.delicious.com/v1/posts/all?red=api 来观察执行结果,浏览器将会要求您输入 Delicious 的用户名与密码:

通过观察 XML 的结构,我们可以从 API 响应中取得所有书签的链接,用 XPATH 表达为 posts/post/@href。用 PowerShell 来表达,代码如下:

$links = $listResponse.posts.post | select -exp href -Unique

考虑到有些链接可能重复,我们加了个 -Unique 参数,取得不重复的唯一结果。

删除链接

通过上述方法得到所有的书签链接之后,我们可以循环调用 /v1/posts/delete? API 来删除它们。根据文档,若删除成功,将返回:

<result code="done" />

所以我们可以这样设计脚本:

if ($response.result.code -eq 'done') {
    #
}

吝啬地休眠

API 文档中有一句严厉的警告,原文如下:

Please wait at least one second between HTTP queries, or you are likely to get automatically throttled. If you are releasing a library to access the API, you must do this.

意思是说 HTTP 请求不能太频繁,至少要间隔 1 秒。但我觉得时间是珍贵的,如果每次 Start-Sleep -Seconds 1 的话,每一次加上网络传输时间,就不止 1 秒了。时间浪费在 sleep 上十分可惜,特别是在大量的循环中更是如此。我希望 sleep 的时间恰好是 1 秒。所以我设计了一个函数,计算当前时间与上一次 sleep 时的时间差。然后精确地 sleep 这个时间差值,一点也不多睡 ;-)

function Invoke-StingySleep ($seconds) {
    if (!$lastSleepTime) {
        $lastSleepTime = Get-Date
    }

    $span = $lastSleepTime + (New-TimeSpan -Seconds 1) - (Get-Date)
    Start-Sleep -Milliseconds $span.TotalMilliseconds
}

不过实际使用中,似乎 Delicious 的开发者比较仁慈。如果我把 Start-Sleep 这行去掉,服务器并没有因为我们连续不断地请求而把我们的程序给屏蔽掉。当然也有可能是我所在的地方网络延迟太大了。

容错技巧

其实这个程序还有很多地方可以改进,例如每次调用删除 API 后判断服务器的 HTTP 响应是否正确,但可以不去改进它。理由是:既然我们的目的是删除所有的书签,那么如果有某一些漏网之鱼没有删掉,那么在下一轮循环中会被查询出来,重新删除。只要脚本工作得不离谱的话,一定能删到完为止。

源代码

$userName = 'vichamp'

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

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

function Invoke-StingySleep ($seconds) {
    if (!$lastSleepTime) {
        $lastSleepTime = Get-Date
    }

    $span = $lastSleepTime + (New-TimeSpan -Seconds 1) - (Get-Date)
    #Start-Sleep -Milliseconds $span.TotalMilliseconds
}

while ($true) {
    Invoke-StingySleep 1
    [xml]$listResponse = Invoke-WebRequest -Uri 'https://api.delicious.com/v1/posts/all?red=api' -Credential $credential
    #[xml]$response = Invoke-WebRequest -Uri 'https://api.del.icio.us/v1/posts/all?hashes' -Credential $credential
    if (!$listResponse.posts.post) {
        break
    }
    $links = $listResponse.posts.post | select -exp href -Unique

    $links | foreach {
        $encodedLink = [System.Web.HttpUtility]::UrlEncode($_)

        Invoke-StingySleep 1
        [xml]$response = Invoke-WebRequest -Uri "https://api.delicious.com/v1/posts/delete?url=$encodedLink"  -Credential $credential
        if ($response.result.code -eq 'done') {
            Write-Output "[$($response.result.code)] $_"
        } else {
            Write-Warning "[$($response.result.code)] $_"
        }
    }
}

echo 'Done'

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

用 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.'

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

快速运行 .ps1 脚本的 N 种方法

由于安全的原因,微软禁止以双击的方式执行 PowerShell 的 .ps1 脚本。但如果我们一味地追求效率,而暂时“无视”其安全性的话,要怎样快速地执行 .ps1 脚本呢?以下是 QQ 群里讨论的一些方案:

为每个 .ps1 配一个 .cmd 批处理文件

这种方法适用于可能需要将 PowerShell 脚本发送给朋友执行,而朋友可能只是初学者或普通用户的场景,并且该脚本不会修改注册表。具体做法是:将以下代码保存为一个 .cmd 文件,放在 .ps1 的同一个目录下。注意主文件名必须和 .ps1 的主文件名一致

@set Path=%Path%;%SystemRoot%\system32\WindowsPowerShell\v1.0\ & powershell -ExecutionPolicy Unrestricted -NoExit -NoProfile %~dpn0.ps1

例如,您希望朋友执行 My-Script.ps1,那么您只需要将以上代码保存为 My-Script.cmd,放在同一个目录之下发给您的朋友即可。

这种方法还有个小小的好处是,不需要为不同的 .ps1 而修改 .cmd 的内容。

用批处理文件做一个简单的菜单,列出 .ps1 文件

该方法由 @PS 网友提供。优点在于可以为多个 .ps1 脚本配一个 .cmd 批处理。执行 .cmd 以后,将显示一个简易的字符界面选择菜单。可以根据用户的选择执行相应的 .ps1 脚本。以下是代码:

@echo off
setlocal enabledelayedexpansion
for  %%i in (*.ps1) do (
     set /a num+=1
     set .!num!=%%i
     echo !num!. %%i
)
set/p n=这里输入序列:
echo !.%n%!
set Path=%Path%;%SystemRoot%\system32\WindowsPowerShell\v1.0\ & powershell -ExecutionPolicy Unrestricted -NoProfile .\!.%n%!
pause

用命令行修改 PowerShell 文件的打开方式

该方法由 @史瑞克 网友提供,只需要在命令行中执行一次以下代码,以后即可双击运行 .ps1 脚本:

ftype Microsoft.PowerShellScript.1="%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe" ".\%1"

用这种方法可以设置打开方式和默认打开方式,需要管理员权限。恢复方法:

ftype Microsoft.PowerShellScript.1="%SystemRoot%\system32\notepad.exe" "%1"

用 PowerShell 脚本修改 PowerShell 文件的打开方式

该方法由 StackOverflow 上的 @JPBlanc 网友提供。只需要在 PowerShell 中执行一次以下代码,以后即可双击运行 .ps1 脚本。优点是可以开可以关。原文是法文,已由 @Andy Arismendi 网友翻译为英文。

<#
.SYNOPSIS
    Change the registry key in order that double-clicking on a file with .PS1 extension
    start its execution with PowerShell.
.DESCRIPTION
    This operation bring (partly) .PS1 files to the level of .VBS as far as execution
    through Explorer.exe is concern.
    This operation is not advised by Microsoft.
.NOTES
    File Name   : ModifyExplorer.ps1
    Author      : J.P. Blanc - jean-paul_blanc@silogix-fr.com
    Prerequisite: PowerShell V2 on Vista and later versions.
    Copyright 2010 - Jean Paul Blanc/Silogix
.LINK
    Script posted on:
    http://www.silogix.fr
.EXAMPLE
    PS C:\silogix> Set-PowAsDefault -On
    Call Powershell for .PS1 files.
    Done !
.EXAMPLE
    PS C:\silogix> Set-PowAsDefault
    Tries to go back
    Done !
#>
function Set-PowAsDefault
{
  [CmdletBinding()]
  Param
  (
    [Parameter(mandatory=$false,ValueFromPipeline=$false)]
    [Alias("Active")]
    [switch]
    [bool]$On
  )

  begin
  {
    if ($On.IsPresent)
    {
      Write-Host "Call Powershell for .PS1 files."
    }
    else
    {
      Write-Host "Try to go back."
    }
  }

  Process
  {
    # Text Menu
    [string]$TexteMenu = "Go inside PowerShell"

    # Text of the program to create
    [string] $TexteCommande = "%systemroot%\system32\WindowsPowerShell\v1.0\powershell.exe -Command ""&'%1'"""

    # Key to create
    [String] $clefAModifier = "HKLM:\SOFTWARE\Classes\Microsoft.PowerShellScript.1\Shell\Open\Command"

    try
    {
      $oldCmdKey = $null
      $oldCmdKey = Get-Item $clefAModifier -ErrorAction SilentlyContinue
      $oldCmdValue = $oldCmdKey.getvalue("")

      if ($oldCmdValue -ne $null)
      {
        if ($On.IsPresent)
        {
          $slxOldValue = $null
          $slxOldValue = Get-ItemProperty $clefAModifier -Name "slxOldValue" -ErrorAction SilentlyContinue
          if ($slxOldValue -eq $null)
          {
            New-ItemProperty $clefAModifier -Name "slxOldValue" -Value $oldCmdValue  -PropertyType "String" | Out-Null
            New-ItemProperty $clefAModifier -Name "(default)" -Value $TexteCommande  -PropertyType "ExpandString" | Out-Null
            Write-Host "Done !"
          }
          else
          {
            Write-Host "Already done !"
          }

        }
        else
        {
          $slxOldValue = $null
          $slxOldValue = Get-ItemProperty $clefAModifier -Name "slxOldValue" -ErrorAction SilentlyContinue
          if ($slxOldValue -ne $null)
          {
            New-ItemProperty $clefAModifier -Name "(default)" -Value $slxOldValue."slxOldValue"  -PropertyType "String" | Out-Null
            Remove-ItemProperty $clefAModifier -Name "slxOldValue"
            Write-Host "Done !"
          }
          else
          {
            Write-Host "No former value !"
          }
        }
      }
    }
    catch
    {
      $_.exception.message
    }
  }
  end {}
}

使用方法很简单,Set-PowAsDefault -On为打开,Set-PowAsDefault为关闭。需要管理员权限。

以上是目前搜集的几种方法,希望对您有用。您可以在这里下载以上所有脚本的例子。

用 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 文件请在这里下载

快速替换文本文件中的字符串

不用开什么vim、emac、UltraEdit、Eclipse之类的编辑器了,PowerShell可以帮助手无寸铁的您快速地替换文本文件中的字符串:

dir *.txt -Recurse | % {
    (gc $_ -Raw) | % { $_ `
        -creplace '111', 'AAA' `
        -creplace '222', 'BBB' `
        -creplace '333', 'CCC'
    } | sc $_
}

注意 -creplace 区分大小写,-replace 不区分大小写。并且它们支持正则表达式!

在PowerShell中以管理员身份运行程序

对于已知的需要以管理员身份运行的命令,我们可以通过这个 Invoke-Admin 函数运行。这个函数确保以管理员身份运行一个程序。如果不是以管理员身份运行,则将弹出 UAC 对话框。

function Invoke-Admin() {
    param ( [string]$program = $(throw "Please specify a program" ),
            [string]$argumentString = "",
            [switch]$waitForExit )

    $psi = new-object "Diagnostics.ProcessStartInfo"
    $psi.FileName = $program
    $psi.Arguments = $argumentString
    $psi.Verb = "runas"
    $proc = [Diagnostics.Process]::Start($psi)
    if ( $waitForExit ) {
        $proc.WaitForExit();
    }
}

来源:Showing the UAC prompt in PowerShell if the action requires elevation