用 PowerShell 批量分割 QQ 聊天记录

纯文本文件有诸多的好处:

  • 通用
  • 易于管理
  • 易于搜索
  • 易于迁移

接下来我们用 PoewrShell 来处理 QQ 的聊天记录。目的是将所有的聊天记录按照“组名/对象名.txt”来分别保存每个好友、每个 QQ 群等的聊天记录。

我现在用的是 QQ 6.1 (11905) 版本。依次打开 QQ / 工具 / 消息管理器,点击右上角的倒三角按钮可以看到“导出全部消息记录”菜单项。我们在接下来的对话框里的保存类型中选择“文本文件(*.txt,不支持导入)”,并用默认的“全部消息记录.txt”文件名保存。保存之后的文件内容大概是如下格式:

消息记录(此消息记录为文本格式,不支持重新导入)

================================================================
消息分组:我的好友
================================================================
消息对象:Victor.Woo
================================================================

2010-01-06 16:57:28 Victor.Woo
http://pic4.nipic.com/20090728/1684061_175750076_2.jpg

2010-05-27 12:29:35 Victor.Woo
6块钱包月55
8000/月
中心端,用户端

================================================================
消息分组:技术.关注
================================================================
消息对象:*PowerShell技术交流
================================================================

2013-06-23 15:52:32 此消彼长,云过有痕<qq_g@163.com>
http://yun.baidu.com/buy/center?tag=4#FAQ02

百度亮了,自己找亮点

2013-06-23 18:42:35 Victor.Woo<victorwoo@gmail.com>
[表情]

观察它的规律:

  • ================================================================ 作为每一段的元数据开始。
  • 接下来依次是消息分组、分隔符、消息对象。
  • ================================================================ 作为元数据的结束。
  • 元数据之后,是正文部分,直到下一个元数据开始。
  • 文件头部还有两行无关内容。
  • 文件尺寸巨大,不适合整体用正则表达式来提取,只能一行一行解析。

我们的目标是生成 我的好友/Victor.Woo.txt技术.关注/.PowerShell技术交流.txt

根据这个规律,我们可以用类似“状态机”的思想来设计 PowerShell 脚本。在遍历源文件的所有行时,用一个 $status 变量来表示当前的状态,各个状态的含义如下:

状态 含义
INIT 初始状态
ENTER_BLOCK 进入一个元数据块
ENTER_GROUP “消息分组”解析完成
ENTER_SPLITTER 元数据中间的分隔符解析完成
ENTER_TARGET “消息对象”解析完成
LEAVE_BLOCK 元数据块解析完成
CONTENT 当前行是正文内容

然后用一个 switch 语句让 $status 变量在这些状态之间来回跳转,就能解析出一个一个独立的消息文件了。完整代码如下:

function Get-Status($status, $textLine, $lineNumber, $block) {
    $splitter = '================================================================'
    switch ($status) {
        'INIT' {
            if ($textLine -eq $splitter) {
                $status = 'ENTER_BLOCK'
            }
        }
        'ENTER_BLOCK' {
            if ($textLine -cmatch '消息分组:(.*)') {
              $block.Group = $matches[1]
                $block.Target = $null
                $status = 'ENTER_GROUP'
                break
            } else {
              Write-Error "[$lineNumber] [$status] $textLine"
                exit
            }
        }
        'ENTER_GROUP' {
            if ($textLine -eq $splitter) {
                $status = 'ENTER_SPLITTER'
                break
            } else {
                Write-Error "[$lineNumber] [$status] $textLine"
                exit
            }
        }
        'ENTER_SPLITTER' {
            if ($textLine -cmatch '消息对象:(.*)') {
              $block.Target = $matches[1]
                $status = 'ENTER_TARGET'
                break
            } else {
              Write-Error "[$lineNumber] [$status] $textLine"
                exit
            }
        }
        'ENTER_TARGET' {
            if ($textLine -eq $splitter) {
                $status = 'LEAVE_BLOCK'
                break
            } else {
                Write-Error "[$lineNumber] [$status] $textLine"
                exit
            }
        }
        'LEAVE_BLOCK' {
            if ($textLine -eq $splitter) {
                $status = 'ENTER_BLOCK'
                break
            } else {
                $status = 'CONTENT'
            }
        }
        'CONTENT' {
            if ($textLine -eq $splitter) {
                $status = 'ENTER_BLOCK'
                break
            } else {
                $status = 'CONTENT'
            }
        }
    }

    return $status
}

