はじめに
Microsoft Excel は同時に同じファイル名のワークブックを開けません。同名のワークブックを比較したいときに重宝するのが Microsoft 謹製ツールである Spreadsheet Compare です。フォルダが異なれば同名のワークブックでも比較できます。無意味ですが,同一のワークブックを比較することもできます。
しかし,この Spreadsheet Compare を使うと下記のエラーが発生することがあります。
この「オブジェクト参照がオブジェクトインスタンスに設定されていません。」というエラーはとても有名なようで,AI に尋ねると下記の回答が返ってきました。
✦ AI による概要
- Spreadsheet Compare(Inquireアドイン)の問題
- 原因
Spreadsheet CompareはExcelの「Inquire」アドインに含まれます。このアドインが破損・無効化・未インストールだとエラーになります。- 対処法
- アドインの有効化
Excelの「ファイル」>「オプション」>「COMアドイン」で「Inquire」にチェックを入れ、有効にします。- Officeの修復
コントロールパネルの「プログラムと機能」からOfficeを「変更」し、「修復」を実行します。- Inquireアドインの再インストール
Officeの再インストールが必要になることもあります。- プログラムの破損・競合(Datalizerなど)
- 原因
Excelアドイン(Datalizerなど)が関連している場合、クライアント情報が壊れている可能性があります。- 対処法
ウイングアーク1stのサポートページに記載されているDatalizer Clientの再インストール手順(クライアント情報のクリア、アンインストール、再インストール、更新)を試します。- カスタムプラグインや自動化スクリプトの問題
- 原因
Microsoft Learnにもあるように、カスタムプラグインがnull(空)のオブジェクトにアクセスしようとすると発生します。UiPathなどのRPAツールでExcel操作をしている場合、処理のタイミングや、データの読み込み前に関数やオブジェクトが準備できていないことが原因。- 対処法:
- Wait(待機)アクティビティ を追加して処理のタイミングを調整する。
- データの読み込み(範囲を読み込み) を先に行い、変数が初期化されていることを確認する。
- システムレベルの問題(Exchange Serverなど)
- 原因
Microsoft Learnのように、Exchangeなどのハイブリッド構成ウィザードで発生する場合は、setup /prepareADの再実行や、権限のある復元が必要になる場合があります。
しかし,自分のケースでは上記のいずれにも当てはまりませんでした。
先に結論を書く
別に Office が壊れている訳でもなく,ワークブックが壊れている訳でもない,あくまで正常な Excel ファイルであっても上記の「オブジェクト参照がオブジェクトインスタンスに設定されていません。」というエラーを引き起こす条件を一つ発見しました。
セルの書式「表示形式」にて「ユーザー定義」の日付に関する書式設定を行うと確実にエラーを引き起こします。具体的に言うと日本語ロケール特有の下記の書式です。
ge.m.dggge"年"m"月"d"日"yyyy"年"m"月"m"月"d"日"
そして,このエラーを回避できるように Excel ファイルを書き換えるツールを作成しました。別にファイルが壊れている訳ではないのでタイトルの「修復する」という表現自体適切ではないかもしれません。
エラー発生メカニズム
下記の Excel ファイルを例題とします。ファイル名は book1.xlsx とし,簡単のためワークシートは1つだけとします。
| A | B | C | D | E | |
| 1 | 文字列 | ||||
|---|---|---|---|---|---|
| 2 | 1,234 | ||||
| 3 | 2月4日 | ||||
| 4 |
Open XML 形式の Excel ファイルの場合,単なる zip ファイルなので適当な圧縮展開ツールを用いて展開することができます。
book1.xlsx
│ [Content_Types].xml
├─_rels
│ .rels
├─docProps
│ app.xml
│ core.xml
└─xl
│ sharedStrings.xml
│ styles.xml
│ workbook.xml
├─_rels
│ workbook.xml.rels
├─theme
│ theme1.xml
└─worksheets
sheet1.xml
このとき xl/worksheets/sheet1.xml にはセルに関する情報が保存されています。A1 のセルの型は文字列 t="s" であり,内容 <v>0</v> は xl/sharedStrings.xml に置かれた共用文字列テーブルの0番目であることを意味しています。次の A2 および A3 のセルの内容 <v>1234</v>,<v>46057</v> は直接書き込まれています。Excel では(2026年)2月4日は1900年1月1日を起点(=1)とした通算日数46057日目に当たります。また,表示形式はそれぞれ s="1",s="2" と指定されています。これらの表示形式は xl/styles.xml に置かれています。
<sheetData>
<row r="1" spans="1:1"><c r="A1" t="s"><v>0</v></c></row>
<row r="2" spans="1:1"><c r="A2" s="1"><v>1234</v></c></row>
<row r="3" spans="1:1"><c r="A3" s="2"><v>46057</v></c></row>
</sheetData>
以下は xl/sharedStrings.xml の共用文字列テーブルの内容です。sst とは Shared String Table の略でしょうか。
<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="1" uniqueCount="1">
<si><t>文字列</t><rPh sb="0" eb="3"><t>モジレツ</t></rPh><phoneticPr fontId="1"/></si>
</sst>
xl/styles.xml の内容です。先ほどの A2 および A3 のセルの参照するスタイルは下記 <xf> タグの要素(0番目から開始して)1番目と2番目に相当します。さらに <xf> タグの要素が数値形式やフォント,塗り潰し,境界線などを定義したテーブルを参照するという仕組みになっており,それらのテーブル自身も xl/styles.xml 内に記述されています。ここで下記の要素は参照されている全ての属性が定義されています。
- フォント:
fontId→<font> - 塗り潰し:
fillId→<fill> - 境界線:
borderId→<border>
しかし,<numFmt> の中に numFmtId="56" に対応した属性は見当たりません。numFmtId の大半は Excel 組み込みの定数として styles.xml には出力されず,ユーザ定義によるカスタム形式のみ styles.xml に出力されるようです。問題は 50~58 の範囲で,Excel ではロケールに合わせた日付形式に割り当てられているのですが,Excel では定義済み領域とみなす一方,Spreadsheet Compare では未定義領域とみなすためエラーになるようです。
<numFmts count="1">
<numFmt numFmtId="179" formatCode="#,##0_ "/>
</numFmts>
<fonts count="1">
<font><sz val="11"/><color theme="1"/><name val="Yu Gothic"/><family val="2"/><scheme val="minor"/></font>
</fonts>
<fills count="1">
<fill><patternFill patternType="none"/></fill>
</fills>
<borders count="1">
<border><left/><right/><top/><bottom/><diagonal/></border>
</borders>
<cellStyleXfs count="1">
<xf numFmtId="0" fontId="0" fillId="0" borderId="0"/>
</cellStyleXfs>
<cellXfs count="3">
<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>
<xf numFmtId="179" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/>
<xf numFmtId="56" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/>
</cellXfs>
<cellStyles count="1">
<cellStyle name="標準" xfId="0" builtinId="0"/>
</cellStyles>
ユーザ定義領域はどこからか?
参考文献1によると,サンプルコードで使用している ID がすべて 164 以上であることから,ユーザ定義領域は 164 以降であると AI が主張しているのですが,自分としてはそこまで強く断定できないと判断して上記の表現としています。
対策あるの?
参考文献2より numFmtId(ID)と実際の formatCode の対応を示します。エラーになる 50~58 の範囲の ID ですが,別の ID に全く同じ formatCode が存在することから,その ID に振り替えることができそうです。振り替え先の ID を alias とします。
| ID | formatCode | alias |
|---|---|---|
| 1 | 0 |
|
| 2 | 0.00 |
|
| 3 | #,##0 |
|
| 4 | #,##0.00 |
|
| 9 | 0% |
|
| 10 | 0.00% |
|
| 11 | 0.00E+00 |
|
| 12 | # ?/? |
|
| 13 | # ??/?? |
|
| 14 | mm-dd-yyy |
|
| 15 | d-mmm-yy |
|
| 16 | d-mmm |
|
| 17 | mmm-yyy |
|
| 18 | h:mm AM/PM |
|
| 19 | h:mm:ss AM/PM |
|
| 20 | h:mm |
|
| 21 | h:mm:ss |
|
| 22 | m/d/yy h:mm |
|
| 37 | #,##0 ;(#,##0) |
|
| 38 | #,##0 ;[Red](#,##0) |
|
| 39 | #,##0.00;(#,##0.00) |
|
| 40 | #,##0.00;[Red](#,##0.00) |
|
| 45 | mm:ss |
|
| 46 | [h]:mm:ss |
|
| 47 | mmss.0 |
|
| 48 | ##0.0E+0 |
|
| 49 | @ |
| ID | formatCode | alias |
|---|---|---|
| 27 | [$-411]ge.m.d |
|
| 28 | [$-411]ggge"年"m"月"d"日" |
|
| 29 | [$-411]ggge"年"m"月"d"日" |
|
| 30 | m/d/yyy |
|
| 31 | yyyy"年"m"月"d"日" |
|
| 32 | h"時"mm"分" |
|
| 33 | h"時"mm"分"ss"秒" |
|
| 34 | yyyy"年"m"月" |
|
| 35 | m"月"d"日" |
|
| 36 | [$-411]ge.m.d |
|
| 50 | [$-411]ge.m.d |
27 |
| 51 | [$-411]ggge"年"m"月"d"日" |
28 |
| 52 | yyyy"年"m"月" |
34 |
| 53 | m"月"d"日" |
35 |
| 54 | [$-411]ggge"年"m"月"d"日" |
28 |
| 55 | yyyy"年"m"月" |
34 |
| 56 | m"月"d"日" |
35 |
| 57 | [$-411]ge.m.d |
27 |
| 58 | [$-411]ggge"年"m"月"d"日" |
28 |
ちなみに日付書式に関する各国ロケールの比較を以下に示しますが,各国バラバラな印象です。例えば 55 は日本と韓国では年月日表示ですが,中台では午前・午後付きの時分表示です。これではロケールを切り替えたらちょっとしたトラブルになるのではないでしょうか?おそらく,これらの領域はロケールを切り替えることを考慮しておらず,むしろ各国のローカライズ担当者が自由に定義した印象です。
| $\small\textsf{ID}$ | $\textsf{ja-jp}\hspace{172px}$ | $\textsf{ko-kr}\hspace{140px}$ | $\textsf{zh-tw}\hspace{172px}$ | $\textsf{zh-cn}\hspace{172px}$ |
|---|---|---|---|---|
| 27 | [$-411]ge.m.d | yyyy"年" mm"月" dd"日" | [$-404]e/m/d | yyyy"年"m"月" |
| 28 | [$-411]ggge"年"m"月"d"日" | mm-dd | [$-404]e"年"m"月"d"日" | m"月"d"日" |
| 29 | [$-411]ggge"年"m"月"d"日" | mm-dd | [$-404]e"年"m"月"d"日" | m"月"d"日" |
| 30 | m/d/yyy | mm-dd-yyy | m/d/yyy | m-d-yy |
| 31 | yyyy"年"m"月"d"日" | yyyy"년" mm"월" dd"일" | yyyy"年"m"月"d"日" | yyyy"年"m"月"d"日" |
| 32 | h"時"mm"分" | h"시" mm"분" | hh"時"mm"分" | h"时"mm"分" |
| 33 | h"時"mm"分"ss"秒" | h"시" mm"분" ss"초" | hh"時"mm"分"ss"秒" | h"时"mm"分"ss"秒" |
| 34 | yyyy"年"m"月" | yyyy-mm-dd | 上午/下午h"時"mm"分" | 上午/下午h"时"mm"分" |
| 35 | m"月"d"日" | yyyy-mm-dd | 上午/下午h"時"mm"分"ss"秒" | 上午/下午h"时"mm"分"ss"秒" |
| 36 | [$-411]ge.m.d | yyyy"年" mm"月" dd"日" | [$-404]e/m/d | yyyy"年"m"月" |
| 50 | [$-411]ge.m.d | yyyy"年" mm"月" dd"日" | [$-404]e/m/d | yyyy"年"m"月" |
| 51 | [$-411]ggge"年"m"月"d"日" | mm-dd | [$-404]e"年"m"月"d"日" | m"月"d"日" |
| 52 | yyyy"年"m"月" | yyyy-mm-dd | 上午/下午h"時"mm"分" | yyyy"年"m"月" |
| 53 | m"月"d"日" | yyyy-mm-dd | 上午/下午h"時"mm"分"ss"秒" | m"月"d"日" |
| 54 | [$-411]ggge"年"m"月"d"日" | mm-dd | [$-404]e"年"m"月"d"日" | m"月"d"日" |
| 55 | yyyy"年"m"月" | yyyy-mm-dd | 上午/下午h"時"mm"分" | 上午/下午h"时"mm"分" |
| 56 | m"月"d"日" | yyyy-mm-dd | 上午/下午h"時"mm"分"ss"秒" | 上午/下午h"时"mm"分"ss"秒" |
| 57 | [$-411]ge.m.d | yyyy"年" mm"月" dd"日" | [$-404]e/m/d | yyyy"年"m"月" |
| 58 | [$-411]ggge"年"m"月"d"日" | mm-dd | [$-404]e"年"m"月"d"日" | m"月"d"日" |
今回,初めて Qiita の記事内にハングルを書きました。
修復ツールを作る
ということで numFmtId を書き換えるツールを作ることにしました。開発言語は C# とします。Windows についてくる .NET Framework 4 世代のコンパイラで作ります。
実装コード
C# コードを以下に示します。今回,XML ドキュメントの取り扱いには .NET Framework の頃からある XmlDocument クラスを用いました。
using System;
using System.IO;
using System.IO.Compression;
using System.Xml;
using System.Collections.Generic;
//------------------------------------------------------------------------------
// Spreadsheet Compare でエラーになる Excel ファイルを調査・修正する
//------------------------------------------------------------------------------
class XlsNumFmtId {
//--------------------------------------------------------------------------
// 定数定義
//--------------------------------------------------------------------------
static string styles_xml = "xl/styles.xml";
//--------------------------------------------------------------------------
// numFmtId の振り替えリスト
//--------------------------------------------------------------------------
static Dictionary<uint,uint> alias = new Dictionary<uint,uint> {
{ 50, 27 }, // [$-411]ge.m.d
{ 51, 28 }, // [$-411]ggge"年"m"月"d"日"
{ 52, 34 }, // yyyy"年"m"月"
{ 53, 35 }, // m"月"d"日"
{ 54, 28 }, // [$-411]ggge"年"m"月"d"日"
{ 55, 34 }, // yyyy"年"m"月"
{ 56, 35 }, // m"月"d"日"
{ 57, 27 }, // [$-411]ge.m.d
{ 58, 28 } // [$-411]ggge"年"m"月"d"日"
};
//--------------------------------------------------------------------------
// メイン関数
//--------------------------------------------------------------------------
static int Main(string[] args) {
//----------------------------------------------------------------------
// ヘルプメッセージ
//----------------------------------------------------------------------
if(args.Length == 0) {
Console.Error.WriteLine("Spreadsheet Compare でエラーになる Excel ファイルを調査・修正します。");
Console.Error.WriteLine("");
Console.Error.WriteLine("XlsNumFmtId(.exe) [入力ファイル名] (出力ファイル名)");
Console.Error.WriteLine("");
Console.Error.WriteLine("{0} の <xf> タグの下記 numFmtId を置き換えます。", styles_xml);
foreach(var key in alias.Keys)
Console.Error.WriteLine("{0} → {1}", key, alias[key]);
return -1;
}
//----------------------------------------------------------------------
// コマンドライン解析
//----------------------------------------------------------------------
var in_file = args[0];
var out_file = (args.Length > 1) ? args[1] : "";
//----------------------------------------------------------------------
// 入出力ファイルのチェック
//----------------------------------------------------------------------
if(!File.Exists(in_file)) {
Console.Error.WriteLine("入力ファイル {0} が存在しません!!", in_file);
return -1;
}
if(out_file != "" &&
String.Equals(Path.GetFullPath(in_file), Path.GetFullPath(out_file), StringComparison.OrdinalIgnoreCase)) {
Console.Error.WriteLine("入力ファイルと出力ファイルが同じです!!");
return -1;
}
//----------------------------------------------------------------------
// ワークブックを開いて numFmtId をチェックする
//----------------------------------------------------------------------
Console.Out.WriteLine("ワークブック {0} を調査します。", in_file);
using(var in_stream = new FileStream(in_file, FileMode.Open, FileAccess.Read, FileShare.Read))
using(var in_zip = new ZipArchive(in_stream, ZipArchiveMode.Read)) {
//------------------------------------------------------------------
// styles.xml を調査する
//------------------------------------------------------------------
var entry = in_zip.GetEntry(styles_xml);
if(entry == null) {
Console.Error.WriteLine("{0} が存在しません!!", styles_xml);
return -1;
}
Console.Out.WriteLine("{0} を調査します。", styles_xml);
var doc = new XmlDocument();
doc.PreserveWhitespace = true;
using(var stream = entry.Open()) {
doc.Load(stream);
}
//------------------------------------------------------------------
// 既定の名前空間を x とする
//------------------------------------------------------------------
var man = new XmlNamespaceManager(doc.NameTable);
man.AddNamespace("x", "http://schemas.openxmlformats.org/spreadsheetml/2006/main");
//------------------------------------------------------------------
// <cellXfs> タグを取得する
//------------------------------------------------------------------
var xfs = doc.SelectSingleNode("//x:cellXfs", man);
if(xfs == null) {
Console.Error.WriteLine("cellXfs が見つかりません!!");
return -1;
}
//------------------------------------------------------------------
// <xf> タグの numFmtId を調査する
//------------------------------------------------------------------
var count = 0;
foreach(XmlElement xf in xfs.ChildNodes) {
if(xf.Name != "xf") continue;
var attr = xf.GetAttributeNode("numFmtId");
if(attr == null) continue;
uint id;
if(!uint.TryParse(attr.Value, out id)) continue;
if(alias.ContainsKey(id)) {
attr.Value = alias[id].ToString();
Console.Out.WriteLine("数値書式ID:{0} → {1}", id, alias[id]);
count++;
}
}
if(count == 0) {
Console.Out.WriteLine("エラーとなる数値書式はありません。");
return 0;
}
if(out_file == "") {
Console.Out.WriteLine("エラーとなる数値書式が {0} 個あります。", count);
return 0;
}
//------------------------------------------------------------------
// 修正したワークブックを保存する
//------------------------------------------------------------------
using(var out_stream = new FileStream(out_file, FileMode.Create, FileAccess.Write))
using(var out_zip = new ZipArchive(out_stream, ZipArchiveMode.Create)) {
foreach(var in_entry in in_zip.Entries) {
var out_entry = out_zip.CreateEntry(in_entry.FullName);
if(string.Equals(in_entry.FullName, styles_xml, StringComparison.OrdinalIgnoreCase)) {
using(var stream = out_entry.Open())
doc.Save(stream);
} else {
using(var src = in_entry.Open())
using(var dst = out_entry.Open())
src.CopyTo(dst);
}
}
Console.Out.WriteLine("エラーとなる数値書式を {0} 個修正しました。", count);
Console.Out.WriteLine("ワークブック {0} に書き込みました。", out_file);
}
}
return 0;
}
}
技術解説
余計な空白や改行を入れないようにする
Excel の出力する XML ファイルは余計な空白や改行が入りませんが,XmlDocument の出力する XML ファイルには見易いようインデントや改行がデフォルトで入ります。Excel と同様に余計な空白や改行を入れないようにするには下記の指定を行います3。
doc.PreserveWhitespace = true;
名前空間について
Excel の出力する XML ファイルを見ると <cellXfs> タグにはプリフィクスが付いていなかったので当初下記のように指定していたら「cellXfs が見つかりません!!」とエラーになりました。
var xfs = doc.SelectSingleNode("//cellXfs");
参考文献4には下記の記載があるのですが,一見してよく分かりません。
XPath 式にプレフィックスが含まれない場合は、名前空間 URI は空の名前空間であると仮定されます。 XML に既定の名前空間が含まれる場合でも、プレフィックスと名前空間 URI を XmlNamespaceManager に追加する必要があります。そうしないと、ノードは選択されません。
参考文献5を読んでようやく理解できました。プレフィックスのつかないタグは「既定の名前空間」に含まれることになるのですが,SelectionSingleNode でプレフィックスをつけないで指定すると「空の名前空間」として扱われるのです。どうやら「空の名前空間」は「既定の名前空間」とは異なる概念のようです。「既定の名前空間」にあるタグを参照するには名前空間 URI(この場合は http://schemas.openxmlformats.org/spreadsheetml/2006/main )に適当なプレフィックス(この場合は x)を付けて参照するしかないということです。
var man = new XmlNamespaceManager(doc.NameTable);
man.AddNamespace("x", "http://schemas.openxmlformats.org/spreadsheetml/2006/main");
var xfs = doc.SelectSingleNode("//x:cellXfs", man);
ビルド方法
ZipArchive を使うので System.IO.Compression.dll への参照が必要です。
csc xlsNumFmtId.cs -r:System.IO.Compression.dll
使い方
引数なしで実行するとヘルプメッセージを表示します。
c:\Qiita>xlsnumfmtid
Spreadsheet Compare でエラーになる Excel ファイルを調査・修正します。
XlsNumFmtId(.exe) [入力ファイル名] (出力ファイル名)
xl/styles.xml の <xf> タグの下記 numFmtId を置き換えます。
50 → 27
51 → 28
52 → 34
53 → 35
54 → 28
55 → 34
56 → 35
57 → 27
58 → 28
出力ファイル名を省略すると xl/styles.xml を検査してエラーとなる数値書式を抽出します。数値書式の置き換え候補(この場合は 35)も示しますが,ファイルに変更は加えません。
c:\Qiita>xlsnumfmtid book1.xlsx
ワークブック book1.xlsx を調査します。
xl/styles.xml を調査します。
数値書式ID:56 → 35
エラーとなる数値書式が 1 個あります。
出力ファイル名を指定すると修正した内容を書き込みます。
c:\Qiita>XlsNumFmtId book1.xlsx book2.xlsx
ワークブック book1.xlsx を調査します。
xl/styles.xml を調査します。
数値書式ID:56 → 35
エラーとなる数値書式が 1 個修正しました。
ワークブック book2.xlsx に書き込みました。
既に修正済みの Excel ファイルに対して実行すると,たとえ出力ファイル名を指定しても出力しません。
c:\Qiita>XlsNumFmtId book2.xlsx book3.xlsx
ワークブック book2.xlsx を調査します。
xl/styles.xml を調査します。
エラーとなる数値書式はありません。
こうして書き換えた Excel ファイルでは無事 Spreadsheet Compare でエラーを起こすことがなくなりました。めでたしめでたし。
注意事項
- 本ツールで修正した Excel ファイルを再び Excel で開いて保存すると,せっかく書き換えた数値書式が元に戻ります。
- 拡張子が
.xlsbの Excel ファイルは取り扱えません。
余談
2026年2月5日,上記のバグをフィードバック Hub に報告しましたが,いつ修正されるでしょうか・・・