古いブログ記事の個人情報・著作権チェック:画像85枚を外した記録

2021年の記事67本を再公開する前に、本文と画像を監査した。コンパイラのパスに残ったWindowsアカウント名、サンプルデータの実名、転載に見える図。確認方法と、残す・差し替える・消すの判断基準。

昔のブログ記事を再公開したり翻訳したりする前に、まずやっておくべき確認があります。何年も前に書いたチュートリアルには、当時は誰も気にしなかった個人情報や著作権のリスクが残っていることがあります。私たちは2021年に書いたプログラミング講座67本を、英語・日本語・韓国語の新しい記事に改訂しました。公開し直す前に、全記事の本文と、使っている画像を1枚残らず監査しています。その結果、実名、Windowsのアカウント名、画像85枚を取り除きました。この記事では、何を探し、どう確かめたかを書きます。

改訂そのものは並列のAIサブエージェントが行い、その流れはAIエージェント18体で2021年の記事67本を並列改訂した記録にまとめました。各エージェントが共通ブリーフに沿って担当記事を監査し、デプロイ前にメインセッションがサイト全体を確認しています。当然ですが、以下では名前やパスを一般化した形でしか書きません。

見つかったもの

何が どこに 対応
Windowsのアカウント名 貼り付けたコンパイルエラーや、自分のパスを表示するプログラムの出力(C:\Users\<name>\Documents\Visual Studio …)。複数の記事 今のコンパイラで出し直した出力に置き換え、パスは省略
同じ種類のパスが写った画像 2021年の例外処理のスクリーンショット 画像を削除し、記事には .NET 10 の実際の出力を掲載
異なる2つのアカウント名 2021年の記事全体 どちらもすべて削除
作者の名前をサンプルデータに 2つの記事でサンプルコードの値として使用 中立的なサンプル名に変更
実在の芸能人の名前 1つの記事のサンプルデータ 変更
サインイン中のプロフィール画像 IDEのメインウィンドウのスクリーンショット 画像を削除
他人のユーザーフォルダ セットアップ記事で流用したベンダーの宣伝画像 画像を削除
教科書風の図 グラフ、迷路、キュー、オブジェクトの図、「図3.」という見出し付きの図 自前の図に差し替え
ベンダーのチュートリアル画面 ファイル名と赤枠の付け方がベンダー公式の資料と一致した2枚 差し替え
ストックフォトと商品写真 列に並ぶ人々の3Dストック画像、デザイナー本棚の商品写真 削除
ゲームの画面 今はOSに付属しないゲームのスクリーンショット 削除
手描きの文字の形 誰かのイニシャルかもしれないコンソールアート 念のため削除

意外だったことが2つあります。1つは、危ない本文は地の文ではなく、いつも貼り付けた出力だったことです。「コンパイラが出したものをそのまま貼っただけ」なので、書き手は読まずにコピーします。もう1つは、出し直した出力にもローカルのパスが入ることです。Win32の記事用に新しく取ったコンパイラ出力には、ツールチェーンのインストール先のパスが入っていました。エージェントは公開前にそれを …/winuser.h に縮めています。コードを実行し直しただけでは、出力はきれいになりません。

本文:パターン検索と、その限界

各エージェントは自分の担当記事を検索し、ウェーブが終わるたびにメインセッションがサイト全体を検索しました。使ったパターンは特定の名前ではなく構造を探すものなので、誰の環境でもそのまま使えます。

"""Scan Markdown posts for text that often leaks personal data. Prints counts, not the values."""
import re, sys, pathlib, collections

RULES = {
    "windows-user-folder": r"[Cc]:\\+[Uu]sers\\+[^\\\s<>]+",
    "unix-home-dir":       r"/(?:Users|home)/[a-z][\w.-]+",
    "email":               r"[\w.%+-]+@[\w-]+(?:\.[\w-]+)+",
    "temp-dir":            r"/var/folders/\w+",
}
ALLOW = re.compile(r"example\.|/me\b|\bid@")   # placeholders we wrote on purpose

root = pathlib.Path(sys.argv[1])
hits = collections.Counter(); files = collections.defaultdict(set); allowed = 0
for p in root.rglob("*.md"):
    for n, line in enumerate(p.read_text(encoding="utf-8").splitlines(), 1):
        for name, rx in RULES.items():
            for m in re.finditer(rx, line):
                if ALLOW.search(m.group(0)):
                    allowed += 1
                    continue
                hits[name] += 1; files[name].add(str(p))
print("files scanned:", sum(1 for _ in root.rglob("*.md")))
for name in RULES:
    print(f"{name:20} {hits[name]:3} hits in {len(files[name])} files")
print("placeholders skipped:", allowed)

今日、ブログの content/ フォルダに対して実行した結果です(Python 3.9、標準ライブラリのみ)。

files scanned: 259
windows-user-folder    3 hits in 3 files
unix-home-dir          0 hits in 0 files
email                  9 hits in 6 files
temp-dir               0 hits in 0 files
placeholders skipped: 27

