Web上にスクリプトを置いて、PowerShellで実行可能にしてみた（その2／住所検索編）

Web上にスクリプトを置いて、PowerShellで実行可能にしてみました。
第二弾は、郵便番号から住所検索をしてみました。
※ご利用は各自の責任でお願いします。
　また、スクリプトは変更する可能性があります。

※不明点などあれば、コメントでご質問頂けるとありがたいです。
（返信には時間がかかるかもしれませんが、できるだけ早くお答えできるようにします）


#使用例

```ps1
#PowerShellを起動し以下コマンドを実行
function Invoke-RemoteScript($url){
  $script = (New-Object Net.WebClient).DownloadString($url)
  Invoke-Expression("&{$script} $args")
}

#例１．初回のみ、住所検索用データベースを構築する（構築先はC:\nancus_invoker\find_address）
Invoke-RemoteScript 'http://nancus.html.xdomain.jp/scripts/find_address.ps1' '530000%' `
| %{'〒' + $_.postal_code.Substring(0,3) + "-" + $_.postal_code.Substring(3,4) + ' ' + $_.prefecture_name + $_.city_name + $_.postal_term_name}

#例２．住所検索用データベースを再構築する（構築先はC:\nancus_invoker\find_address）
Invoke-RemoteScript 'http://nancus.html.xdomain.jp/scripts/find_address.ps1' '530000%' '-renew'`
| %{'〒' + $_.postal_code.Substring(0,3) + "-" + $_.postal_code.Substring(3,4) + ' ' + $_.prefecture_name + $_.city_name + $_.postal_term_name}

#例３．初回のみ、住所検索用データベースを構築する（構築先はC:\custom_work\find_address）
Invoke-RemoteScript 'http://nancus.html.xdomain.jp/scripts/find_address.ps1' '530000%' '-app_dir C:\custom_work'`
| %{'〒' + $_.postal_code.Substring(0,3) + "-" + $_.postal_code.Substring(3,4) + ' ' + $_.prefecture_name + $_.city_name + $_.postal_term_name}

#例４．住所検索用データベースを再構築する（構築先はC:\custom_work\find_address）
Invoke-RemoteScript 'http://nancus.html.xdomain.jp/scripts/find_address.ps1' '530000%' '-app_dir C:\custom_work' '-renew'`
| %{'〒' + $_.postal_code.Substring(0,3) + "-" + $_.postal_code.Substring(3,4) + ' ' + $_.prefecture_name + $_.city_name + $_.postal_term_name}

#↓期待される結果（2015/05/17時点）↓
#〒530-0000 大阪府大阪市北区以下に掲載がない場合
#〒530-0001 大阪府大阪市北区梅田
#〒530-0002 大阪府大阪市北区曽根崎新地
#〒530-0003 大阪府大阪市北区堂島
#〒530-0004 大阪府大阪市北区堂島浜
#〒530-0005 大阪府大阪市北区中之島（次のビルを除く）
```


#ソースコード

呼び出し先のPowerShellはこのようになっています。

