PowerShell 技能连载 - 查找缺省的 Outlook 配置文件
PowerShell 可以操作 COM 对象,例如 Outlook 应用程序。以下简单的两行代码能返回当前的 Outlook 配置文件名:
$outlookApplication = New-Object -ComObject Outlook.Application
$outlookApplication.Application.DefaultProfileName
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)
}
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`
* 最后一行需要特殊处理
$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
命题:
怎样把字符 “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
如果您只是需要替换文本中字符出现的所有位置,这是很简单的。以下可以将文本中所有的“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 部分:
通过阅读 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 账户下的所有书签 。
我们需要提交的书签中,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
您也可以点击这里下载源代码。
美味书签是 Delicious 在线书签服务的中国本地化版本。由于各方面原因,美味书签实现的功能有限,远远达不到 Delicious 的功能。所以我希望将美味书签中的使用记录迁移回 Delicious。
经过一年使用,我在美味书签中已经积累了 5000+ 条书签记录。由于美味书签不支持书签导出功能,所以将美味书签中的书签导出至 Delicious 是一件需要动手动脑的事。幸好我们有 PowerShell 脚本,可以助我们完成这项单调枯燥的事。
这是一个系列文章,一共分为 3 部分:
美味书签的登录页面地址为 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 个书签”的字样,其中 30 和 5126 两个数字是我们关心的。我们用正则表达式 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 那么高效。在我们需要做一定的优化,以缩短大量的查询的总时间。我的优化原则如下:
以下是 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') }
美味书签的时间以 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 部分:
通过阅读 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 这种优雅的方式来提示用户输入。
调用 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 技能连载 - 怪异的文本格式化(以及解决方法)
试试以下的代码并且找到问题所在:
$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