用 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为关闭。需要管理员权限。

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

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

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

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

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

Clover 3 为您的 Windows Explorer 插上翅膀!

Clover 3为您的Windows Explorer插上翅膀!

Clover 是 Windows Explorer 资源管理器的一个扩展,为其增加类似谷歌 Chrome 浏览器的多标签页功能。

Clover 3

官方网站:易捷科技

Version:
3.0.386

Requirements:
Windows XP / Windows 7 / Windows 8

Language:
English / 简体中文 / 繁體中文 / 日本語 / Français / Español / Deutsch / Nederlands / Português

获取2013年剩余天数

用PowerShell获取2013年剩余天数的两种写法。

((Get-Date 2014-1-1) - (Get-Date)).Days
100

([datetime]"2014-1-1" - [datetime]::now).Days
100

顺便励志一下,2013年只剩下100天了。Come on,小伙伴们!

定时休眠的命令

例如2个小时以后休眠:

timeout /t 7200 /nobreak & shutdown /h

注意事项:

  1. shutdown /h /t xxx 这样的组合是没用的。
  2. 注意倒计时过程中不能按 CTRL+C 组合键来中止倒计时,否则会立即休眠。正确中止倒计时的方法是直接关闭命令行窗口。

修正文件名/目录名的PowerShell脚本

计划写一系列整理文件用的脚本。例如根据id3来对mp3文件归档、根据exif信息来对照片归档、根据verycd上的资源名称对下载的文件归档……
这时候会遇到一个问题:Windows的文件系统是不允许某些特殊字符,以及设备文件名的。详细的限制请参见:http://zh.wikipedia.org/wiki/%E6%AA%94%E6%A1%88%E5%90%8D%E7%A8%B1。

这个PowerShell脚本帮助你避开这些坑。具体的做法是将特殊字符替换成’.’,对于恰好是设备名称的主文件名或扩展名之前添加’_’。

function Get-ValidFileSystemName
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]$FileSystemName
    )

    process{
        $deviceFiles = 'CON', 'PRN', 'AUX', 'CLOCK$', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9'
        $fileName = [System.IO.Path]::GetFileNameWithoutExtension($FileSystemName)
        $extension = [System.IO.Path]::GetExtension($FileSystemName)
        if ($extension.StartsWith('.'))
        {
            $extension = $extension.Substring(1)
        }

        if ($deviceFiles -contains $fileName)
        {
            $fileName = "_$fileName"
        }

        if ($deviceFiles -contains $extension)
        {
            $extension = "_$extension"
        }

        if ($extension -eq '')
        {
            $FileSystemName = "$fileName$extension"
        }
        else
        {
            $FileSystemName = "$fileName.$extension"
        }

        $FileSystemName = $FileSystemName -creplace '[\\/|?"*:<>\x00\x1F\t\r\n]', '.'
        return $FileSystemName
    }
}

以GTD的思想整理目录的PowerShell脚本

这是花了一个晚上写的PowerShell脚本,可以把你的目录以GTD的思想整理得井井有条。
但是GTD功能已经完整并通过测试。github地址:victorwoo/Get-ThingsDone

不了解GTD的同学请参考以下材料:
[《搞定》中文版] 1、[《Get Things Done》英文版] 2

#========================================================================
# Created on:   2013-3-7 16:21
# Created by:   Victor.Woo
# Organization: www.vichamp.com
# Filename:  Get-ThingsDone.ps1
#========================================================================

function Check-Enviroment
{
  $gtdPath = "HKCU:\Software\Vichamp\GTD"
  if ((Get-ItemProperty $gtdPath -ErrorAction SilentlyContinue).AutoStart -eq "False")
  {
    return
  }

  $runPath = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run'
  $run = Get-ItemProperty $runPath
  if ($run.GTD -eq $null)
  {
    $title = '自动执行请求'
    $message = '在当前 Windows 用户登录时自动运行此脚本,可以自动帮助您整理、规划当日的工作内容。如果您选择了“是”,但将来不希望自动启动,请执行 uninstall.cmd。是否在当前用户登录时自动执行脚本?'
    $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes","Windows 用户登录时自动运行此脚本。"
    $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No","Windows 用户登录时不运行此脚本,并且不再提示。"
    $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes,$no)
    $result = $Host.UI.PromptForChoice($title,$message,$options,0)
    switch ($result)
    {
      0 {
        Set-ItemProperty -Path $runPath -Name GTD -Value $gtdCmd
      }
      1 {
        md $gtdPath -Force
        Set-ItemProperty -Path $gtdPath -Name AutoStart -Value "False"
      }
    }
  }
}