$status = 'INIT'
$lineNumber = 0
$block = @{}
$targetPath = $null
cat 全部消息记录.txt -Encoding UTF8 | foreach {
    $textLine = $_
    $lineNumber++
    $status = Get-Status $status $textLine $lineNumber $block
    switch ($status) {
        'LEAVE_BLOCK' {
            if ($block.Target -eq '最近联系人') {
                break
            }
            $dirName = $block.Group.Replace('*', '.')
            if (!(Test-Path $dirName)) {
                md $dirName | Out-Null
            }

            $fileName = $block.Target.Replace('*', '.')

            $targetPath = (Join-Path $dirName $fileName) + '.txt'
            if (Test-Path $targetPath) {
                del $targetPath
            }

            echo $targetPath
        }
        'CONTENT' {
            #echo $textLine
            if ($block.Target -eq '最近联系人') {
                break
            }
            Out-File -InputObject $textLine -Encoding utf8 -LiteralPath $targetPath -Append
        }
    }
}

您也可以在这里下载完成后的版本。

如何简洁地汇报 bug

英文:

Given input X I would expect output Y, but your app gives me Z instead.

中文:

我输入了 X,期望输出结果是 Y,而您的程序输出了 Z。

用 PowerShell 屏蔽腾讯 QQ 秀

我们在 用 PowerShell 屏蔽腾讯 QQ 的广告 这篇文章中介绍过了如何屏蔽 QQ 聊天窗口的横幅广告,那么如何屏蔽 QQ 秀的广告呢?请参见 QQ2013 删除QQ秀广告

但是其中的步骤有点繁琐。我们把整个流程用 PowerShell 写一遍,对于用户只要执行一下即可:

$folder = "${env:ProgramFiles}\Tencent\QQ\Plugin\Com.Tencent.QQShow\"
$folder
$rdbFile = Join-Path $folder 'Bundle.rdb'
$rdbDir = Join-Path $folder 'Bundle'

$xmlPath = Join-Path $folder 'Bundle\I18N\2052\UrlBundle.xml'

if (Test-Path "$rdbFile.bak") {
    Write-Warning "$rdbFile.bak 文件已存在,请确认是否已经替换?"
    Write-Warning "程序退出。"
    return
}

$rdbFile
.\RDB.exe """$rdbFile"""
move $rdbFile "$rdbFile.bak"
.\D4QQenc.exe (Join-Path $folder 'Bundle\I18N\2052\UrlBundle.xml.enc')

del (Join-Path $folder 'Bundle\I18N\2052\UrlBundle.xml.enc')

[xml]$urlBundle = Get-Content $xmlPath -Encoding UTF8 | where { $_ -ne '' }

@('IDS_QQSHOW_MARKET', 'IDS_3DSHOW_MARKET', 'IDS_FLASHSHOW_MARKET') | foreach {
    $id = $_
    ($urlBundle.StringBundle.String | where { $_.id -eq $id })."#text" = ''
}
$urlBundle.OuterXml | Set-Content $xmlPath -Encoding UTF8

.\RDB.exe """$rdbDir"""

您也可以从这里 下载 写好的脚本,祝您使用愉快。
本方法在 QQ2013(SP6) 上验证通过。

用 PowerShell 更新 Oray 花生壳动态 IP

花生壳oray 公司提供的 DDNS 客户端。官方的客户端庞大臃肿:

不过好在花生壳开放了基于 http 的 API。这样我们可以很容易地用 PowerShell 实现更新动态 IP 的功能:

param (
    $UserName = 'xxx',
    $Password = 'yyy',
    $HostName,
    $IP
)

function Get-ExternalIP {
    #(Invoke-WebRequest 'http://myip.dnsomatic.com' -UseBasicParsing).Content
    ((Invoke-WebRequest 'http://ddns.oray.com/checkip').ParsedHtml.body.innerText -split ':')[1].Trim()
}