残った12件は目で確認しました。Windowsの3件は、セットアップ記事で「古いスクリーンショットにユーザーフォルダが写っていた」と説明している箇所の C:\Users\… で、名前は省略記号になっています。メールの9件は、許可リストが拾わない大文字の Dev@Example.com と、正規表現の記事でわざと使ったテスト用アドレス(一部は非ASCIIのドメイン)でした。これをスキャンで判定することはできません。スキャンにできるのは、人が読める長さまで一覧を短くすることだけです。

このスキャンでは名前は捕まりません。運営者の名前を探す最初のサイト全体検索は単語境界付きのパターンで、サンプルの識別子の中で名前が別の語とくっついていた記事を1本取りこぼしました。それを見つけたのは、記事を読んでいたエージェントです。パターンを広げて検索し直し、デプロイ前の最終スキャンでは何も出ませんでした。名前は、名前ごとにパターンを作り、大文字小文字を区別せず、単語境界なしで探す必要があります。そのうえで、サンプルデータを人が読むこと。いかにも適当なサンプル値の中にこそ、実名は紛れ込みます。

画像:1枚残らず開く

手元の環境にはOCRがなく、画像の監査は自動化できませんでした。それで構わないと判断しました。ブリーフでは、各エージェントに2021年の記事が参照する画像をすべて開いて見て判断するよう指示しています。最後の6本のバッチを始めた時点で、旧画像のフォルダには114個のファイルがありました。

画像を開くと、本文のスキャンでは絶対に見つからない問題が見つかりました。アイコンには文字がありません。スクリーンショットの中のユーザー名は画素です。教科書から写した図には透かしもなく、ただ教科書らしい見た目をしているだけです。エージェントは見たままを報告しました。「ユーザーフォルダのパスが写ったMicrosoftの宣伝画像」「サインイン中のアカウントのプロフィール画像が写ったVS2019のメインウィンドウ」「列に並ぶ人々の3Dストック画像」。

画像は1枚ずつ、同じ規則に同じ順で当てはめました。

判断図。古い画像から始める。ユーザー名、フォルダのパス、プロフィール画像などの個人情報が写っているか。写っていれば削除する。写っていなければ、自分たちで作ったもので、今も正確か。そうでなければ自前の図に差し替える。そうであれば残す。ラベルが韓国語だけなら韓国語版にだけ残す。
各エージェントが2021年の画像1枚ずつに当てはめた規則。個人情報を最初に確認する。それが写っていれば、ほかの長所があっても残せないからだ。

個人情報を最初に見るのは、ほかの長所では埋め合わせられないからです。正確で役に立ち、自分たちで作ったスクリーンショットでも、アカウントが写っていれば外します。次に、明らかに自分たちのもので、今も正確かを問います。自作であるだけでは足りません。制約の表が間違っていた図、2次元配列なのにジャグ配列のような添字を示していた図も外しました。

残す・差し替える・消す

画像の状態 判断 今回の例
ユーザー名、フォルダのパス、メール、アイコンが写っている 正確でも削除 ローカルパスが写った例外の画面、アイコンが写ったIDE
第三者のもの(ストック、商品、ゲーム画面、ベンダーの宣伝) 削除 キューのストック画像、本棚の写真、ベンダーの宣伝画像
転載の可能性(教科書風、ベンダーのチュートリアル風) 自前の図に差し替え BFS/DFS記事のグラフと迷路の図
自作だが誤り 差し替え 誤りのある制約の表
自作だが古い(昔のインストーラー、昔のOS) 削除。考え方が必要なら差し替え 2019年のセットアップ手順のインストーラー・IDE画面11枚
自作で正確、ラベルが韓国語だけ 韓国語版にだけ残す 韓国語ラベルのジェネリクスの図
自作で正確、言語に依存しない 全言語で残す 1次元配列の図

下の2行が大事です。全部消す監査は簡単ですが、本物の作品まで失います。最後のバッチの開始時にディスクにあった画像のうち、28枚は改訂後の記事でも使っています。

コードから描く図に差し替える

考え方として必要だった図は、すべてYAMLで定義してビルド時にSVGへ描く図に置き換えました。箱、矢印、棒グラフを3言語で描き、ダークモードとスマートフォン用の配置にも対応します。改訂した67本には、こうした図が全部で108個あります。テキストから描くので、著作権の答えは単純です。私たちが書いたものです。名前は example.com や <app> のようなプレースホルダーだけなので、うっかりアイコンやパスが入ることもありません。

図の役割も変わりました。古い画像の多くは、IDEやインストーラーのある瞬間を写したスクリーンショットで、すぐに古くなります。新しい図が示すのは考え方です。ジャグ配列がメモリにどう並ぶか、貪欲法がどこで失敗するか。こちらは古くなりません。

6段階のフローチャート。1つ目は、ユーザーフォルダ、ホームディレクトリ、メールアドレスのパターンで本文を検索する。2つ目は、旧記事が参照する画像をすべて開く。3つ目は、画像ごとに残す・差し替える・削除するを決める。4つ目は、差し替える画像の代わりに自前のSVG図を描く。5つ目は、使われなくなった画像をローカルのバックアップフォルダへ移す。6つ目は、ドライランを先に行ってからサーバーから削除し、消したURLが404を返すことを確かめる。
実施した順に並べた監査の流れ。ローカルのバックアップがあり、ドライランで消えるファイルが正確に分かるまで、サーバーからは何も消していない。