```ps1:find_address.ps1
#住所検索
param(
  [Parameter(Mandatory=$true)]$postal_code,
  $app_dir = "C:\nancus_invoker",
  [switch]$renew
)
$work_dir   = Join-Path $app_dir  "find_address"
$saved_data = Join-Path $work_dir "saved_data.json"

function Invoke-RemoteScript($url){
  $script = (New-Object Net.WebClient).DownloadString($url)
  Invoke-Expression("&{$script} $args")
}

#作業フォルダをクリア
if([System.IO.Directory]::Exists($work_dir) -and $renew.IsPresent){
  Remove-Item $work_dir -Recurse | Out-Null
}

#作業フォルダを作成
if(![System.IO.Directory]::Exists($work_dir)){
  New-Item $work_dir -ItemType Directory | Out-Null

  #SQLite3（データベースエンジン）をローカルで実行可能にする
  $exe_sqlite3 =
    Invoke-RemoteScript 'http://nancus.html.xdomain.jp/scripts/prepare_sqlite3.ps1' $work_dir

  #郵便番号一覧をローカルで読込可能にする
  $csv_postal_codes =
    Invoke-RemoteScript 'http://nancus.html.xdomain.jp/scripts/prepare_postal_codes.ps1' $work_dir

  #郵便番号一覧をSQLite3データベースに変換
  $db_postal_codes  = ($csv_postal_codes + ".sqlite3")

@"
CREATE TABLE mst_postal_codes(
  japanese_local_goverment_code,
  old_postal_code,
  postal_code,
  prefecture_name_kana,
  city_name_kana,
  postal_term_name_kana,
  prefecture_name,
  city_name,
  postal_term_name,
  flg_has_plural_postal_codes,
  flg_small_admin_units_have_each_house_numbers,
  flg_has_city_blocks,
  flg_postal_code_has_plural_postal_terms,
  flg_updated,
  update_reason_code
);

.mode csv
.import $($csv_postal_codes -replace "\\","\\" <# \から\\へ #>) mst_postal_codes
"@ | & $exe_sqlite3 $db_postal_codes

  #中間ファイルを削除
  Remove-Item $csv_postal_codes

  #実行情報を保存
  @{
    exe_sqlite3      = $exe_sqlite3;
    db_postal_codes  = $db_postal_codes;
  } | ConvertTo-Json | Out-File $saved_data -Encoding default

#前回実行情報を読込
}else{
  $hash = Get-Content $saved_data -Raw | ConvertFrom-Json
  $exe_sqlite3      = $hash.exe_sqlite3
  $db_postal_codes  = $hash.db_postal_codes
}

#郵便番号から住所を検索
@"
.mode csv
SELECT * FROM mst_postal_codes WHERE postal_code like '$postal_code';
"@ | & $exe_sqlite3 $db_postal_codes `
   | ConvertFrom-Csv -Header japanese_local_goverment_code,
                             old_postal_code,
                             postal_code,
                             prefecture_name_kana,
                             city_name_kana,
                             postal_term_name_kana,
                             prefecture_name,
                             city_name,
                             postal_term_name,
                             flg_has_plural_postal_codes,
                             flg_small_admin_units_have_each_house_numbers,
                             flg_has_city_blocks,
                             flg_postal_code_has_plural_postal_terms,
                             flg_updated,
                             update_reason_code
```

```ps1:prepare_sqlite3.ps1
#SQLite3をローカルで実行可能にしてパスを返す
param([Parameter(Mandatory=$true)]$work_dir)

#zip解凍関数（引用元：http://qiita.com/kmr_hryk/items/5dfe87c0035887cbd8e4）
function psunzip {
   #エクスプローラーのオブジェクトを作成する
   $Expcom = New-Object -ComObject Shell.Application

   #第一引数のzipファイルパスを登録
   $zipFile = $Expcom.NameSpace($args[0])

   #第二引数の出力先ディレクトリパスを登録
   $tgtDir = $Expcom.NameSpace($args[1])

   #zipファイル内のオブジェクト(ファイル)一覧を取得してForEach-Objectに渡す
   $zipFile.Items() | ForEach-Object {

      #ファイルを一つずつ出力先ディレクトリにコピー
      $tgtDir.CopyHere($_.path)
   }
}

$uri      = 'http://www.sqlite.org/2015/sqlite-shell-win32-x86-3081001.zip'
$zip_path = Join-Path $work_dir 'sqlite-shell-win32-x86-3081001.zip'
$exp_path = $work_dir
$path     = Join-Path $work_dir 'sqlite3.exe'

Invoke-WebRequest -Uri $uri -OutFile $zip_path
if(![System.IO.Directory]::Exists($exp_path)){
  New-Item $exp_path -ItemType Directory | Out-Null
}
psunzip  $zip_path $exp_path

if([System.IO.File]::Exists($path)){
  #中間ファイルを削除
  Remove-Item $zip_path

  return $path
}else{
  return ""
}
```

```ps1:prepare_postal_codes.ps1
#郵便番号一覧をローカルで読込可能にしてパスを返す
param([Parameter(Mandatory=$true)]$work_dir)

