PowerShell 技能连载 - 将单词首字母转换为大写

要正确地将单词首字母转换为大写,您可以用正则表达式或者一点系统函数:

用正则表达式的话,您可以这样做:

$sentence = 'here is some text where i would like the first letter to be capitalized.'
$pattern = '\b(\w)'
[RegEx]::Replace($sentence, $pattern, { param($x) $x.Value.ToUpper() })

用系统函数的话,这样做可以达到相同的效果:

$sentence = 'here is some text where i would like the first letter to be capitalized.'
(Get-Culture).TextInfo.ToTitleCase($sentence)

正则表达式稍微复杂一点,但是功能更多。例如如果出于某种古怪的原因,您需要将每个单词的首字母替换为它的 ASCII 码,那么正则表达式可以轻松地实现:

$sentence = 'here is some text where i would like the first letter to be capitalized.'
$pattern = '\b(\w)'
[RegEx]::Replace($sentence, $pattern, { param($x) [Byte][Char]$x.Value })

PowerShell 技能连载 - 查找缺省的 Outlook 配置文件

PowerShell 可以操作 COM 对象,例如 Outlook 应用程序。以下简单的两行代码能返回当前的 Outlook 配置文件名:

$outlookApplication = New-Object -ComObject Outlook.Application
$outlookApplication.Application.DefaultProfileName

PowerShell 技能连载 - PowerShell 4.0 中的动态方法

从 PowerShell 4.0 开始,方法名可以是一个变量。以下是一个简单的例子:

$method = 'ToUpper'
'Hello'.$method()

当您需要调用的方法须通过一段脚本计算得到的时候,这个特性十分有用。

function Convert-Text
{
  param
  (
    [Parameter(Mandatory)]
    $Text,
    [Switch]$ToUpper
  )

  if ($ToUpper)
  {
    $method = 'ToUpper'
  }
  else
  {
    $method = 'ToLower'
  }
  $text.$method()
}

以下是用户调用该函数的方法:

PS> Convert-Text 'Hello'
hello
PS> Convert-Text 'Hello' -ToUpper
HELLO

缺省情况下,该函数将文本转换为小写。当指定了开关参数 -ToUpper 时,函数将文本转换为大写。由于动态方法特性的支持,该函数不需要为此写两遍代码。

译者注:在旧版本的 PowerShell 中,您可以通过 .NET 方法(而不是脚本方法)中的反射来实现相同的目的。虽然它不那么整洁,但它能运行在 PowerShell 4.0 以下的环境:

function Convert-Text
{
  param
  (
    [Parameter(Mandatory)]
    $Text,
    [Switch]$ToUpper
  )

  if ($ToUpper)
  {
    $method = 'ToUpper'
  }
  else
  {
    $method = 'ToLower'
  }
  $methodInfo = $Text.GetType().GetMethod($method, [type[]]@())
  $methodInfo.Invoke($Text, $null)
}

用 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 技能连载 - PowerShell 4.0 中的动态参数

在 PowerShell 中,您可以使用变量来指代属性名。这段示例脚本定义了四个 profile 的属性名,然后在一个循环中分别查询这些属性值:

$list = 'AllUsersAllHosts','AllUsersCurrentHost','CurrentUserAllHosts','CurrentUserCurrentHost'
foreach ($property in $list)
{
    $profile.$property
}

您也可以在一个管道中使用它:

'AllUsersAllHosts','AllUsersCurrentHost','CurrentUserAllHosts','CurrentUserCurrentHost' |
ForEach-Object { $profile.$_ }

通过这种方式,您可以检查和返回 PowerShell 当前使用的所有 profile:

'AllUsersAllHosts','AllUsersCurrentHost','CurrentUserAllHosts','CurrentUserCurrentHost' |
ForEach-Object { $profile.$_ } |
Where-Object { Test-Path $_ }

类似地,您可以首先使用 Get-Member 来获取一个指定对象包含的所有属性。以下代码可以返回 PowerShell 的“PrivateData”对象中所有名字包含“color”的属性:

$host.PrivateData | Get-Member -Name *color* | Select-Object -ExpandProperty Name

接下来,您可以用一行代码获取所有的颜色设置:

$object = $host.PrivateData
$object | Get-Member -Name *color* -MemberType *property | ForEach-Object {
    $PropertyName = $_.Name
    $PropertyValue = $object.$PropertyName
    "$PropertyName = $PropertyValue"
} |
Out-GridView

用 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 技能连载 - 替换文本中的指定字符

如果您只是需要替换文本中字符出现的所有位置,这是很简单的。以下可以将文本中所有的“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 的功能。所以我希望将美味书签中的使用记录迁移回 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.'

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