function TryCreate-Directory ([Parameter(Mandatory = $True)] [string]$dirName)
{
  $private:dir = Join-Path $baseDir $dirName
  if (-not (Test-Path $dir))
  {
    Write-Output "$dir 不存在,正在创建。"
    mkdir $dir | Out-Null
  }
}

function TryCreate-Directories ()
{
  Write-Output "正在检查目录完整性"
  $dirNames |
  % {
    TryCreate-Directory $_
  }
}

function Remove-Directories ()
{
  $dirNames |
  % {
    $private:dir = Join-Path $baseDir $_
    if (Test-Path $dir)
    {
      Write-Warning "正在移除$dir"
      rm $dir -Recurse
    }
  }
}

function MoveTo-WithRenamming (
  [Parameter(Mandatory = $True)] [System.IO.FileSystemInfo]$item,
  [Parameter(Mandatory = $True)] [string]$targetDir)
{
  function Get-NextFilePath ([string]$dir,[System.IO.FileInfo]$fileInfo)
  {
    $Private:targetFilePath = Join-Path $dir $fileInfo.Name
    if (Test-Path $Private:targetFilePath)
    {
      $Private:index = 1
      do {
        $Private:targetFilePath = Join-Path $dir "$($fileInfo.BaseName) ($index)$($fileInfo.Extension)"
        $Private:index++
      }
      while (Test-Path $Private:targetFilePath)
    }
    return [System.IO.FileInfo]$Private:targetFilePath
  }

  function Get-NextDirectoryPath ([string]$dir,[System.IO.DirectoryInfo]$directoryInfo)
  {
    $Private:targetDirectoryPath = Join-Path $dir $directoryInfo.Name
    if (Test-Path $Private:targetDirectoryPath)
    {
      $Private:index = 1
      do {
        $Private:targetDirectoryPath = Join-Path $dir "$($directoryInfo.Name) ($index)"
        $Private:index++
      }
      while (Test-Path $Private:targetDirectoryPath)
    }
    return [System.IO.DirectoryInfo]$Private:targetDirectoryPath
  }

  Write-Output "正在移动 $item 至 $targetDir 目录"
  if ($item -is [System.IO.FileInfo])
  {
    # 待移动的是文件
    [System.IO.FileInfo]$item = [System.IO.FileInfo]$item
    $Private:targetFilePath = Join-Path $targetDir $item.Name
    if (Test-Path $Private:targetFilePath)
    {
      # 目标文件已存在
      $targetFileInfo = [System.IO.FileInfo]$Private:targetFilePath
      $Private:targetFilePath = Get-NextFilePath $targetDir $item

      if ($item.LastWriteTime -eq $targetFileInfo.LastWriteTime -and $item.Length -eq $targetFileInfo.Length)
      {
        # 文件时间和大小相同
        Write-Warning "源文件 $item.FullName 与目标文件 $targetFileInfo.FullName 相同,删除源文件"
        Remove-Item $item.FullName
      }
      else
      {
        Write-Warning "目标文件已存在,自动改名为$($Private:targetFilePath.Name)"
        Move-Item $item.FullName $Private:targetFilePath | Out-Null
      }
    }
    else
    {
      # 目标文件不存在
      if (!(Test-Path $targetDir))
      {
        # 目标目录不存在,创建目标目录
        md $targetDir | Out-Null
      }
      Move-Item $item.FullName $Private:targetFilePath | Out-Null
    }
  } elseif ($item -is [System.IO.DirectoryInfo])
  {
    # 待移动的是目录
    [System.IO.DirectoryInfo]$item = [System.IO.DirectoryInfo]$item
    $Private:targetDirectoryPath = Join-Path $targetDir $item.Name
    if (Test-Path $Private:targetDirectoryPath)
    {
      $Private:targetDirectoryPath = Get-NextDirectoryPath $targetDir $item
      Write-Warning "目标文件夹已存在,自动改名为$($Private:targetDirectoryPath.Name)"
    }
    Move-Item $item.FullName $Private:targetDirectoryPath | Out-Null
  }
}

function Process-IsolatedItems
{
  Write-Output "正在将游离内容移至 [STUFF] 目录"
  Get-ChildItem $baseDir -Exclude ($dirNames + $reservedDirs + $reservedFiles) |
  % {
    MoveTo-WithRenamming $_ $stuffDir
  }
}

function Process-TomorrowDir
{
  Write-Output "正在处理 [TOMORROW] 目录"
  Get-ChildItem $tomorrowDir |
  % {
    MoveTo-WithRenamming $_ $todayDir
  }
}

