前回までで、棒グラフと円グラフを1つのPowerShellスクリプトから生成できるようになった。今回は、元データとしてCSVに加えTSVにも対応する方法を考える。これもリファインのサンプルとして面白い題材なので、参考にしていただきたい。
→連載「PowerShell Core入門 - 基本コマンドの使い方」の過去回はこちらを参照。
TSVファイルからcsv2chart.ps1を使ってグラフを生成する
まず、前回までの成果物「csv2barchart.ps1」を確認しよう。
#!/usr/bin/env pwsh
#========================================================================
# 引数を処理
# -CSVFile パス グラフのCSVデータファイルパス
# -PNGFile パス 生成するPNG画像のファイルパス
# -OutFile パス 中間生成されるHTMLファイルパス
# -GraphTitle タイトル グラフのタイトル
# -Width 幅 生成するPNG画像の幅
# -Height 高さ 生成するPNG画像の高さ
#========================================================================
Param(
[Parameter(Mandatory=$true)][String]$CSVFile,
[String]$PNGFile = (Get-Location).ToString() + '\out.png',
[String]$OutFile = (Get-Location).ToString() + '\out.html',
[String]$GraphTitle = 'グラフ',
[Int]$Width = 2000,
[Int]$Height = 1200
)
#========================================================================
# csv2chart.ps1を使ってグラフを生成
#========================================================================
csv2chart.ps1 `
-CSVFile $CSVFile `
-PNGFile $PNGFile `
-GraphTitle $GraphTitle `
-Width $Width `
-Height $Height `
-GraphType 'Bar'
csv2barchart.ps1
#!/usr/bin/env pwsh
#========================================================================
# 引数を処理
# -CSVFile パス グラフのCSVデータファイルパス
# -PNGFile パス 生成するPNG画像のファイルパス
# -OutFile パス 中間生成されるHTMLファイルパス
# -GraphTitle タイトル グラフのタイトル
# -Width 幅 生成するPNG画像の幅
# -Height 高さ 生成するPNG画像の高さ
#========================================================================
Param(
[Parameter(Mandatory=$true)][String]$CSVFile,
[String]$PNGFile = (Get-Location).ToString() + '\out.png',
[String]$OutFile = (Get-Location).ToString() + '\out.html',
[String]$GraphTitle = 'グラフ',
[Int]$Width = 2000,
[Int]$Height = 1200
)
#========================================================================
# csv2chart.ps1を使ってグラフを生成
#========================================================================
csv2chart.ps1 `
-CSVFile $CSVFile `
-PNGFile $PNGFile `
-GraphTitle $GraphTitle `
-Width $Width `
-Height $Height `
-GraphType 'Pie'
csv2chart.ps1が本体であり、上記の「csv2barchart.ps1」と「csv2barchart.ps1」がラッパースクリプトになっている。これに対し、今回からはcsv2chart.ps1を使って「tsv2barchart.ps1」と「tsv2piechart.ps1」を作ろうというわけだ。
これまでと同じアプローチを取るのであれば、「tsv2barchart.ps1」と「tsv2piechart.ps1」にそれぞれ、TSVファイルをCSVファイルに変換する処理を加え、そこからcsv2chart.ps1を呼び出せばよいということになる。
より汎用化させた「data2chart.ps1」を作る
しかし、ここで前回や前々回で取り組んできた内容を考える必要がある。先ほどのアプローチを取った場合、「tsv2barchart.ps1」と「tsv2piechart.ps1」に「TSVファイルをCSVファイルに変換する処理」というコードが追加されることになる。つまり、2つのファイルにほぼ同じコードが存在するようになるわけだ。生成するグラフの種類を増やすために、さらにラッパースクリプトを作成することになれば、「TSVファイルをCSVファイルに変換する処理」はさらに多くのファイルに存在することになるだろう。
これは、メンテナンスをする上でとても面倒な状況を生むことになる。だとすれば、この処理は1カ所にまとめておいた方がよい。
現状を考えると、csv2chart.ps1に「TSVファイルをCSVファイルに変換する処理」を取り込めば、ラッパースクリプト側に似たようなコードが複数存在する状況を避けることはできる。そうなると「csv2chart.ps1」というスクリプト名が不適切になってくる。
これは、csv2chart.ps1というスクリプトをさらに汎用化させて「data2chart.ps1」というPowerShellスクリプトにすればスッキリする。
data2chart.ps1を作ってみる
早速書き換えた成果物「data2chart.ps1」を次に示す。
#!/usr/bin/env pwsh
#========================================================================
# 引数を処理
# -File パス グラフのデータファイルパス
# -PNGFile パス 生成するPNG画像のファイルパス
# -OutFile パス 中間生成されるHTMLファイルパス
# -GraphTitle タイトル グラフのタイトル
# -Width 幅 生成するPNG画像の幅
# -Height 高さ 生成するPNG画像の高さ
# -GraphType グラフ種類 生成するグラフの種類
#========================================================================
Param(
[Parameter(Mandatory=$true)][String]$DataFile,
[String]$PNGFile = (Get-Location).ToString() + '\out.png',
[String]$OutFile = (Get-Location).ToString() + '\out.html',
[String]$GraphTitle = 'グラフ',
[Int]$Width = 2000,
[Int]$Height = 1200,
[ValidateSet('Bar','Pie')]$GraphType = 'Bar'
)
#========================================================================
# データを種類に応じて読み込み
#========================================================================
$DataFileExtension = [IO.Path]::GetExtension($DataFile)
switch ($DataFileExtension) {
'.csv' {
$CsvData = Import-Csv $DataFile -Header 1,2
}
'.tsv' {
$CsvData = Import-Csv $DataFile -Header 1,2 -Delimiter `t
}
default {
$CsvData = Import-Csv $DataFile -Header 1,2
}
}
#========================================================================
# 気温データを取得
#========================================================================
# ヘッダを変数に格納
foreach ($Row in $CsvData) {
$CsvHeader1 = $Row.1
$CsvHeader2 = $Row.2
break
}
#========================================================================
# Google Charts用HTMLの用意
#========================================================================
$GoogleChartsHTML1 = @"
<html>
<head>
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<script type="text/javascript">
// Google Visualization APIおよびコアチャートパッケージを読み込み
google.charts.load('current', {'packages':['corechart']});
// Google Visualization API読み込み完了後に実行
google.charts.setOnLoadCallback(drawChart);
function drawChart() {
// データテーブルを作成
var data = new google.visualization.DataTable();
data.addColumn('string', '');
data.addColumn('number', '$CsvHeader2');
data.addRows([
"@
$GoogleChartsHTML2 = @"
]);
// チャートオプションを設定
var options = {'title':'$GraphTitle',
'width':'$Width',
'height':'$Height'};
// 初期化およびチャートの生成
var chart = new google.visualization.${GraphType}Chart(document.getElementById('chart_div'));
// チャートイメージをbase64エンコードされたPNG画像データとして出力
google.visualization.events.addListener(chart, 'ready', function () {
document.getElementById('chart_base64').innerHTML = chart.getImageURI();
});
// チャートを描画
chart.draw(data, options);
}
</script>
</head>
<body>
<div id="chart_div"></div>
<div id="chart_base64"></div>
</body>
</html>
"@
#========================================================================
# CSVデータをGoogle Chartsで使える形式へ加工
#========================================================================
$GraphData = ""
$FirstLineIs = $true
foreach ($Row in $CsvData) {
if ($FirstLineIs) {
$FirstLineIs = $false
continue
}
$GraphData = $GraphData + "['" + $Row.1 + "'," + $Row.2 + "],"
}
$GraphData = $GraphData.trim(",")
#========================================================================
# Google Charts用のHTMLを出力
#========================================================================
$GoogleChartsHTML1 > $OutFile
$GraphData >>$OutFile
$GoogleChartsHTML2 >>$OutFile
#========================================================================
# WebDriver起動
#========================================================================
webdriver_edge_start.ps1
#========================================================================
# Microsoft EdgeでGoogle Charts用のHTMLをオープン
#========================================================================
$FileURL = "file:///" + $OutFile.Replace('\','/')
'Microsoft Edgeでグラフを描画します。'
Set-SeUrl -Url $FileURL
#========================================================================
# 描画されたグラフをBase64エンコードされたPNGデータとして取得
#========================================================================
'描画したグラフをBase64エンコードされたPNGデータとして取得します。'
$Element = Get-SeElement -By XPath -Value '//*[@id="chart_base64"]'
$Base64 = $Element.Text -replace 'data:image/png;base64,',''
#========================================================================
# Base64エンコードデータをデコードしてファイルへ保存
#========================================================================
'Base64エンコードされたPNGデータをデコードして保存します。'
$Bytes = [Convert]::FromBase64String($Base64)
[IO.File]::WriteAllBytes($PNGFile, $Bytes)
#========================================================================
# WebDriverを終了
#========================================================================
webdriver_edge_stop.ps1
#========================================================================
# 作業用の一時ファイルを削除
#========================================================================
Remove-Item $OutFile
詳しい説明は省くが、これまであった「-CSVFile」というパラメータを削除し、代わりに「-DataFile」というパラメータが必須パラメータとして追加されている。そして、指定されたファイルに対して自動的にデータをCSVデータとしてメモリへ読み込む処理を行っている。今回はCSVとTSVファイルのみの対応となっているが、このロジックに従っていけば、より多くのデータ形式に自動対応させることができる。
実行して動作を確認
では、動作を確認してみよう。まず、data2chart.ps1を使ってCSVファイルから棒グラフを生成する。
生成されたグラフデータは次のようになる。
次にdata2chart.ps1を使ってTSVファイルから円グラフを生成する。
生成されたグラフデータは次のようになる。
このようにdata2chart.ps1のみでCSVファイルからもTSVファイルからもグラフデータが生成できたことがわかる。
サクッと作り変えることがポイント
PowerShellスクリプトを活用する際の利点として、これまで何度もサクッと作り替えができる点を挙げてきた。大切なのはインタフェース(この場合はパラメータ)であり、この部分さえ永続性を保っておけば内部は好きなようにいじってもよい。
新しいPowerShellスクリプトを作成し、整理して書き換えて……を素早くできるのがPowerShellスクリプトの良いところだ。ちゃんと整理しておけば後からメンテナンスする段階でとても簡単になる。
そして最初からこの整理された状態にする必要がないところもポイントだ。似たようなスクリプトをたくさん作るわけでないのであれば、特にコードを整理して分ける必要もない。必要になったら整理していけばよい。この手軽さがスクリプト的な活用方法だ。
付録
webdriver_edge_start.ps1
#!/usr/bin/env pwsh
#========================================================================
# Microsoft Edge WebDriverを起動する
#========================================================================
#========================================================================
# 動作しているMicrosoft Edge WebDriverをすべて終了
#========================================================================
webdriver_edge_stop.ps1
#========================================================================
# Seleniumモジュールがない場合にはインストール
#========================================================================
if (-Not (Get-InstalledModule -Name Selenium 2> $Null)) {
'Seleniumモジュールをインストールします。'
Install-Module -Name Selenium -AllowPrerelease -Force
Get-InstalledModule -Name Selenium
}
#========================================================================
# Microsoft Edge WebDriverを起動
#========================================================================
'Microsoft Edge WebDriverを起動します。'
$Size = '1200,800'
if (-Not (Start-SeDriver -Browser Edge -Size $Size 2> $Null 3> $Null))
{
#================================================================
# Microsoft EdgeとMicrosoft Edge WebDriverのバージョンが一致して
# いないためにドライバが動作しなかった可能性がある。
#================================================================
#================================================================
# 不要なドライバプロセスを終了
#================================================================
webdriver_edge_stop.ps1
#================================================================
# Microsoft Edgeのバージョン番号
#================================================================
$EdgeDir='C:\Program Files (x86)\Microsoft\Edge\Application\'
$EdgeVersion=( Get-ChildItem -Name $EdgeDir |
Where-Object { $_ -NotMatch "[a-zA-Z]+" } |
Select-Object -First 1 )
# ↑ 【Select-Object -First 1の理由】
# 更新前のバージョンと更新後のバージョンが同時に
# 存在するタイミングがあるので、更新後のバージョン
# のみを取得するためにSelect-Objectを実行している。
#================================================================
# Microsoft Edge WebDriverダウンロードURLとデプロイ先パス
#================================================================
$DriverURL="https://msedgedriver.azureedge.net/$EdgeVersion/edgedriver_win64.zip"
$SeModVer=(Get-InstalledModule -Name Selenium).Version -replace "-.+$",""
$DriverDir="$env:HOME\Documents\powershell\Modules\Selenium\$SeModVer\assemblies"
$DriverDownloadDir="$DriverDir\_download"
#================================================================
# WebDriverダウンロード用の一時ディレクトリを作成
#================================================================
New-Item $DriverDownloadDir -ItemType Directory -Force
#================================================================
# Microsoft Edgeと同じバージョンのMicrosoft Edge WebDriverを
# ダウンロード
#================================================================
"Microsoft Edge WebDriver version $EdgeVersion をダウンロードします。"
curl -get `
-o $DriverDownloadDir\edgedriver_win64.zip `
$DriverURL
#================================================================
# Microsoft Edge WebDriverをデプロイ
#================================================================
"Microsoft Edge WebDriver version $EdgeVersion をインストールします。"
Expand-Archive -Path $DriverDownloadDir\edgedriver_win64.zip `
-Destination $DriverDownloadDir `
-Force
Copy-Item -Path $DriverDownloadDir\msedgedriver.exe `
-Destination $DriverDir\msedgedriver.exe `
-Force
#================================================================
# WebDriverダウンロード用の一時ディレクトリを削除
#================================================================
Remove-Item $DriverDownloadDir -Recurse -Force
#================================================================
# Microsoft Edge WebDriverを起動する
#================================================================
if (-Not (Start-SeDriver -Browser Edge -Size $Size 2> $Null 3> $Null))
{
#========================================================
# 原因不明の起動不能
#========================================================
#========================================================
# 不要なドライバプロセスを終了
#========================================================
webdriver_edge_stop.ps1
Exit
}
}
'Microsoft Edge WebDriverの起動処理完了。'
webdriver_edge_stop.ps1
#!/usr/bin/env pwsh
#========================================================================
# Microsoft Edge WebDriverを終了する
#========================================================================
#========================================================================
# WebDriverプロセスを終了
#========================================================================
if (Get-Process -Name msedgedriver 2> $Null)
{
'動作しているMicrosoft Edge WebDriverを終了します。'
Get-Process -Name msedgedriver 2> $Null
# Microsoft Edge WebDriverを終了
Stop-SeDriver 2> $Null
# まだ動作しているほかのMicrosoft Edge WebDriverを終了
if (Get-Process -Name msedgedriver 2> $Null)
{
Get-Process -Name msedgedriver 2> $Null | Stop-Process
}
'動作しているMicrosoft Edge WebDriverの終了処理完了。'
}