function Update-OrayDdns {
    param (
        [parameter(Mandatory = $true)]
        [string]$UserName,

        [parameter(Mandatory = $true)]
        [string]$Password,

        [parameter(HelpMessage = '需要更新的域名,此域名必须是开通花生壳服务。多个域名使用,分隔,默认为空,则更新护照下所有激活的域名。')]
        [string]$HostName,

        [parameter(HelpMessage = '需要更新的IP地址,可以不填。如果不指定,则由服务器获取到的IP地址为准。')]
        [string]$IP
    )

    $request = 'http://ddns.oray.com/ph/update?hostname={0}' -f ($HostName -join ',')
    if ($IP) {
        $request = $request + '&myip=' + $IP
    }
    $encoded =  [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($UserName+":"+$Password ))
    $headers = @{Authorization = "Basic "+$encoded}
    $response = Invoke-WebRequest $request -Headers $headers -UseBasicParsing

    $codes = @{
        good = '更新成功,域名的IP地址已经更新。'
        nochg = '更新成功,但没有改变IP。一般这种情况为本次提交的IP跟上一次的一样。'
        notfqdn = '未有激活花生壳的域名。'
        nohost = '域名不存在或未激活花生壳。'
        abuse = '请求失败,频繁请求或验证失败时会出现。'
        '!donator' = '表示此功能需要付费用户才能使用,如https。'
        911 = '系统错误'
    }

    $code = ($response.Content -split ' ')[0]
    $message = $codes[$code]

    if ($code -eq 'good' -or $code -eq 'nochg') {
        Write-Output $message
    } elseif ($code -eq 'notfqdn' -or $code -eq 'nohost') {
        Write-Warning $message
    } else {
        Write-Error $message
    }
}

Update-OrayDdns $UserName $Password $HostName

您也可以从这里 下载 写好的脚本。

用 PowerShell 移除 Evernote 的广告

破解过程

Evernote(印象笔记)免费用户的左下角有个正方形的广告,点击关闭按钮反而会出来一个对话框:

虽然破解 + 写脚本 + 写这篇博客花了一两个小时,但是如果能节约更多读者的时间,并且提高一点技术水平,也算有益了吧。

我们用 Visual Studio 中的 Spy++ 查看一下控件的窗口类名:

得到的结果是“ENAdBrowserCtrl”,从窗口类名来看,似乎是为了广告而设计的。出于保险起见,用 WinHex 搜索了一下,这个字符串只出现一次,并且是采用双字节编码的“45004E0041006400420072006F0077007300650072004300740072006C”。

我们尝试破坏这个字符串试试:用 WinHex 的“填充选块”功能,将这块区域替换成 00,然后保存运行,广告果然没有了。

但是手工修改毕竟比较麻烦,而且未来版本更新以后还要再次破解。所以简单写了个 PowerShell 脚本来自动完成破解。

PowerShell 自动化脚本

请将以下代码保存成 Remove-EvernoteAD.ps1 并以管理员身份执行 :)
脚本的思路是以二进制的方式搜索指定的模式(pattern),并替换成新的模式。涉及到一些字节操作和进制转换。

$pattern = '45004E0041006400420072006F0077007300650072004300740072006C'
$replacement = $pattern -replace '.', '0'

function Replace-Pattern ($buffer, $pattern, $replacement) {
    $isPatternMatched = $false
    for ($offset = 6220000; $offset -lt $buffer.Length - $pattern.Length; $offset++) {
        $isByteMatched = $true
        for ($patternOffset = 0; $patternOffset -lt $pattern.Length; $patternOffset++) {
            if ($buffer[$offset + $patternOffset] -ne $pattern[$patternOffset]) {
                $isByteMatched = $false
                break
            }
        }
        if ($isByteMatched) {
            $isPatternMatched = $true
            break
        }
    }

    if ($isPatternMatched) {
        for ($index = 0; $index -lt $pattern.Length; $index++) {
            $buffer[$offset + $index] = [byte]0
        }

        return $true
    } else {
        return $false
    }
}