#zip解凍関数（引用元：http://qiita.com/kmr_hryk/items/5dfe87c0035887cbd8e4）
function psunzip {
   #エクスプローラーのオブジェクトを作成する
   $Expcom = New-Object -ComObject Shell.Application

   #第一引数のzipファイルパスを登録
   $zipFile = $Expcom.NameSpace($args[0])

   #第二引数の出力先ディレクトリパスを登録
   $tgtDir = $Expcom.NameSpace($args[1])

   #zipファイル内のオブジェクト(ファイル)一覧を取得してForEach-Objectに渡す
   $zipFile.Items() | ForEach-Object {

      #ファイルを一つずつ出力先ディレクトリにコピー
      $tgtDir.CopyHere($_.path)
   }
}

$uri      = 'http://www.post.japanpost.jp/zipcode/dl/kogaki/zip/ken_all.zip'
$zip_path = Join-Path $work_dir 'ken_all.zip'
$exp_path = $work_dir
$path     = Join-Path $work_dir 'ken_all.csv'

Invoke-WebRequest -Uri $uri -OutFile $zip_path
if(![System.IO.Directory]::Exists($exp_path)){
  New-Item $exp_path -ItemType Directory | Out-Null
}
psunzip  $zip_path $exp_path

if([System.IO.File]::Exists($path)){
  #中間ファイルを削除
  Remove-Item $zip_path

  return $path
}else{
  return ""
}
```

#解説
処理の概要としては、
（１）住所検索用データベースの構築
（２）郵便番号から住所検索
というようなことをやっています。

##１．引数定義
（１）postal_code：検索対象の郵便番号を指定。曖昧検索が可能（※）
（２）app_dir：住所検索用データベース構築先の指定で必要（省略可）
（３）renew：住所検索用データベースを再構築する場合に指定

※SQLite3のlike句の使い方に従います。
　以下の記事が参考になると思います。
　http://www.dbonline.jp/sqlite/select/index6.html

```ps1:find_address.ps1
param(
  [Parameter(Mandatory=$true)]$postal_code,
  $app_dir = "C:\nancus_invoker",
  [switch]$renew
)
```

##２．変数・関数定義
work_dir：住所検索用データベース構築先
saved_data：実行情報の保存先（解説は後ほど）
Invoke-RemoteScript：
　詳しくは以下を参照
　http://qiita.com/minr/items/b4f71dad4438707d84d3

```ps1:find_address.ps1
$work_dir   = Join-Path $app_dir  "find_address"
$saved_data = Join-Path $work_dir "saved_data.json"

function Invoke-RemoteScript($url){
  $script = (New-Object Net.WebClient).DownloadString($url)
  Invoke-Expression("&{$script} $args")
}
```

##３．住所検索用データベースの構築
＜renewスイッチ指定時＞
・住所検索用データベースを再構築します。

＜renewスイッチ未指定時＞
・初回実行時は住所検索用データベースを構築します。
・2回目以降実行時は、新たに住所検索用データベースを構築せず、既に構築されている住所検索用データベースを使います。

※住所検索用データベース構築は少し時間がかかります。
（ちなみに、筆者のPCでは30秒ほどかかりました）

```ps1:find_address.ps1
#作業フォルダをクリア
if([System.IO.Directory]::Exists($work_dir) -and $renew.IsPresent){
  Remove-Item $work_dir -Recurse | Out-Null
}