function Process-CalendarDir
{
  Write-Output "正在处理 [CALENDAR] 目录"
  Get-ChildItem $calendarDir -File |
  % {
    MoveTo-WithRenamming $_ $stuffDir
  }

  Get-ChildItem $calendarDir -Directory |
  % {
    $regex = [regex]'(?m)^(?<year>19|20\d{2})[-_.](?<month>\d{1,2})[-_.](?<day>\d{1,2})$'
    $match = $regex.Match($_.Name)
    if ($match.Success)
    {
      $Private:year = $regex.Match($_.Name).Groups['year'].Value;
      $Private:month = $regex.Match($_.Name).Groups['month'].Value;
      $Private:day = $regex.Match($_.Name).Groups['day'].Value;
      $Private:date = New-Object System.DateTime $Private:year,$Private:month,$Private:day
      $now = (Get-Date)
      $today = $now.Subtract($now.TimeOfDay)
      if ($date -lt $today)
      {
        Write-Output "移动过期任务 $($_.Name) 到 [STUFF] 目录"
        MoveTo-WithRenamming $_ $stuffDir
      }
      elseif ($date -eq $today)
      {
        Write-Output "移动今日任务 $($_.Name) 到 [TODAY] 目录"
        MoveTo-WithRenamming $_ $todayDir
      }
      elseif ($date -eq $today.AddDays(1))
      {
        Write-Output "移动明日任务 $($_.Name) 到 [TOMORROW] 目录"
        MoveTo-WithRenamming $_ $tomorrowDir
      }
    }
    else
    {
      Write-Output "[CALENDAR] 目录下,$($_.Name) 名字不符合规范,将移动至 [STUFF] 目录"
      MoveTo-WithRenamming $_ $stuffDir
    }
  }
}

function Process-ArchiveDir
{
  Write-Output "正在检查 [ARCHIVE] 目录"

  # 创建本月目录
  $nowString = "{0:yyyy.MM}" -f (Get-Date)
  $thisMonthDir = Join-Path $archiveDir $nowString
  if (-not (Test-Path $thisMonthDir))
  {
    Write-Output "正在创建本月目录"
    md $thisMonthDir
  }

  # 移除除本月之外的空目录
  Get-ChildItem $archiveDir -Exclude $nowString -Recurse |
  Where { $_.PSIsContainer -and @( Get-ChildItem -LiteralPath $_.FullName -Recurse | Where { !$_.PSIsContainer }).Length -eq 0 } |
  % {
    Write-Output "正在删除空目录$($_.FullName)"
    Remove-Item -Recurse
  }

  # 移动所有文件到 本月存档 目录
  Get-ChildItem $archiveDir -File |
  % {
    $lastWriteTime = "{0:yyyy.MM}" -f $_.LastWriteTime
    $lastWriteDir = Join-Path $archiveDir $lastWriteTime
    Write-Output "移动 [ARCHIVE] 目录下,$($_.Name) 游离文件至 $lastWriteDir 存档目录"
    MoveTo-WithRenamming $_ $lastWriteDir
  }

  # 检查目录命名是否符合规范。
  Get-ChildItem $archiveDir -Directory |
  % {
    $regex = [regex]'(?m)^(?<year>19|20\d{2})[-_.](?<month>\d{1,2})$'
    $match = $regex.Match($_.Name)
    if ($match.Success)
    {
      # Archive目录下的名字符合格式
      $year = $regex.Match($_.Name).Groups['year'].Value;
      $month = $regex.Match($_.Name).Groups['month'].Value;
      $date = New-Object System.DateTime $year,$month,1
      if ($date -gt (Get-Date))
      {
        Write-Output "[ARCHIVE] 目录下,$($_.Name) 名字不符合规范(存档日期超出当前时间),将移动至 [STUFF] 目录"
        MoveTo-WithRenamming $_ $stuffDir
      }
      else
      {
        $formattedDate = "{0:yyyy.MM}" -f $date
        if ($_.Name -ne $formattedDate)
        {
          $targetDirectory = [System.IO.DirectoryInfo](Join-Path $_.Parent.FullName $formattedDate)
          Write-Warning "将 [ARCHIVE] 下的目录名 $($_.Name) 处理为规范格式 $($targetDirectory.Name)"
          Move-Item $_.FullName $targetDirectory.FullName
        }
      }
    } else
    {
      # Archive目录下的名字符不合格式
      $lastWriteTime = $nowString = "{0:yyyy.MM}" -f $_.LastWriteTime
      $lastWriteDir = Join-Path $archiveDir $lastWriteTime
      Write-Output "移动 [ARCHIVE] 目录下,$($_.Name) 游离文件夹至 $lastWriteDir 存档目录"
      MoveTo-WithRenamming $_ $lastWriteDir
    }
  }
}