function Convert-HexStringToByteArray
{
    ################################################################
    #.Synopsis
    # Convert a string of hex data into a System.Byte[] array. An
    # array is always returned, even if it contains only one byte.
    #.Parameter String
    # A string containing hex data in any of a variety of formats,
    # including strings like the following, with or without extra
    # tabs, spaces, quotes or other non-hex characters:
    # 0x41,0x42,0x43,0x44
    # x41x42x43x44
    # 41-42-43-44
    # 41424344
    # The string can be piped into the function too.
    ################################################################
    [CmdletBinding()]
    Param ( [Parameter(Mandatory = $True, ValueFromPipeline = $True)] [String] $String )

    #Clean out whitespaces and any other non-hex crud.
    $String = $String.ToLower() -replace '[^a-f0-9\\\,x\-\:]',''

    #Try to put into canonical colon-delimited format.
    $String = $String -replace '0x|\\x|\-|,',':'

    #Remove beginning and ending colons, and other detritus.
    $String = $String -replace '^:+|:+$|x|\\',''

    #Maybe there's nothing left over to convert...
    if ($String.Length -eq 0) { ,@() ; return }

    #Split string with or without colon delimiters.
    if ($String.Length -eq 1)
    { ,@([System.Convert]::ToByte($String,16)) }
    elseif (($String.Length % 2 -eq 0) -and ($String.IndexOf(":") -eq -1))
    { ,@($String -split '([a-f0-9]{2})' | foreach-object { if ($_) {[System.Convert]::ToByte($_,16)}}) }
    elseif ($String.IndexOf(":") -ne -1)
    { ,@($String -split ':+' | foreach-object {[System.Convert]::ToByte($_,16)}) }
    else
    { ,@() }
    #The strange ",@(...)" syntax is needed to force the output into an
    #array even if there is only one element in the output (or none).
}

echo '本程序用于去除 Evernote 非会员左下角的正方形广告。'
echo '请稍候……'

$patternArray = Convert-HexStringToByteArray $pattern
$replacementArray = Convert-HexStringToByteArray $replacement


$path = "${Env:ProgramFiles}\Evernote\Evernote\Evernote.exe"
$path86 = "${Env:ProgramFiles(x86)}\Evernote\Evernote\Evernote.exe"
if (Test-Path $path) {
    $execute = Get-Item $path
} elseif (Test-Path $path86) {
    $execute = Get-Item $path86
} else {
    Write-Warning '没有找到 Evernote.exe。'
    exit
}

$exe = gc $execute -ReadCount 0 -Encoding byte

if (Replace-Pattern $exe $patternArray $replacementArray) {
    $newFileName = $execute.Name + '.bak'
    $newPath = Join-Path $execute.DirectoryName $newFileName
    Stop-Process -Name Evernote -ErrorAction SilentlyContinue
    Move-Item $execute $newPath
    Set-Content $execute -Value $exe -Encoding Byte
    echo '广告去除成功!Evernote 未来升级后需重新运行本程序。'
    Start-Process $execute
} else {
    Write-Warning '无法去除广告,是否已经去除过了?'
    if (!(Get-Process -Name Evernote -ErrorAction SilentlyContinue)) {
        Start-Process $execute
    }
}

您也可以从这里 下载 写好的脚本。

用 PowerShell 屏蔽腾讯 QQ 的广告

非会员 QQ,在对话窗口的右上角会显示一个广告横幅,如图所示:

我们可以将 %appdata%\Tencent\Users\QQ号\QQ\Misc.db 文件删除并且替换成一个同名文件夹,就可以屏蔽该广告:

如果您有多个 QQ 号的话,我们可以用 PowerShell 来批量完成该任务:

echo '本脚本用于屏蔽 QQ 对话窗口右上方的广告条。'
Read-Host '请关闭所有 QQ,按回车键继续' | Out-Null

$usersDir = "$($env:AppData)\Tencent\Users\"
dir $usersDir -Directory | foreach {
    $qq = $_
    $qqDir = Join-Path $_.FullName 'QQ'
    $miscDb = Join-Path $qqDir Misc.db
    if (Test-Path -PathType Leaf $miscDb) {
        echo "正在禁用 $qq 的广告"
        del $miscDb
        md $miscDb | Out-Null
    }
}
exit
echo '处理完毕。'

您也可以从这里 下载 写好的脚本,祝您使用愉快。
本方法在 QQ2013(SP6) 上验证通过。

手动控制 VMware 服务

VMware Workstation 想必是很多朋友的必装软件,强大的虚拟机功能已经不用多解释了。这里提点小小的内存优化建议,就是我们在安装完 VMware Workstation 之后,它默认是开机自启动的。那有人会说,打开msconfig,在启动项里将它关闭不就行了吗?其实不然,VMware的几个进程都是以服务方式启动的,vmware-authd.exe、vmnetdhcp.exe、vmnat.exe等等,如不经处理,它们会常驻在系统内存中。而我们并不是每天都会使用虚拟机,所以那些进程大部分时间是在浪费我们的系统资源。

