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

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 か所に絞った門番です。

用語

用語この実験での意味
xlsxExcel のブックを保存する形式です。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 XMLxlsx のパッケージ構成、workbook.xmlのstate、core properties を定める標準です
Python zipfilezip を展開せずにメモリー上で読み書きできます
Python xml.etree.ElementTreeXML を解析して要素と属性を取り出せます

実測したのは上の最小サンプルです。販売する完成品が安全かどうかは、数式、外部リンク、コメント、画像、Excel での表示を含めて、別に確認します。

この記事をシェア