前回までの取り組みで、CSVデータから棒グラフのPNG画像を生成するPowerShellスクリプトを作成した。これで、汎用的なPowerShellスクリプトとして使う上で必要となる最低限の機能は実装したことになる。今回はこのスクリプトから、入力するデータをタブ区切り形式(TSV:Tab-Separated Values)に対応させる方法を取り上げる。
→連載「PowerShell Core入門 - 基本コマンドの使い方」の過去回はこちらを参照。
CSVデータからグラフを書くPowerShellスクリプト
前回の成果物「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
)
#========================================================================
# 気温データを取得
#========================================================================
$CsvData = Import-Csv $CSVFile -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.BarChart(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
次のようなCSV形式のファイル(data.csv)を指定することで、そのデータに対応したグラフ画像データを生成してくれる。
"場所","気温"
"小河内","17.6"
"青梅","20.8"
"練馬","21.4"
"八王子","20.2"
"府中","20.7"
"東京","21.3"
"江戸川臨海","19.6"
"羽田","18.7"
"大島","19.8"
"大島北ノ山","19.2"
"新島","18.8"
"神津島","17.9"
"三宅島","19.0"
"三宅坪田","20.6"
"八重見ヶ原","21.2"
"八丈島","20.5"
"父島","24.2"
今回は、指定するデータにTSVを指定できるように拡張する。
なぜTSVに対応させるのか
CSVは、データ交換用のフォーマットとして広く普及している。シンプルでわかりやすいため、PowerShellスクリプトに渡すデータ形式としてCSVを採用するのは自然な流れだ。
TSVは、CSVとよく似ているが、こちらはデータの区切りにカンマではなくタブを採用している。TSVもCSV同様、よく使われるフォーマットの一つだ。
特に、TSVはアプリケーション間のコピー&ペーストによく使われている。スプレッドシートでデータをコピーした場合や、Webブラウザでテーブルデータをコピーした場合は、TSVになっていることが多い。データをTSVで保存しておくのもよくあることなので、PowerShellスクリプトとしてもTSVに対応しておくと何かと便利だ。
TSVデータを処理するtsv2barchart.ps1
例えば、先ほどのCSVのファイルをTSVに変換すると次のようになる(data.tsv)。データがタブで区切られていることがわかる。
場所 気温
小河内 17.6
青梅 20.8
練馬 21.4
八王子 20.2
府中 20.7
東京 21.3
江戸川臨海 19.6
羽田 18.7
大島 19.8
大島北ノ山 19.2
新島 18.8
神津島 17.9
三宅島 19.0
三宅坪田 20.6
八重見ヶ原 21.2
八丈島 20.5
父島 24.2
TSVに対応させる方法はいくつかある。コードが重複して存在するとメンテナンスや書き換えが面倒になるので、csv2barchart.ps1を処理の本体とし、tsv2barchart.ps1はTSVデータをCSVデータに変換してからcsv2barchart.ps1を呼び出すスクリプトとして実装するというのが1つの方法になる。
PowerShellにはCSVのデータを読み込むコマンドレットとしてImport-Csvが、CSVとしてデータをファイルに出力するコマンドレットとしてExport-Csvが用意されている。このコマンドレットは区切り文字(デリミタ)を指定するパラメータを持っており、TSVも扱うことができるようになっている。Import-CsvとExport-Csvを使うと、CSVファイルからTSVファイルを生成することができる。
先に、書き換えたものを示しておこう。以下が今回の成果物「tsv2barchart.ps1」だ。
#!/usr/bin/env pwsh
#========================================================================
# 引数を処理
# -TSVFile パス グラフのTSVデータファイルパス
# -PNGFile パス 生成するPNG画像のファイルパス
# -OutFile パス 中間生成されるHTMLファイルパス
# -GraphTitle タイトル グラフのタイトル
# -Width 幅 生成するPNG画像の幅
# -Height 高さ 生成するPNG画像の高さ
#========================================================================
Param(
[Parameter(Mandatory=$true)][String]$TSVFile,
[String]$PNGFile = (Get-Location).ToString() + '\out.png',
[String]$OutFile = (Get-Location).ToString() + '\out.html',
[String]$GraphTitle = 'グラフ',
[Int]$Width = 2000,
[Int]$Height = 1200
)
#========================================================================
# TSVファイルからCSVファイルを生成
#========================================================================
$CSVFile = $TSVFile + ".csv"
Import-Csv -Path $TSVFile -Delimiter `t | Export-Csv -Path $CSVFile
#========================================================================
# csv2barchart.ps1を使ってグラフを生成
#========================================================================
csv2barchart.ps1 `
-CSVFile $CSVFile `
-PNGFile $PNGFile `
-GraphTitle $GraphTitle `
-Width $Width `
-Height $Height
#========================================================================
# 作業用の作成したCSVファイルを削除
#========================================================================
Remove-Item $CSVFile
グラフ生成処理の本体はcsv2barchart.ps1を使っているので、その辺りには触れていない。つまり、tsv2barchart.ps1は、csv2barchart.ps1を呼び出すためのラッパースクリプトということになる。
実行してみよう
tsv2barchart.ps1を実行すると次のようになる。
生成されたグラフ画像は次のようになる。
TSVファイルからもグラフデータが生成できるようになったことがわかる。
重複コードとコピーコード
今回のtsv2barchart.ps1は、csv2barchart.ps1をコピーして中身をちょっと書き換えるくらいでも実現することができる。書き換えるコードが少なくて済むので、そのやり方も悪くはない。ただし、留意点はある。
スクリプトをコピーして別のスクリプトを作成した場合、同じような処理を行うコードが複数のファイルに散在するという状況が生まれる。その場合、コードの一部を書き換えたときに、同じ処理を行っている別のファイルのコードも書き換えなければいけないケースがある。こうしたときでも、今回のcsv2barchart.ps1のように、1つのファイルに処理を集約してあれば、そのファイルの書き換えだけで済む。
だがそうすると今度は、csv2barchart.ps1を書き換えることでほかのファイルが余計な影響を受ける可能性が生まれる。ほかのファイルに影響を与えないようにスクリプトを書き換える作業は、結構気を使う。したがって、ファイルをコピーして編集するのか、ラッパースクリプトを作成してコピーは行わないのか、その辺りはケースバイケースで判断することが必要だ。
なお、その他の方法としてはパラメータでファイル形式を指定したり、CSV/TSVを個別に指定したりといった方法も考えられる。スクリプトファイルの数を増やしたくない場合は、そうした方法をとると良いだろう。
付録
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の終了処理完了。'
}