AIで作るExcel商品の作成者欄を空にしても個人情報が残った

作成者欄を直したら、検査は合格しました。それでも売り物の家計簿 Excel には、持ち主の Windows ユーザー名が入ったまま残っていました。
2026 年 10 月 3 日、AI で下書きした家計簿テンプレートを想定し、2 枚のシートを持つ xlsx の最小サンプルを作りました。販売した実物ではなく、個人情報が残る典型パターンをわざと入れた実験用データです。最初の版は、作成者欄に架空の名前「山田太郎」を入れ、2 枚目の「メモ」シートを非表示にし、その中にC:\Users\yamada\Desktop\kakeibo_draft.xlsxという作業メモを置きました。
最初は作成者欄だけを検査しました。この検査は、作成者欄を直した時点で 0 件になりました。私は「これで販売前の確認は終わり」と判断しかけましたが、非表示シートを開くとパスがそのまま残っていました。今回はこの失敗を、3 段階の版で再現します。
前回から今回への差分
前回は、AI で下書きした予定表 ICS の開始時刻と改行を検査しました。今回は配布するファイルを Excel の xlsx に変え、買い手に見える中身ではなく、ファイルに紛れ込む作成者名・非表示シート・ローカルパスを検査します。
| 時点 | 確認できること | 残ること |
|---|---|---|
| 前回完了時 | ICSの開始時刻と改行を検査できます | xlsx内の個人情報は未確認です |
| 今回完了後 | 作成者欄、非表示シート、シート内のWindowsパスを検査できます | 数式、外部リンク、コメント、画像の属性、Excelでの実表示は別途確認します |
飛ばせる章: xlsx が zip 形式であることを知っている場合は「3 段階の版を同じ検査にかける」から進めます。
なぜ作成者欄だけでは足りないのか
xlsx は、XML ファイルを zip でまとめた形式です。作成者名はdocProps/core.xmlに、シートの表示状態はxl/workbook.xmlに、セルの文字列はxl/worksheets/の下にあります。置き場所が違うので、1 か所を直しても他の場所は変わりません。
Microsoft のサポートページは、共有前の文書に隠れたデータや個人情報が残る点を説明しています。調べて削除する手段として Document Inspector も案内しています。販売用のファイルは、1 人に渡すメモより多くの人へ複製されます。一度流出した名前やパスは取り戻せません。
作成者欄は、見落としにくい場所です。逆に、非表示シートの作業メモは作った本人も忘れます。AI に「整理して」と頼んだ結果、元のファイル名入りのメモが残る流れも想像できます。
[AIで家計簿の下書きを作る]
↓
[xlsxを展開して3か所を読む]
├─ core.xml : 作成者・最終更新者
├─ workbook.xml: 非表示シート
└─ sheet*.xml : Windowsパス
↓
├─ 1件以上 → 修正して再検査
└─ 0件 → Excelで開いて目視 → 人が販売可否を判断
自動検査が 0 件でも、販売の承認にはなりません。今回のスクリプトは、個人情報の完全な検出器ではなく、見落としやすい 3 か所に絞った門番です。
用語
| 用語 | この実験での意味 |
|---|---|
| xlsx | Excel のブックを保存する形式です。XML を zip でまとめています |
| core.xml | 作成者と最終更新者などを保存する、docProps下のファイルです |
| creator / lastModifiedBy | ブックを作った人と、最後に保存した人の欄です |
| 非表示シート | 画面上のタブからは見えなくなっているシートです |
| ローカルパス | C:\Users\...のように、作った人の PC の中の場所を示す文字列です |
| 偽陰性 | 問題があるのに検査が 0 件と答えてしまうことです |
3 段階の版を同じ検査にかける
まず検査用のスクリプトをxlsx_lab.pyに保存します。ファイルはメモリー上で組み立てるので、xlsx 自体はディスクに作りません。Python 3 の標準ライブラリだけで動きます。
cat > xlsx_lab.py <<'PY'
import io, re, zipfile
import xml.etree.ElementTree as ET
NS = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
RNS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
PKG = "http://schemas.openxmlformats.org/package/2006/relationships"
HEAD = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
def sheet(text):
return HEAD + '<worksheet xmlns="' + NS + '"><sheetData><row r="1"><c r="A1" t="inlineStr"><is><t>' + text + '</t></is></c></row></sheetData></worksheet>'
def build(author, hidden, memo):
state = ' state="hidden"' if hidden else ""
parts = {
"[Content_Types].xml": HEAD + '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/></Types>',
"_rels/.rels": HEAD + '<Relationships xmlns="' + PKG + '"><Relationship Id="rId1" Type="' + RNS + '/officeDocument" Target="xl/workbook.xml"/></Relationships>',
"xl/workbook.xml": HEAD + '<workbook xmlns="' + NS + '" xmlns:r="' + RNS + '"><sheets><sheet name="家計簿" sheetId="1" r:id="rId1"/><sheet name="メモ" sheetId="2"' + state + ' r:id="rId2"/></sheets></workbook>',
"xl/_rels/workbook.xml.rels": HEAD + '<Relationships xmlns="' + PKG + '"><Relationship Id="rId1" Type="' + RNS + '/worksheet" Target="worksheets/sheet1.xml"/><Relationship Id="rId2" Type="' + RNS + '/worksheet" Target="worksheets/sheet2.xml"/></Relationships>',
"xl/worksheets/sheet1.xml": sheet("月の支出"),
"xl/worksheets/sheet2.xml": sheet(memo),
"docProps/core.xml": HEAD + '<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:creator>' + author + '</dc:creator><cp:lastModifiedBy>' + author + '</cp:lastModifiedBy></cp:coreProperties>',
}
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as z:
for name, body in parts.items():
z.writestr(name, body)
return buf.getvalue()
def author_check(blob):
core = ET.fromstring(zipfile.ZipFile(io.BytesIO(blob)).read("docProps/core.xml"))
return [el.tag.split("}")[1] for el in core if el.text != "hibanas lab"]
def full_check(blob):
z = zipfile.ZipFile(io.BytesIO(blob))
found = ["作成者欄 " + t for t in author_check(blob)]
wb = ET.fromstring(z.read("xl/workbook.xml"))
found += ["非表示シート " + s.get("name") for s in wb.iter("{" + NS + "}sheet") if s.get("state")]
for name in z.namelist():
if name.startswith("xl/worksheets/"):
found += ["ローカルパス " + p for p in re.findall(r"[A-Za-z]:\\[^<\s]+", z.read(name).decode("utf-8"))]
return found
memo = "作業メモ C:\\Users\\yamada\\Desktop\\kakeibo_draft.xlsx"
stages = [
("初稿", build("山田太郎", True, memo)),
("中間稿", build("hibanas lab", True, memo)),
("修正稿", build("hibanas lab", False, "更新履歴 v1.0")),
]
for label, blob in stages:
print(label + ": 作成者欄だけの検査 " + str(len(author_check(blob))) + " 件 / 全体検査 " + str(len(full_check(blob))) + " 件")
for item in full_check(blob):
print(" - " + item)
assert [len(author_check(b)) for _, b in stages] == [2, 0, 0]
assert [len(full_check(b)) for _, b in stages] == [4, 2, 0]
PY
保存だけなので、出力はありません。次に実行します。
python3 xlsx_lab.py
初稿: 作成者欄だけの検査 2 件 / 全体検査 4 件
- 作成者欄 creator
- 作成者欄 lastModifiedBy
- 非表示シート メモ
- ローカルパス C:\Users\yamada\Desktop\kakeibo_draft.xlsx
中間稿: 作成者欄だけの検査 0 件 / 全体検査 2 件
- 非表示シート メモ
- ローカルパス C:\Users\yamada\Desktop\kakeibo_draft.xlsx
修正稿: 作成者欄だけの検査 0 件 / 全体検査 0 件
中間稿が失敗の本体です。作成者欄だけの検査は 0 件で、そのまま売りに出せる顔をしています。全体検査は同じファイルから 2 件を見つけました。末尾のassertがあるので、件数が想定とずれればコマンドは失敗します。
初稿の作成者欄はcreatorとlastModifiedByの 2 件です。この 2 件を直すだけでは、4 件のうち 2 件が残ります。修正稿では、作成者を中立な名前にし、メモシートを表示状態に戻し、パスを含まない更新履歴へ置き換えました。
ここまでの結果は、合成した 3 つのバイト列に対するものです。Excel や LibreOffice で開いた表示は確認していません。検査器は、Windows 形式のパスしか探しません。/Users/yamada/のような macOS のパスや、メールアドレス、氏名の本文は見つけられません。非表示シートはstate属性の有無で見ているだけで、非表示の行や列には触れていません。
次に変えること
実際の商品では、AI に頼んで作った xlsx を、人が保存し直すたびに作成者欄が書き換わります。納品直前に 1 度だけ検査しても、最後に保存した人が変わればやり直しです。検査は、書き出しの最後の工程に置きます。
この検査器は、stateの値がhiddenかveryHiddenかを区別しません。veryHiddenは Excel の通常の操作画面から再表示しにくい状態ですが、どちらも同じ「非表示シート」として数えます。実際の配布物では、Excel 標準の Document Inspector を最後に通し、結果をこの簡易検査と見比べます。
よくあるエラー
| 症状 | 原因 | 対処 |
|---|---|---|
| 作成者欄の検査が 0 件なのに全体検査が 0 件になりません | 非表示シートやシート内の文字列が残っています | xl/workbook.xmlのstateとxl/worksheets/の文字列を直します |
KeyError: 'docProps/core.xml'が出ます | 保存元のアプリがcore.xmlを作っていません | その場合は作成者情報がない状態として扱い、スクリプトに存在確認を足します |
BadZipFileが出ます | xlsx ではない、または壊れたファイルです | 元のファイルを Excel で開き直して保存し直します |
| パスが 1 件も見つからないのに個人名が残っています | 氏名が本文にあるか、パスが macOS 形式です | 検査語を足すか、Document Inspector と目視で確認します |
出典と検証範囲
| 一次情報 | この回で確認した事項 |
|---|---|
| Microsoft サポート: Document Inspector で非表示データと個人情報を削除する | 共有前の文書に残る隠れたデータや個人情報を Document Inspector で調べ、削除できます |
| Ecma International: ECMA-376 Office Open XML | xlsx のパッケージ構成、workbook.xmlのstate、core properties を定める標準です |
Python zipfile | zip を展開せずにメモリー上で読み書きできます |
Python xml.etree.ElementTree | XML を解析して要素と属性を取り出せます |
実測したのは上の最小サンプルです。販売する完成品が安全かどうかは、数式、外部リンク、コメント、画像、Excel での表示を含めて、別に確認します。


