【PowerShell】Word に埋め込まれたリンク先不明のグラフからデータを抽出する
Microsoft Word 七不思議の1つ、それはリンク先がなくなってもグラフを表示し続けるところ……。
データを再利用しようとしてこんなメッセージが出てガックリしたことは数知れず。
仕方なく手入力しようと データラベルの追加
をクリックして、こんな桁数の数字が表示された日には絶望すら覚えました。
しかしこうして数値を表示できているわけですから、何かしらの構造的なデータは埋め込まれているのは間違いありません。
そこで今回は PowerShell を使って Word データをほじくり返し、グラフに表示されている値を抽出してみようと思います。
成果
Word を開き、調べたいグラフをクリックして選択した状態でコマンドレットを実行すると、コンソール上にグラフ内のデータ系列の値をオブジェクトとして取得できるようになります。
環境:
> $PSVersiontable
Name Value
---- -----
PSVersion 7.2.4
PSEdition Core
GitCommitId 7.2.4
OS Microsoft Windows 10.0.19044
Platform Win32NT
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
WSManStackVersion 3.0
事前準備:開かれている Word を操作できるようにする
バージョン6以降で Core の名を冠した PowerShell はクロスプラットフォーム化の代償として、 [System.Runtime.InteropServices.Marshal]::GetActiveObject()
を使った「現在開かれている Office ソフトの操作」ができなくなっています。
バージョン5までの Windows PowerShell であればこのステップは不要です。
PowerShell 6.0 以降でこの機能を有効化するには、Add-Type
で C# のコードを埋め込んでやればOKです。
if (-not ('Pwsh.Marshal' -as [type]))
{Add-Type -Namespace "Pwsh" -Name "Marshal" -MemberDefinition @'
internal const String OLEAUT32 = "oleaut32.dll";
internal const String OLE32 = "ole32.dll";
public static Object GetActiveObject(String progID)
{
Object obj = null;
Guid clsid;
try
{
CLSIDFromProgIDEx(progID, out clsid);
}
catch (Exception)
{
CLSIDFromProgID(progID, out clsid);
}
GetActiveObject(ref clsid, IntPtr.Zero, out obj);
return obj;
}
[DllImport(OLE32, PreserveSig = false)]
private static extern void CLSIDFromProgIDEx([MarshalAs(UnmanagedType.LPWStr)] String progId, out Guid clsid);
[DllImport(OLE32, PreserveSig = false)]
private static extern void CLSIDFromProgID([MarshalAs(UnmanagedType.LPWStr)] String progId, out Guid clsid);
[DllImport(OLEAUT32, PreserveSig = false)]
private static extern void GetActiveObject(ref Guid rclsid, IntPtr reserved, [MarshalAs(UnmanagedType.Interface)] out Object ppunk);
'@ }
SilkyFowl 氏に深謝! 😭
その上で下記のようなコマンドレットを作ってやれば、あとは VBA の文法で現在開いている文書を操作できるようになります。
function Get-ActiveWordApp {
$office = [Pwsh.Marshal]::GetActiveObject("Word.Application")
try {
if ($office.Documents.Count -lt 1) {
return $null
}
return $office
}
catch {
return $null
}
}
コンソールから ActiveDocument
で文書の情報を取得したり、
> Get-ActiveWordApp | sv w;
$w.ActiveDocument.name # 開いている文書の名前が出力される
Selection
で選択範囲を扱えます。
> Get-ActiveWordApp | sv w;
$w.Selection.font.bold=$true # 選択している文字が太字になる
VBA のイミディエイトウインドウと違って debug.print()
と都度入力しなくていいのが最高ですね。
メイン処理
# VBA と同じく配列のインデックスは1から始まるので注意!
function Get-EmbeddedDataOnActiveWordDocument {
$wd = Get-ActiveWordApp
if (-not $wd) { return }
$chart = $null
if ($wd.Selection.Range.InlineShapes.Count -eq 1 -and $wd.Selection.Range.InlineShapes(1).HasChart) {
# グラフが「行内」のとき
$chart = $wd.Selection.Range.InlineShapes(1).Chart
}
if ($wd.Selection.ShapeRange.Count -eq 1 -and $wd.Selection.ShapeRange(1).HasChart) {
# グラフが「行内」ではないとき
$chart = $wd.Selection.ShapeRange(1).Chart
}
if (-not $chart) {
"グラフを1つ選択した状態で実行してください!" | Write-Host -ForegroundColor Red
return
}
$nSeries = $chart.SeriesCollection().Count
1..$nSeries | ForEach-Object {
$label = $chart.SeriesCollection($_).Name
$xVals = $chart.SeriesCollection($_).XValues
$vals = $chart.SeriesCollection($_).Values
$data = 1..$xVals.Count | ForEach-Object {
return [PSCustomObject]@{
"X" = $xVals.get($_);
"Value" = $vals.get($_);
}
}
return [PSCustomObject]@{
"Label" = $label;
"Data" = $data;
}
}
}
グラフ内のデータ系列(折れ線グラフなら折れ線の1本1本)は SeriesCollection オブジェクトです。
Chart.SeriesCollection(1)
で1番目の系列を取得できるようなメソッド形式になっていて、Chart.SeriesCollection.Count
のようにプロパティから系列数を取得することができず苦労しました( Chart.SeriesCollection().Count
と引数なしで呼び出せばいいなんて所見殺しですよね…?)。
Discussion