#作業フォルダを作成
if(![System.IO.Directory]::Exists($work_dir)){
  New-Item $work_dir -ItemType Directory | Out-Null

  #SQLite3（データベースエンジン）をローカルで実行可能にする
  $exe_sqlite3 =
    Invoke-RemoteScript 'http://nancus.html.xdomain.jp/scripts/prepare_sqlite3.ps1' $work_dir

  #郵便番号一覧をローカルで読込可能にする
  $csv_postal_codes =
    Invoke-RemoteScript 'http://nancus.html.xdomain.jp/scripts/prepare_postal_codes.ps1' $work_dir

  #郵便番号一覧をSQLite3データベースに変換
  $db_postal_codes  = ($csv_postal_codes + ".sqlite3")

@"
CREATE TABLE mst_postal_codes(
  japanese_local_goverment_code,
  old_postal_code,
  postal_code,
  prefecture_name_kana,
  city_name_kana,
  postal_term_name_kana,
  prefecture_name,
  city_name,
  postal_term_name,
  flg_has_plural_postal_codes,
  flg_small_admin_units_have_each_house_numbers,
  flg_has_city_blocks,
  flg_postal_code_has_plural_postal_terms,
  flg_updated,
  update_reason_code
);

.mode csv
.import $($csv_postal_codes -replace "\\","\\" <# \から\\へ #>) mst_postal_codes
"@ | & $exe_sqlite3 $db_postal_codes

  #中間ファイルを削除
  Remove-Item $csv_postal_codes

  #実行情報を保存
  @{
    exe_sqlite3      = $exe_sqlite3;
    db_postal_codes  = $db_postal_codes;
  } | ConvertTo-Json | Out-File $saved_data -Encoding default

#前回実行情報を読込
}else{
  $hash = Get-Content $saved_data -Raw | ConvertFrom-Json
  $exe_sqlite3      = $hash.exe_sqlite3
  $db_postal_codes  = $hash.db_postal_codes
}
```

###３－１．SQLite3（データベースエンジン）をローカルで実行可能にする
ポイントだけ抜粋すると、以下の処理を行っています。

・WEB上からZIPファイルをダウンロード

```ps1:prepare_sqlite3.ps1
Invoke-WebRequest -Uri $uri -OutFile $zip_path
```

・ZIPファイルを解凍

```ps1:prepare_sqlite3.ps1
if(![System.IO.Directory]::Exists($exp_path)){
  New-Item $exp_path -ItemType Directory | Out-Null
}
psunzip  $zip_path $exp_path
```

・解凍したファイルのパスを戻り値として返す

```ps1:prepare_sqlite3.ps1
  return $path
```

###３－２．郵便番号一覧をローカルで読込可能にする
やっていることは、基本的に「３－１．SQLite3（データベースエンジン）をローカルで実行可能にする」と同じです。
なので、３－１と３－２は、そのうち共通化するかもしれません。

###３－３．郵便番号一覧をSQLite3データベースに変換

```ps1:find_address.ps1
  $db_postal_codes  = ($csv_postal_codes + ".sqlite3")

@"
CREATE TABLE mst_postal_codes(
  japanese_local_goverment_code,
  old_postal_code,
  postal_code,
  prefecture_name_kana,
  city_name_kana,
  postal_term_name_kana,
  prefecture_name,
  city_name,
  postal_term_name,
  flg_has_plural_postal_codes,
  flg_small_admin_units_have_each_house_numbers,
  flg_has_city_blocks,
  flg_postal_code_has_plural_postal_terms,
  flg_updated,
  update_reason_code
);

.mode csv
.import $($csv_postal_codes -replace "\\","\\" <# \から\\へ #>) mst_postal_codes
"@ | & $exe_sqlite3 $db_postal_codes
```

SQLite3コマンドを実行し、
・mst_postal_codesテーブルを作成（CREATE TABLE）
・CSVモードに変更（.mode csv）
・郵便番号一覧CSVをインポート（.import）
という処理をしています。
処理後、db_postal_codesのパスにデータベースファイルが作成されます。


SQLite3のコマンドについては、以下ページを参考にしました。
http://www.dbonline.jp/sqlite/sqlite_command/

ちなみに、PowerShellで「@"」から「"@」で囲まれた文字列のことを「ヒアドキュメント」と呼びます。ヒアドキュメントに関しては、以下のページが参考になると思います。
http://win.just4fun.biz/PowerShell/PowerShell%E3%81%A7%E3%83%92%E3%82%A2%E3%83%89%E3%82%AD%E3%83%A5%E3%83%A1%E3%83%B3%E3%83%88%E3%81%AE%E8%A8%98%E8%BF%B0%E6%96%B9%E6%B3%95.html

###３－４．実行情報の保存／読込

・実行情報を保存
SQLite3の実行ファイルと、住所検索用データベースのパス情報を、JSON形式でファイル出力します。

```ps1:find_address.ps1
  @{
    exe_sqlite3      = $exe_sqlite3;
    db_postal_codes  = $db_postal_codes;
  } | ConvertTo-Json | Out-File $saved_data -Encoding default
```

保存されたファイルの中身は、以下のようになります。

```json:saved_data.json
{
    "exe_sqlite3":  "C:\\nancus_invoker\\find_address\\sqlite3.exe",
    "db_postal_codes":  "C:\\nancus_invoker\\find_address\\ken_all.csv.sqlite3"
}
```


・前回実行情報の読込
2回目以降の実行時は、実行情報ファイル（saved_data.json）を読み込み、変数に展開しています。

```ps1:find_address.ps1
  $hash = Get-Content $saved_data -Raw | ConvertFrom-Json
  $exe_sqlite3      = $hash.exe_sqlite3
  $db_postal_codes  = $hash.db_postal_codes
```


##４．郵便番号から住所検索

```ps1:find_address.ps1
@"
.mode csv
SELECT * FROM mst_postal_codes WHERE postal_code like '$postal_code';
"@ | & $exe_sqlite3 $db_postal_codes `
   | ConvertFrom-Csv -Header japanese_local_goverment_code,
                             old_postal_code,
                             postal_code,
                             prefecture_name_kana,
                             city_name_kana,
                             postal_term_name_kana,
                             prefecture_name,
                             city_name,
                             postal_term_name,
                             flg_has_plural_postal_codes,
                             flg_small_admin_units_have_each_house_numbers,
                             flg_has_city_blocks,
                             flg_postal_code_has_plural_postal_terms,
                             flg_updated,
                             update_reason_code
```

SQLite3コマンドを実行し、住所検索用データベースの郵便番号一覧から、指定した郵便番号(postal_code)の条件に一致するデータを取得（SELECT）しています。

取得データはCSV形式で出力した後、ConvertFrom-Csvコマンドにて、オブジェクトデータに変換し、戻り値として返しています。


#おわりに（経緯など）

今回、住所検索をテーマにしたのは、
・実用で役に立ちそう
・仕様が簡単
という理由からでした。はじめは郵便番号CSVを読み込んで、サクッと検索！みたいなイメージを持っていましたが・・・

```ps1:住所検索の処理イメージ
param($postal_code,$csv_postal_codes)

Get-Content $csv_postal_codes `
| ConvertFrom-Csv -Header postal_code,address_name `
| Where{$_.postal_code -eq $postal_code} `
| Select address_name
#本当は郵便番号一覧の項目は他にもいっぱいあるのですが、説明のため単純化しています。

```

いざ作ってみると、早速、性能の問題に直面しました。

まあ、データが12万件もあるので、ある程度時間はかかるような気がしていましたが、5分くらい待たされたので、これは使えないな、と。

ファイルを分割する等して処理を工夫したら、性能は上げられるのでしょうが、あまり複雑なことはしたくなかったので、データベースエンジンを使うことにしました。

データベースエンジンの中でも、SQLite3を選択した理由は、
・インストールが簡単（ZIPファイルを解凍するだけで実行可能）
・ファイルサイズが小さい（562KB）
・無料で使える（パブリックドメイン）
ということから、PowerShellでSQLite3のインストール処理を行えば、利用者が手動でSQLite3をインストールすることなく実行できるためです。
（あとは、今まで筆者がSQLiteを使ったことがなかったので、面白そうだったから、というのも理由の１つです）

果たして性能の方は大丈夫だろうか・・・と思い、
早速、SQLite3のデータベースを構築しました。
その後、住所検索（SELECT）を行ったところ、1秒もかからない間に結果が返ってきました。

PowerShellで検索した時と比べて、約300倍以上の速さ。
（もしかしたら、筆者のPowerShellの組み方が悪いだけなのかもしれませんが）

改めて、データベースエンジンってすごい。