但如果在服务里面将它们全部禁用,那么 VMware 也就不能使用了。最好的方法就是打开服务管理器,将它的几个服务项先全部右击停止,然后双击进去,在启动类型中改为“手动”。这样一来,开机就不会自动启动了。那么,要开 VMware 的时候怎么办呢?一个个手工开启?没必要,写个 PowerShell 脚本就可以了,我用的是最新版VMware Workstation 10,代码如下:

将所有 VMware 服务设置为手动:

# Set-VMWareServiceToManual.ps1
Get-Service -DisplayName vmware* | % {
    Set-Service -Name $_.Name -StartupType Manual
}

将所有 VMware 服务设置为自动(缺省):

# Set-VMWareServiceToAuto.ps1
Get-Service -DisplayName vmware* | % {
    Set-Service -Name $_.Name -StartupType Automatic
}

启动所有 VMware 服务(准备运行 VMware 的时候):

# Start-VMWareService.ps1
Get-Service -DisplayName vmware* | % {
    Start-Service -Name $_.Name
}

停止所有 VMware 服务(运行 VMware 完毕以后):

# Stop-VMWareService.ps1
Get-Service -DisplayName vmware* | % {
    Stop-Service -Name $_.Name -Force
}

下载地址:VMWareService.zip

快速生成树形结构的纯文本

今天帮朋友整理一些材料,需要为这些材料整理一个目录。之前有研究过一些方案,例如:

这些方案有一个共性:麻烦。也就是无法像手头的工具一样拿来就用。于是发掘了一番,发现 tree 这个 dos 时代的命令刚好能满足需要。该命令的帮助如下:

以图形显示驱动器或路径的文件夹结构。

TREE [drive:][path] [/F] [/A]

   /F   显示每个文件夹中文件的名称。
   /A   使用 ASCII 字符,而不使用扩展字符。

我们可以用以下命令将 D:\work 下的结构输出到 output.txt 文本文件:

TREE "D:\work" /F /A > output.txt

然后用记事本之类的文本编辑器对它进行简单的编辑,就可以达到目的。

还可以拓展一下思路:在撰写文章的时候,常常需要描述一个有层次的结构(可以是心得体会之类的,不仅限于描述一系列文件)。此时可以在硬盘里创建一个临时目录,在里面创建一些文件夹和文件,用资源管理器拖拽调整目录结构,然后用上述命令导出一个目录文件,就可以快速地用于文档的撰写了。请不要徒手编辑这样的文本,因为那样很愚蠢,调整起来也相当费功夫。

命令执行效果参考:

卷 os 的文件夹 PATH 列表
卷序列号为 0000002C 000E:BD6F
C:.
|   HaxLogs.log
|   setmockup.log
|   WEVTUTIL.exe
|
+---adt-bundle-windows-x86
|   |   SDK Manager.exe
|   |
|   +---android-ndk-r9
|   |   |   documentation.html
|   |   |   GNUmakefile
...
|   |   |   README.TXT
|   |   |   RELEASE.TXT
|   |   |
|   |   +---build
|   |   |   +---awk
|   |   |   |       check-awk.awk
|   |   |   |       extract-debuggable.awk

用 PowerShell 脚本获取天气实况

只要两行命令,就可以“轻松”地获取实时天气预报:

(curl http://61.4.185.48:81/g/ -UseBasicParsing).Content -cmatch 'var id=(\d+);' | Out-Null
irm "http://www.weather.com.cn/data/sk/$($matches[1]).html" | select -exp weatherinfo

使用效果:

PS >(curl http://61.4.185.48:81/g/ -UseBasicParsing).Content -cmatch 'var id=(\d+);' | Out-Null
PS >irm "http://www.weather.com.cn/data/sk/$($matches[1]).html" | select -exp weatherinfo


city    : 福州
cityid  : 101230101
temp    : 15
WD      : 北风
WS      : 2级
SD      : 79%
WSE     : 2
time    : 10:20
isRadar : 1
Radar   : JC_RADAR_AZ9591_JB

您还可以把第二行改为以下形式,获取更猛的数据:

irm "http://m.weather.com.cn/data/$($matches[1]).html" | select -exp weatherinfo

或:

irm "http://www.weather.com.cn/data/cityinfo/$($matches[1]).html" | select -exp weatherinfo

源代码下载

顺便透露一下,高富帅一般不这么看天气预报哦!

用 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

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