function Explore-Dirs
{
  if ((Get-ChildItem $stuffDir) -ne $null)
  {
    explorer $stuffDir
  }

  if ((Get-ChildItem $todayDir) -ne $null)
  {
    explorer $todayDir
  }
}

$STUFF = "1.STUFF"
$TODAY = "2.TODAY"
$TOMORROW = "3.TOMORROW"
$UPCOMING = "4.UPCOMING"
$CALENDAR = "5.CALENDAR"
$SOMEDAY = "6.SOMEDAY"
$ARCHIVE = "7.ARCHIVE"

$dirNames = $STUFF,$TODAY,$TOMORROW,$UPCOMING,$CALENDAR,$SOMEDAY,$ARCHIVE
$reservedDirs = ".git","_gsdata_"
$reservedFiles = ".gitignore","Get-ThingsDone.ps1","README*.md","gtd_logo.png","LICENSE.md","GTD.cmd","uninstall.cmd"

$baseDir = Split-Path $MyInvocation.MyCommand.Path
$stuffDir = Join-Path $baseDir $STUFF
$todayDir = Join-Path $baseDir $TODAY
$tomorrowDir = Join-Path $baseDir $TOMORROW
$calendarDir = Join-Path $baseDir $CALENDAR
$archiveDir = Join-Path $baseDir $ARCHIVE
$gtdCmd = Join-Path $baseDir "GTD.cmd"

Get-Date | Write-Output

Check-Enviroment
TryCreate-Directories

Process-IsolatedItems
Process-TomorrowDir
Process-CalendarDir
Process-ArchiveDir

Explore-Dirs

######################### 开发临时用(在 ISE 中选中并按 F8 执行) #########################
return

{
  return
  # 创建游离内容。
  $null | Set-Content (Join-Path $baseDir "to.del.file.txt")
  md (Join-Path $baseDir "to.del.dir") | Out-Null
}

{
  return
  # 对代码排版。
  Import-Module D:\Dropbox\script\DTW.PS.PrettyPrinterV1\DTW.PS.PrettyPrinterV1.psd1
  Edit-DTWCleanScript D:\Dropbox\vichamp\GTD\Get-ThingsDone.ps1
}

{
  return
  # 移除所有目录
  Remove-Directories
}

在vim里转换HEX数据的显示

在做嵌入式设备开发和调试工作中,可能常常遇到以下场景,需要把:

0000:60 00 00 00 00 00 00 00  00 00 00 42 00 00 18 00  `....... ...B....:0015
0016:01 80 80 00 03 14 20 24  20 11 08 17 08 12 34 56  ...... $  .....4V:0031
0032:78 08 87 65 43 21 36 35  30 34 31 37 31 38 00 59  x..eC!65 041718.Y:0047
0048:30 30 30 30 30 30 30 30  30 30 30 31 30 30 30 30  00000000 00010000:0063
0064:30 30 30 30 49 43 41 52  44 30 30 30 30 30 30 30  0000ICAR D0000000:0079
0080:34 30 30 30 30 30 30 31  32 31 36 30 30 30 30 30  40000001 21600000:0095
0096:30 34 30 30 30 30 32 30  34 30 35 31 31 31 31 31  04000020 40511111:0111
0112:31 31 31                                          111              :0127

这样的文本,转换为单行,头尾部分不要,中间重复的空格也不要。如下:

60 00 00 00 00 00 00 00 00 00 00 42 00 00 18 00 01 80 80 00 03 14 20 24 20 11 08 17 08 12 34 56 78 08 87 65 43 21 36 35 30 34 31 37 31 38 00 59 30 30 30 30 30 30 30 30 30 30 30 31 30 30 30 30 30 30 30 30 49 43 41 52 44 30 30 30 30 30 30 30 34 30 30 30 30 30 30 31 32 31 36 30 30 30 30 30 30 34 30 30 30 30 32 30 34 30 35 31 31 31 31 31 31 31 31

这个可以用vim的替换来实现,命令是:
:%s/\d*:\(\(\x\|\s\)\{24}\) \(\(\x\|\s\)\{24}\).*\n/\1\3/g

我们还可以为它在.vimrc中定义一个快捷键映射:
map <C-H> :%s/\d*:\(\(\x\\|\s\)\{24}\) \(\(\x\\|\s\)\{24}\).*\n/\1\3/g<CR>
这样以后按一下CTRL+H键就搞定了。