経緯
掃除中に見つかったメモリースティックには、10年くらい前にバックアップした携帯電話メール(VMG ファイル)がたくさん入っていた。
で、VMG ファイルをひとつひとつメモ帳で開いて読んで、懐かしさにひたっていたのだけど、なにぶん量がたくさんあるので、一気読みできるよう、まとめてエクセルのワークシートに書き出す PowerShell スクリプトを作ってみることにした。
改めて思ったけど、あんまり昔のメールを何でもかんでも後生大事に取っておくのもいいのか悪いのか。過去に引きずられすぎてもいけないし、過去の反省は未来に生かしたいし。ひさびさにセンチメンタルな気持ちになってしまったのと同時に、これからとこれまでの年月の重みを感じた。
データのフロー
携帯電話のメール
↓ バックアップ
VMG ファイル(たくさん)
↓ PowerShell
エクセルの表
事前調査
VMG ファイルのフォーマット
VMG ファイルは、こんな内容だった。
BEGIN:VMSG
VERSION:1.1
X-IRMC-STATUS:READ
X-IRMC-TYPE:INET
X-IRMC-BOX:INBOX
BEGIN:VCARD
VERSION:2.1
N:
TEL:
END:VCARD
BEGIN:VENV
BEGIN:VCARD
VERSION:2.1
N:
TEL:
END:VCARD
BEGIN:VENV
BEGIN:VBODY
From:*************@docomo.ne.jp
To:
Date:29 Jan 2003 22:41:00 +0900
Subject:=?shift-jis?B?UmU6UmU6?=
MIME-Version:1.0
Content-Type:text/plain;charset="shift-jis"
Content-Transfer-Encoding:8bitここが本文
END:VBODY
END:VENV
END:VENV
END:VMSG
検索してみたところ、こういうデータは、vMessage と呼ばれているみたいだけど、固まった仕様があるのかどうか不明。日本国内の携帯電話で一般に使われているとか。vCard というのは聞いたことがあるけど。
とりあえず「BEGIN:VBODY」と「END:VBODY」の間にある文字列をメールのデータとして扱うことにした。
MIME ヘッダーのフォーマット
VMG ファイルに含まれているメールのデータ中、Subject ヘッダーが読めない形式になっていたため、これを元の文字列に戻す必要がある。読めないというのは、つまりこういう形式。
Subject:=?shift-jis?B?UmU6UmU6?=
何となく Shift-JIS のバイトを Base64 でエンコードしたものかな?という感じだけど、Base64 にはクエスチョンマークなんて現れないし、変な方言なんだろうかと思っていたところ、Wikipedia によると、これは、
=?charset?encoding?encoded-text?=
という意味であって、MIME のヘッダで ASCII 以外の符号化された文字列を扱う場合には、この表現を使うらしい。
上記の例だと、実際の件名は、「UmU6UmU6」の部分を Base64 でデコードして、Shift-JIS にエンコードしたらよいということ。
PowerShell で Base64 をデコードする方法
.NET Framework のライブラリで見つかった。
System.Convert クラスの FromBase64String メソッドを使えば、Base64 の文字列→バイト配列のデコードができる。
PowerShell で Shift-JIS にエンコードする方法
これも .NET Framework のライブラリにあった。
System.Text.Encoding オブジェクトの GetString メソッドを使って、バイト配列→文字列にエンコードできる。
で、Shift-JIS 用の Encoding オブジェクトは、Encoding クラスの GetEncoding メソッドに 932 を渡して取得する。Shift-JIS 用というか、きっと CP932 用だね。
PowerShell でテキストファイルの全内容を文字列で取得する方法
テキストファイルの内容を取得するには、Get-Content コマンドレットを使う方法もあるけど、一行ずつしか読めない。
今回はファイルの全内容をいったんひとつの文字列に読み込んで、そのあと正規表現を使って必要な情報を抽出しようと考えているので、一行ずつだと、いちいち Join か何かひと手間余分にかかる。
そこで、System.IO.StreamReader オブジェクトの ReadToEnd メソッドを使うことにした。
StreamReader クラスは、使うシーンから見て、Java の java.io.BufferedReader っぽい気がする。
PowerShell で正規表現を使う方法
これも .NET Framework のクラスを使うのかなと思っていら、言語レベルで正規表現に対応していて、-match という演算子が使えるらしい。
PS C:\Users\daisuke> “Date:29 Jan 2003 22:41:00 +0900” ?match “Date:(.*)” True PS C:\Users\daisuke> $Matches Name Value ---- ----- 1 29 Jan 2003 22:41:00 +0900 0 Date:29 Jan 2003 22:41:00 +0900
こんなふうに使う。-match 演算子を使った式自体は真偽値を返して、後方参照をするときには、$Matches[1] といったように、専用の変数を使う。
PowerShell でエクセルのワークシートに書き出す方法
最初は Excel.Application のオブジェクトを作って、ワークシートをごりごり作ろうと考えていたけど、ConvertTo-CSV というコマンドレットを使えば、オブジェクト配列を CSV に変換できるということがわかった。
簡単そうなので、このコマンドレットを使ってみる。
コード(PowerShell 2.0)
できたー。
VMG ファイルの入っているフォルダの名前を、引数に指定するか、コードの1行目を書き変えて実行。
処理結果の CSV 形式のデータは標準出力に出力されるため、ファイルとして保存するときは、リダイレクトを指定して実行する。
param($path = "G:\MSMELCO\RECV\FOLDER"); $enc = [Text.Encoding]::GetEncoding(932); function isDirectory([IO.FileSystemInfo] $test) { return ($test.Attributes -band [IO.FileAttributes]::Directory) -eq [IO.FileAttributes]::Directory; } function ExtractMailContent() { process { if(isDirectory($_)) { return; } # ファイルのテキスト全てを$contentに読み込む $reader = New-Object IO.StreamReader($_.FullName, $enc); $content = $reader.ReadToEnd(); $reader.Close(); # VBODYのブロックがメールの目印 if($content -match "BEGIN:VBODY([\s\S]*?)END:VBODY") { $m = $Matches[1]; # メールのデータを保管するオブジェクトを初期化 $mail = New-Object PSObject; $mail | Add-Member -MemberType NoteProperty -Name "file" -Value $_.Name; $mail | Add-Member -MemberType NoteProperty -Name "from" -Value $null; $mail | Add-Member -MemberType NoteProperty -Name "date" -Value $null; $mail | Add-Member -MemberType NoteProperty -Name "subject" -Value $null; $mail | Add-Member -MemberType NoteProperty -Name "body" -Value $null; # Fromヘッダーがあったら発信者の値を読み込む if($m -match "From:(.*)\r\n") { $mail.from = $Matches[1]; } # Dateヘッダーがあったら発信日時の値を読み込む if($m -match "Date:(.*)\r\n") { $mail.date = [System.DateTime]::Parse($Matches[1]); } # Subjectヘッダーがあったら件名の値を読み込む if($m -match "Subject:(.*)\r\n") { $subject = $Matches[1]; if($subject -match "=\?shift\-jis\?B\?(.*)\?=") { $mail.subject = $enc.GetString([Convert]::FromBase64String($Matches[1])); } else { $mail.subject = $subject; } } # 2回改行しているところがあったら本文を読み込む if($m -match "\r\n\r\n([\s\S]*)$") { $mail.body = $Matches[1]; } return $mail; } } } Get-ChildItem $path | ExtractMailContent | ConvertTo-CSV -Delimiter "`t" -NoTypeInformation
つまづきがちなポイント
正規表現の「.」は改行にはマッチしない
最初、絶対マッチするはずなのにマッチしないなーと思っていたら、改行挟んでいた。ありがちといえばありがち。
PowerShell のエスケープ文字は「`」
出力する CSV の列区切りにタブを指定したとき、最初「\t」ってやって怒られた。正解は「`t」。
Hashtable の配列を ConvertToCSV に渡しても、期待した通りにはならない
最初に作ったスクリプトでは、メールのデータを System.Collections.Hashtable で保管して、その配列を ConvertTo-CVS コマンドレットに渡していた。
ところが Hashtable オブジェクト自身のプロパティが CSV に出力してしまう。例えば IsReadOnly とか、Keys とか Values とか。そんなもの出力されても困る。
あれこれ調査した結果、このページを参考にして、PSObject という オブジェクトを使うことで解決した。
PSObject オブジェクトは、Javascript でオブジェクトを扱うように、プロパティやメソッドを動的に追加することができるものぽい。
参考文献
- Multipurpose Internet Mail Extensions – Wikipedia <http://ja.wikipedia.org/wiki/Multipurpose_Internet_Mail_Extensions>
- Hash Table to Export-CSV – PowerShellCommunity.org – Windows PowerShell Discussion Forums – Using PowerShell – General PowerShell <http://www.powershellcommunity.org/Forums/tabid/54/aff/1/aft/5459/afv/topic/Default.aspx>
- Hey, Scripting Guy! Windows PowerShell 1.0 のスクリプトを Windows PowerShell 2.0 で使用する方法はありますか | 2009/10 <http://technet.microsoft.com/ja-jp/scriptcenter/ff458162.aspx>