バックアップ、削除、404の確認

記事から画像を外しても、インターネットからは消えません。ファイルは古いURLに残り、そのURLは画像検索にすでに載っているかもしれません。だから削除は4段階にしました。

  1. 未使用のファイルを一覧にする。 旧画像フォルダの全ファイルと、記事中の /static/img/legacy/… への参照を短いスクリプトで突き合わせます。
  2. 削除せず、ローカルのバックアップへ移す。 判断を誤っても、元のファイルはコピー1回で戻せます。
  3. pruneを付けてデプロイのドライランを行う。 デプロイスクリプトは、自分でアップロードしたファイルだけを、許可リストのパスの下でだけ削除します。ドライランが正確な一覧を出すので、画像だけであることを確かめました。prune would delete: 74 | non-image: []。
  4. デプロイ後、削除したURLを要求して404を確かめる。 1回目はローカルパスが写ったスクリーンショット、2回目はアイコンが写ったIDEのスクリーンショットでした。
404 /static/img/legacy/2021/06/20190404_171228_204.png

最後はCDNのキャッシュ削除で、これは運営者がホスティングの管理画面から行います。CDNは、キャッシュが切れるまで削除済みのファイルを配信し続けることがあるからです。

2回のデプロイの合計は、1回目が11枚、2回目が74枚で、85枚を退避してサーバーから削除しました。すべてローカルに残してあります。ただ、消えるはずだったのに消えなかったファイルが1つあります。この記事を書きながら、同じ未使用画像のスクリプトを実行した結果です。

29 files, 28 referenced, 1 unused
   /static/img/legacy/2021/07/st.jpg

デザイナー本棚の商品写真です。未使用75件の一覧には入っていましたが、移動するシェルのループが一覧の最終行を読み落としました。ファイルの最後に改行がなかったためです。どの記事にも使われていないのに、サーバーの古いURLには残っていました。この記事を公開するのと同じデプロイで削除します。ループのバグの話はAIエージェントの最初の診断が間違っていたときに書きました。監査としての教訓は、一覧にした数、移した数、サーバーが消した数を比べることです。

棒グラフ。85枚はローカルのバックアップへ移し、サーバーから削除した。28枚は改訂後の記事が今も参照しているので残した。未使用の1枚は移動ループから漏れ、この記事のために数え直したときに見つかった。
最後の6本のバッチを始めた時点でディスクに残っていた旧画像114枚の行き先。デプロイのログと画像フォルダから数えた。

正直な限界

  • 「転載に見える」は判断で、証明ではありません。 すべての図を出どころまでたどったわけではなく、自信を持って自作と言えないものは外しました。その中には、たぶん自作のものも含まれています。
  • OCRなし。 画像の中の文字は1枚ずつ目で読みました。114枚なら可能でしたが、数千枚では無理です。
  • ウェブは忘れません。 サーバーからの削除で止まるのは、私たちのサーバーからの配信だけです。検索エンジンのキャッシュやウェブアーカイブの複製は消えません。本当に機微な情報なら、検索エンジンへの削除依頼が別に必要です。
  • 法的な助言ではありません。 自分たちの記事に保守的な規則を当てはめただけです。特定の画像が引用として認められるかどうかは、資格のある人が答えるべき問題です。
  • スキャンはその時点の記録です。 上の本文スキャンは執筆時点の259ファイルが対象でした。新しい記事にも同じスキャンが必要なので、今はすべてのエージェントのブリーフに入れています。

要するに、貼り付けた出力を読み、画像はすべて開き、図は自分で描き、ローカルにバックアップを残し、削除は404と件数で証明する、ということです。

よくある質問

古いプログラミング記事のどこに個人情報が潜んでいる?

ほとんどは貼り付けたツールの出力です。Windowsのユーザーフォルダを含むコンパイルエラーやスタックトレース、サインイン中のアカウントのアイコンが写ったIDEのスクリーンショット、作者が自分の名前を入れたサンプルデータ。書いている最中は、どれも個人情報には見えません。

記事から画像を外すだけで十分?

不十分です。参照を消してもファイルは古いURLに残り、検索エンジンがそのURLを知っている可能性もあります。私たちは未使用のファイルをローカルのバックアップへ移し、ドライランを先に行ってからサーバーから削除し、URLが404を返すことを確かめました。最後にCDNのキャッシュ削除を運営者が行います。

著作権上のリスクがある画像はどう判断した?

2021年の画像すべての出どころは証明できないので、規則を決めました。明らかに自分たちの作品で、今も正確なものだけを残す。教科書風の図、ベンダーのチュートリアルのスクリーンショット、ストックフォト、商品写真は、コードから描く自前の図に差し替えました。

  • #個人情報
  • #著作権
  • #コンテンツ監査
  • #画像
  • #AIエージェント
  • #ブログ運営

t.laboratory

実際に作って運用しているアプリ・サーバー・サービスで起きた問題と、その解決に使ったコードを記録する技術ブログです。

運営者情報 · アプリ

↑