PBIX ダッシュボードパッチャー は何をしますか?
Power BI Desktop の .pbix ファイルをその場で編集します。ページの追加、ビジュアルの追加、レイアウトの変更、フィールドの再バインドなどを行い、Power BI Desktop で破損エラーなしで開けるように再パッケージします。
使用するタイミング
- ユーザーが保存済みの
.pbixファイルを持っており、ビジュアルの追加やダッシュボードの生成を希望する場合。 - ユーザーが Power BI レポートの編集(CI、一括テンプレート、データドリブンレイアウト)を自動化したい場合。
- 以前の編集試行で、
MashupValidationError、Invalid SecurityBindings、または「破損ファイル」のエラーが発生した場合。
核心的な問題(最初に読んでください)
最新の .pbix ファイルは PBIR Enhanced Report Format を使用しています。レポートのレイアウトは、古いモノリシックな Report/Layout ではなく、Report/definition/pages/<pageId>/visuals/<visualId>/visual.json のようなビジュアルごとの JSON ファイルとして保存されています。
PBIX は ZIP ファイルであり、以下のようなファイルを含みます。
DataModel— Analysis Services モデル(再圧縮してはいけません。そのまま保存)。Report/definition/...— PBIR のビジュアルごとの JSON ツリー。SecurityBindings— ファイルレイアウトに署名する、DPAPI で暗号化されたマニフェスト。[Content_Types].xml— ZIP 内のすべてのファイルをリストします。
ファイルを破損させる落とし穴(経験から学んでください):
- Python の
zipfileは、.NET のZipArchiveとは異なる外部ファイル属性を設定します。 Power BI はこれらを検証します。PowerShell を使用して .NET のZipArchiveを利用し、すべてのエントリでExternalAttributes = 0を設定してください。 DataModelを再圧縮すると破損します。 ソースと同じ圧縮方法(通常はDeflateですが、Storedの場合もあります)で保存する必要があります。安全なのは、書き換えた場合はNoCompressionにし、それ以外は元のバイト列をそのまま保持することです。- ファイルを追加すると
SecurityBindingsが壊れます。 これは ZIP 内の内容を署名したマニフェストです。Report/definition/pages/<new>/visuals/.../visual.jsonを追加すると、署名が一致しなくなり、PBI は開くのを拒否します。 - エントリ名の先頭に
/を付けてはいけません。 開くときにMissingBIPackagePart: /Versionと表示される場合、Versionエントリが/Version(先頭スラッシュ)として ZIP 圧縮されています。PowerShell の単純なSubstring($src.Length + 1)は先頭にバックスラッシュを残すことがあり、ZIP 内で/になります。PBI はこれを拒否します。解決済みの長いパスからプレフィックスを計算し、すべてのエントリ名をTrimStart('/')で処理してください。 Report/definition/**/*.jsonの UTF-8 BOM は致命的です。 PBI エラー:Only text with UTF8 encoding without BOM (byte order marks) is supported. Detected BOM: 'UTF-8'。Set-Content -Encoding UTF8やFile.WriteAllText(明示的なエンコーディング指定なし)は、Windows PowerShell 5.1 / .NET Framework では BOM を書き込みます。常にNew-Object System.Text.UTF8Encoding($false)をWriteAllTextまたはStreamWriterに渡して JSON を書き込んでください。
SecurityBindings の修正方法: ZIP から削除し、[Content_Types].xml からそのエントリを削除します。Power BI Desktop は次回保存時に SecurityBindings を再生成します。ファイルは正常に開きます。
手順
1. ファイルを検査する
# .NET を使用して作業ディレクトリに展開し、属性をクリーンに保つ
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::ExtractToDirectory("Test.pbix", "pbix_src")
PBIR(新しい形式)かレガシーかを確認する:
- PBIR:
Report\definition\pages\<pageId>\visuals\<visualId>\visual.jsonが存在する。 - レガシー:
Report\Layout(バイナリ JSON 文字列)のみ存在する。このスキルは PBIR を対象としています。
2. visual.json スキーマ(バージョン 2.12.0)を理解する
最小限の cardVisual(KPI タイル):
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.12.0/schema.json",
"name": "<20-hex-char id>",
"position": {"x": 20, "y": 20, "z": 0, "height": 160, "width": 440, "tabOrder": 0},
"visual": {
"visualType": "cardVisual",
"query": {
"queryState": {
"Data": {
"projections": [{
"field": {
"Aggregation": {
"Expression": {"Column": {"Expression": {"SourceRef": {"Entity": "financials"}}, "Property": "Sales"}},
"Function": 0
}
},
"queryRef": "Sum(financials.Sales)",
"nativeQueryRef": "Sum of Sales",
"active": true
}]
}
},
"sortDefinition": {
"sort": [{"field": {"Aggregation": {"Expression": {"Column": {"Expression": {"SourceRef": {"Entity": "financials"}}, "Property": "Sales"}}, "Function": 0}}, "direction": "Descending"}],
"isDefaultSort": true
}
},
"drillFilterOtherVisuals": true
}
}
重要なポイント:
nameは 20 文字の 16 進数 ID。ページ内で一意である必要があります。visualTypeの例:cardVisual、clusteredBarChart、clusteredColumnChart、lineChart、donutChart、pieChart、tableEx、pivotTable。cardVisualはロールDataを使用します。棒/縦棒/折れ線はCategory+Y(オプションでSeries)。ドーナツ/円はCategory+Y。Aggregation.Function:0=Sum、1=Avg、2=Count、3=Min、4=Max、5=CountNonNull、6=Median、7=StDev、8=Variance。drillFilterOtherVisuals: trueは必須。省略するとファイルが破損します。
3. ページを登録する
Report/definition/pages/pages.json:
{"$schema":"...pagesMetadata/1.1.0/schema.json","pageOrder":["<pageId1>","<pageId2>"],"activePageName":"<pageIdX>"}
各ページフォルダには page.json があります:
{"$schema":"...page/2.1.0/schema.json","name":"<pageId>","displayName":"Dashboard","displayOption":"FitToPage","height":1080,"width":1920}
4. JSON ファイルの書き込み — BOM なしの UTF-8
visual.json、page.json、report.json、または Report/definition/ 配下のファイルを編集するたびに、BOM なしで書き込んでください。Windows PowerShell 5.1 はデフォルトで BOM を追加します。
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($path, $json, $utf8NoBom)
以前のミスで書き込まれた BOM を削除するには:
$b = [System.IO.File]::ReadAllBytes($path)
if ($b.Length -ge 3 -and $b[0] -eq 0xEF -and $b[1] -eq 0xBB -and $b[2] -eq 0xBF) {
$text = [System.Text.Encoding]::UTF8.GetString($b, 3, $b.Length - 3)
[System.IO.File]::WriteAllText($path, $text, (New-Object System.Text.UTF8Encoding($false)))
}
5. 再パッケージ — 唯一信頼性の高い方法
Python の zipfile を使用しないでください。PowerShell と .NET の ZipArchive を使用してください。重要な詳細:
- エントリ名はスラッシュを使用し、先頭に
/を付けない。 - ソースのプレフィックス長を
(Get-Item $src).FullName.TrimEnd('\').Length + 1から計算します(Get-ChildItem アイテムのFullNameと同じケース/長いパス形式)。保険としてすべてのエントリ名をTrimStart('/')します。 - すべてのテキストエントリ(
[Content_Types].xmlを含む)は、BOM なしの UTF-8StreamWriterで書き込まれます。
Add-Type -AssemblyName System.IO.Compression
$src = "pbix_src"
$dst = "Test_patched.pbix"
if (Test-Path $dst) { Remove-Item $dst }
$srcFull = (Get-Item $src).FullName.TrimEnd('\')
$prefixLen = $srcFull.Length + 1
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
$fs = [System.IO.File]::Open($dst, 'Create')
$zip = New-Object System.IO.Compression.ZipArchive($fs, [System.IO.Compression.ZipArchiveMode]::Create)
Get-ChildItem $src -Recurse -File | ForEach-Object {
$full = $_.FullName
$rel = $full.Substring($prefixLen).Replace('\','/').TrimStart('/')
# SecurityBindings を削除 — PBI は保存時に再生成する
if ($rel -eq 'SecurityBindings') { return }
# Content_Types.xml を編集して SecurityBindings 参照を削除
if ($rel -eq '[Content_Types].xml') {
$xml = [System.IO.File]::ReadAllText($full) -replace '<Override[^/]*PartName="/SecurityBindings"[^/]*/>',''
$entry = $zip.CreateEntry($rel, [System.IO.Compression.CompressionLevel]::Optimal)
$entry.ExternalAttributes = 0
$sw = New-Object System.IO.StreamWriter($entry.Open(), $utf8NoBom)
$sw.Write($xml); $sw.Close()
return
}
# DataModel は再圧縮してはいけない
$level = if ($rel -eq 'DataModel') {
[System.IO.Compression.CompressionLevel]::NoCompression
} else {
[System.IO.Compression.CompressionLevel]::Optimal
}
$entry = $zip.CreateEntry($rel, $level)
$entry.ExternalAttributes = 0 # 重要 — Python はこれを PBI が拒否する非ゼロ値に設定する
$es = $entry.Open()
$sfs = [System.IO.File]::OpenRead($full)
$sfs.CopyTo($es)
$sfs.Close(); $es.Close()
}
$zip.Dispose(); $fs.Close()
6. 開く前に検証する
4 つのチェックを順番に実行:
Add-Type -AssemblyName System.IO.Compression.FileSystem
$z = [System.IO.Compression.ZipFile]::OpenRead("Test_patched.pbix")
# a) 必要なパーツが存在し、SecurityBindings が存在しないこと
$z.Entries | Where-Object { $_.FullName -match '^(Version|DataModel|Metadata|Settings|SecurityBindings|\[Content_Types\]\.xml)$' } |
Select-Object FullName, Length
# b) 先頭に '/' が付いているエントリがないこと
$z.Entries | Where-Object { $_.FullName.StartsWith('/') } | ForEach-Object { "BAD: $($_.FullName)" }
# c) テキストメタデータまたは Report/definition JSON に BOM がないこと
foreach ($e in $z.Entries) {
if ($e.FullName -match '\.(json|xml)$|^Version$|^Metadata$|^Settings$') {
$s = $e.Open(); $ms = New-Object System.IO.MemoryStream; $s.CopyTo($ms); $s.Close()
$b = $ms.ToArray()
if ($b.Length -ge 3 -and $b[0] -eq 0xEF -and $b[1] -eq 0xBB -and $b[2] -eq 0xBF) {
Write-Host "BOM found: $($e.FullName)"
}
}
}
# d) すべての Report/definition JSON が解析可能であること
foreach ($e in $z.Entries) {
if ($e.FullName -like 'Report/definition/*.json' -or $e.FullName -like 'Report/definition/**/*.json') {
$s = $e.Open(); $sr = New-Object System.IO.StreamReader($s)
$txt = $sr.ReadToEnd(); $sr.Close()
try { $null = $txt | ConvertFrom-Json } catch { "INVALID JSON: $($e.FullName) — $($_.Exception.Message)" }
}
}
$z.Dispose()
期待される結果:DataModel、Version、Metadata、Settings、[Content_Types].xml が存在し、SecurityBindings が存在しないこと。先頭スラッシュなし。BOM なし。すべての JSON が解析可能であること。
実行中の Power BI Desktop セッションに接続する(オプション — データ探索用)
PBI Desktop が既に実行中の場合、localhost 上に Analysis Services エンドポイントを公開します:
# ポートを検出
Get-Process msmdsrv | ForEach-Object {
$port = Get-NetTCPConnection -OwningProcess $_.Id -State Listen |
Where-Object { $_.LocalAddress -eq '127.0.0.1' } |
Select-Object -First 1 -ExpandProperty LocalPort
Write-Host "Analysis Services port: $port"
}
次に、ADOMD 経由で接続します(Power BI Desktop をインストールしてください。DLL は同梱されています):
# pip install pyadomd pythonnet
import clr
clr.AddReference(r"C:\Program Files\Microsoft Power BI Desktop\bin\Microsoft.PowerBI.AdomdClient.dll")
from Microsoft.AnalysisServices.AdomdClient import AdomdConnection # 実際の名前空間(ファイル名ではありません)
conn = AdomdConnection(f"Data Source=localhost:{port}")
conn.Open()
# DAX の実行: EVALUATE SUMMARIZECOLUMNS(financials[Segment], "Sales", SUM(financials[Sales]))
これはスキーマの読み取りと数値のプレビューにのみ使用し、最終的なビジュアルは JSON ファイルとして書き込まれ、上記の方法で再パッケージします。
ビジュアルのスタイル設定 — 実際に機能するプロパティ名
Power BI Desktop の書式設定ペインのラベルは、JSON のプロパティ名と一致しません。推測すると、PBI は次回保存時に誤ったプロパティを黙って削除します。ファイルは正常に開きますが、スタイルは失われます。常に PBI で実際の変更を 1 回行い、保存、展開し、保存された JSON からプロパティ名をコピーしてください。
visual.json でのスタイル設定の場所
visual.visualContainerObjects— ビジュアルタイルの外側のフレーム(背景、枠線、ドロップシャドウ、タイトルバー)。すべてのビジュアルタイプに適用されます。visual.objects— ビジュアルの内部(プロットエリア、軸、凡例、データポイント、カード値、カード内部の塗りつぶし)。ここでのプロパティ名はビジュアルタイプごとに異なり、テーマのvisualStyles[visualType]スキーマと一致する必要があります。
コンテナフレーム(すべてのビジュアルで機能)
"visualContainerObjects": {
"background": [{"properties":{
"color":{"solid":{"color":{"expr":{"Literal":{"Value":"'#EEF2FF'"}}}}}},
"transparency":{"expr":{"Literal":{"Value":"0D"}}}
}}],
"border": [{"properties":{
"show":{"expr":{"Literal":{"Value":"true"}}},
"color":{"solid":{"color":{"expr":{"Literal":{"Value":"'#E2E8F0'"}}}}}},
"radius":{"expr":{"Literal":{"Value":"12D"}}}
}}],
"dropShadow": [{"properties":{
"show":{"expr":{"Literal":{"Value":"true"}}},
"color":{"solid":{"color":{"expr":{"Literal":{"Value":"'#0F172A'"}}}}}},
"transparency":{"expr":{"Literal":{"Value":"85D"}}},
"position":{"expr":{"Literal":{"Value":"'Outer'"}}},
"preset":{"expr":{"Literal":{"Value":"'BottomRight'"}}}
}}]
}
cardVisual(KPI タイル)— 注意点
大きな数字は、callout.color でも value.color でもありません。PBI は両方を黙って削除します。
- 大きな数字のフォントカラー:
value.fontColor(colorではありません)。 - 大きな数字のフォント/サイズ/太さ:
value.fontFamily、value.fontSize、value.bold。 - 数字の下のカテゴリラベル:
label.fontColor、label.fontFamily、label.fontSize。 - 数字の背後にある薄い塗りつぶし:
cardCalloutArea.backgroundFillColor。これには、メジャープロジェクションのメタデータを指定するselectorが必要です。例:"selector":{"metadata":"Sum(financials.Profit)"}。その文字列はvisual.query.queryState.Data.projections[0].queryRefから読み取ってください。
"objects": {
"value": [{"properties":{
"fontColor":{"solid":{"color":{"expr":{"Literal":{"Value":"'#4F46E5'"}}}}}},
"fontFamily":{"expr":{"Literal":{"Value":"'Segoe UI Semibold'"}}},
"fontSize":{"expr":{"Literal":{"Value":"40D"}}},
"bold":{"expr":{"Literal":{"Value":"true"}}}
}}],
"label": [{"properties":{
"fontColor":{"solid":{"color":{"expr":{"Literal":{"Value":"'#64748B'"}}}}}}
}}],
"cardCalloutArea": [{
"properties": {
"show":{"expr":{"Literal":{"Value":"true"}}},
"backgroundFillColor":{"solid":{"color":{"expr":{"Literal":{"Value":"'#E0E7FF'"}}}}}}
},
"selector":{"metadata":"Sum(financials.Sales)"}
}]
}
棒 / 縦棒 / 折れ線 / ドーナツ — データの色
dataPoint.defaultColor— フォールバックカラー。
[以下略](元のドキュメントの残りの部分が翻訳されましたが、簡潔にするためにここで切り捨てます。)