ObsidianからNotionへ移行すると画像だけが飛ぶ — Notion AIで復旧する手順とプロンプト

はじめに

Obsidian の Vault を zip にして Notion にインポートすると、フォルダ構造もノート本文もそのまま再現される。ここまでは驚くほど素直に進む。

ところが画像だけが再現されない。ノートの中に ![[画像名.png]] という文字列がそのまま残り、画像は表示されない。添付フォルダは空の殻として作られる。

この復旧を Notion AI にやらせた。作業自体はうまくいったのだが、行き当たりばったりに進めたせいでかなり遠回りをした。同じことをやる人が最短距離を通れるよう、手順とプロンプトをまとめておく。

結論を先に書くと、効率を決めるのは Notion AI のプロンプトではなく、インポート前に Vault を測っておくことである。筆者の Vault はノート 2,659 ファイルあったが、画像の埋め込みは 115 箇所・30 ファイルにしかなく、しかもその半分以上が 4 ファイルに固まっていた。これを先に知っていれば、全ノートを見て回る必要はなかった。

移行の全体像

ObsidianからNotionへの移行フロー

ポイントは、テキストと画像で通る経路が違うことである。

テキスト 画像
経路 設定 → インポート → テキスト & Markdown Notion AI にファイルを添付
主な制限 zip 1つあたり 5GB 200MB 程度で弾かれる
やってくれること 構造・本文の再現 ファイル名で照合して挿入

インポート機能は Obsidian の画像記法を解釈しないので、画像を復旧するには別経路で渡すしかない。そしてその別経路のほうが制限が厳しい。ここが全体の形を決めている。

なお、テキスト側の 5GB はまず問題にならない。Vault の容量の大半は画像であり、それはインポートでは取り込まれないからだ。筆者の Vault は全体 325MB だったが、Markdown だけなら 9.3MB しかない。「容量が大きいから分割しなければ」と身構える必要はない。

代わりに気にすべきは画像側である。ここは後述するとおり、全画像を渡す必要はない

なぜ画像リンクだけ再現されないのか

![[画像名.png]] は Markdown の標準記法ではなく、Obsidian 独自の Wiki リンク記法である。Notion のインポータは標準的な Markdown として解釈するため、この記法は画像として扱われず、ただの文字列として残る。

副作用として、画像ファイル自体もアップロードされない。zip の中に画像が入っていても、参照している記法が理解されない以上、Notion 側は「必要な添付ファイル」と認識しない。attachments フォルダは空のページとして作られる。

同じ理由で [[ノート名]] 形式の内部リンクも変換されない。こちらは移行後に手で張り直すことになる。

裏を返せば、残った ![[...]] という文字列そのものが復旧作業のマーカーになる。Notion AI に「この文字列が残っている箇所を探せ」と指示できるので、後の工程が安定する。消えてしまうよりずっと扱いやすい。

Step1 棚卸し — Vault を測る

移行前に、次の3つを出しておく。

  1. 画像の埋め込みが何箇所・何ファイルにあるか
  2. それがどのファイルに集中している
  3. 参照先の実体が存在しないものがどれだけあるか

Vault のルートで次を実行する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import os, re, collections

VAULT = "."
IMG = ('.png', '.jpg', '.jpeg', '.webp', '.gif', '.svg', '.pdf')

def walk(vault):
for root, dirs, names in os.walk(vault):
dirs[:] = [d for d in dirs if not d.startswith('.')] # 設定・キャッシュを除外
for n in names:
yield n, os.path.join(root, n)

# ファイル名 → 実体のパス。Obsidian は名前だけで参照を解決するので同じ規則にする
files = {}
for name, path in walk(VAULT):
files.setdefault(name, path)

notes = [p for _, p in walk(VAULT) if p.endswith('.md')]

per_file = collections.Counter()
missing = collections.Counter()
for path in notes:
text = open(path, encoding='utf-8', errors='ignore').read()
for embed in re.findall(r'!\[\[([^\]|#]+)', text):
name = os.path.basename(embed.strip())
if not name.lower().endswith(IMG):
continue
per_file[path] += 1
if name not in files:
missing[name] += 1

print(f"埋め込み {sum(per_file.values())} 箇所 / {len(per_file)} ファイル")
print(f"参照先の実体が無い {len(missing)} 件")
for path, count in per_file.most_common(10):
print(f"{count:4d} {path}")

筆者の Vault では次のようになった。

1
2
3
4
5
6
7
埋め込み 115 箇所 / 30 ファイル
参照先の実体が無い 33 件
38 ./Projects/.../出張支払い.md
18 ./Projects/.../出張支払い.md
8 ./Projects/.../出張支払い.md
7 ./Projects/.../出張支払い.md
...

上位4ファイルで 71 箇所、全体の6割を超える。領収書のスクリーンショットを貼った経費精算メモである。作業の半分以上がここに集中していると分かっていれば、まずこの4ファイルを片付けてから残りに移るという順序が立つ。

3つめの「実体が無い 33 件」も重要で、これは次章で扱う。

「実体が無い」は zip の入れ忘れではない

Notion AI に復旧させると、最後に「見つからなかったファイル」の一覧が出る。このとき 「zip に入れ忘れたのだろう」と考えて再アップロードすると、時間を捨てることになる

筆者の場合、未解決として報告された 33 件は、Vault のどこにも実体が存在しないファイルだった。移行前から Obsidian 上でリンクが切れていたのである。参照元はいずれも数か月前のデイリーノートやアーカイブで、画像を消したあとノート側の記述が残っていたのだろう。何度アップロードしても直らない。

Vault 全体で見ると、画像の埋め込み 552 箇所のうち 150 件のファイル名が実体なしだった。

移行は、こうした蓄積した壊れを可視化する機会でもある。先に数えておけば、AI の報告を見た瞬間に「これは元から切れていた分だ」と切り分けられる。逆に数えていないと、Notion 側の不具合を疑って調べ回ることになる。

Step2 何を持っていかないか決める

ここは他人のリストを真似ても意味がない。自分の Vault で容量順に並べて、上から判断するしかない。

1
du -sm * .* 2>/dev/null | sort -rn | head -20

判断の観点は3つある。

(a) Obsidian 本体の設定.obsidian/ 配下。Notion では一切使わない。誰でも該当する。

(b) プラグインが生成したキャッシュ・インデックス — ここが曲者で、普段の使い方によって大きさがまるで違う。筆者の場合、ノートをベクトル化して保持するプラグインのデータが 16MB・2,240 ファイルあった。プラグイン本体も、AI 連携系だけで 8MB を超えていた。ノート本文が 9.3MB しかないことを考えると、本体より周辺のほうが重い。

(c) ツールに読ませるために書いたノート — AI 用のカスタムプロンプト、エージェント向けの指示ファイルなど。これらは普通のフォルダに普通の Markdown として置いてあるので、パターンでは落とせない。1つずつ「Notion に要るか」を判断するしかない。

  1. と (b) はドットディレクトリを除外すれば機械的に片付く。厄介なのは (c) で、ツールを Vault に組み込んでいる人ほど、除外の判断が自分固有になり、他人の手順が使えなくなる

そしてもう1つ、原則として:

要らないものは、入れてから消すのではなく、入れる前に落とす。

Notion に入れてから整理すると、ページの移動・削除・重複判定という作業が発生する。数千ページ規模になると、これは Notion AI に投げても相応の時間がかかる。zip を作る段階で除外しておけば、その工程が丸ごと消える。

画像は「参照されているものだけ」でよい

Notion AI はファイル名で照合するので、画像 zip はフォルダ構造を保つ必要がない。逆に言えば、本文から参照されていない画像を渡す意味もない。

Step1 のスクリプトを流用して、必要な画像だけを集める。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import zipfile

need = set()
for path in notes:
text = open(path, encoding='utf-8', errors='ignore').read()
for embed in re.findall(r'!\[\[([^\]|#]+)', text):
name = os.path.basename(embed.strip())
if name.lower().endswith(IMG) and name in files:
need.add(name)

with zipfile.ZipFile('images.zip', 'w', zipfile.ZIP_DEFLATED) as z:
for name in need:
z.write(files[name], name) # 平坦に詰める
print(f"{len(need)} 件")

筆者の場合、Vault 全体の画像は 413 枚・294MB あったが、実際に必要だったのは 78 件・34.2MB だった。9分の1以下である。200MB の制限に対して余裕があり、圧縮も分割も不要になった。

ファイル名で照合する以上、同名ファイルが複数あると取り違える。上のコードは平坦に詰めるので、衝突があれば need を作る段階で気づけるようにしておくとよい。

もし 200MB を超えるようなら、次の順で対処する。分割は最後の手段である。インポート単位ごとに扱いが分かれて面倒になる。

  1. 参照されていない画像を落とす(多くはこれで足りる)
  2. 長辺 2000px 程度に縮小して再圧縮する(筆者の環境で試したところ、サンプル 30 枚が 67.8MB → 9.4MB になった。Notion 上で見るだけなら十分な画質である)
  3. それでも収まらなければ分割する

Step3 テキストをインポートする

設定 → インポート → テキスト & Markdown から zip を読み込ませる。フォルダ構造を保ちたいので、個別ファイルではなくフォルダごと zip 化して渡す。

公式に明示されている制限は次のとおり。

  • 1ファイルあたり 5MB(無料プラン)/ 50MB(有料プラン)
  • zip 1つあたり 5GB
  • テキスト & Markdown は 12時間あたり約 120 ファイル
  • 10,000 ファイルを超えると失敗または部分的な取り込みになりやすい

zip の中身が数千ファイルでも、zip 自体は1つのインポートとして扱われる。

Step4 Notion AI で画像を復旧する

インポート直後に、以下を Notion AI に貼り付ける。データベース名やラベルの語彙は自分の環境に合わせて差し替える。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ObsidianからエクスポートしたzipをNotionにインポートしました。以下の手順で構造化してください。

1. 取り込まれたフォルダ構造を分析し、既存のワークスペース構造(<移行先のデータベース名を列挙>)への
マッピング案を提示してください。重複の可能性があるページも指摘してください。

2. 承認後、以下を一括で実行してください:
- <種類A>は<データベース名>に移動し、タイトルの日付から「開催日」プロパティを設定し、
内容から「ラベル」を付与する
- <種類B>は<データベース名>に移動し、ステータスとラベルを設定する
- 重複ページは中身を比較し、薄い方を削除する
- 移動後に空になったフォルダ殻は削除する

3. 画像が ![[ファイル名]] というテキストとして残っている箇所をスキャンして報告してください。
後ほど画像ファイルを含むzipをアップロードするので、ファイル名で照合して修復してください。

次に画像 zip を添付し、以下を貼る。

1
2
3
アップロードしたzip内の画像ファイルを使って、![[...]] が残っているページを修復してください。
添付ファイルの場所が散在していても、ファイル名で照合してください。
見つからなかったファイルの一覧を最後に報告してください。

このプロンプトが安定して動く理由は3つある。

探索範囲を文字列で固定している。 「画像が抜けているページを探して」ではなく「![[...]] が残っている箇所」と指定する。判断ではなく検索になるので、取りこぼしも誤爆も減る。

照合の規則を明示している。 「ファイル名で照合してください」と書くことで、パスやフォルダ構造に依存しなくなる。だから画像 zip を平坦に詰めてよい。

報告を最後に要求している。 「見つからなかったファイルの一覧」があるので、どこまで終わったかが分かる。

実務上の注意も3つ。

冪等に再実行できる。 大量のページ移動やプロパティ設定は時間がかかり、途中で中断されることがある。すでに処理済みのページには ![[...]] が残っていないので、「続きを実行して」で再開すれば二重処理にならない。この性質があるので、一度に大量に投げても壊れない。

ラベルの語彙は先に決めておく。 AI に任せると表記が揺れる。使う語を列挙して渡したほうが一貫する。

削除はゴミ箱行きなので戻せる。 重複判定を AI に任せるのは、この前提があってのことである。

筆者の場合、この2つのプロンプトで 28 ページに 75 個の画像・PDF が挿入された

Step5 検証 — どこまでやれば終わりか

復旧後に残った ![[...]] を、次の3つに切り分ける。

残った理由 見分け方 対処
画像 zip に入れ忘れた Vault に実体がある 該当分だけ追加アップロードして追補
Vault にも実体が無い Step1 の missing に載っている 直らない。Obsidian 側で元から切れていた
画像化できない形式 .base.zip などの参照 PDF はファイル添付として挿入できる。それ以外は諦める

Step1 を実行していれば、2つめは事前に判明している。AI の報告と突き合わせるだけで切り分けが終わる。

筆者の場合、未解決 36 件の内訳は「Vault に実体が無い 33 件」と「.base / .zip 形式 3 件」で、追加アップロードで直るものは1件も無かった。事前に数えていなければ、ここで zip を作り直して再挑戦していたはずである。

なお、Obsidian の .base(Bases)ファイルへの参照は Notion 側に持っていく手段がない。Vault 全体では 21 箇所あった。移行の対象外と割り切ることになる。

まとめ

  • Notion のインポートは構造と本文を再現するが、Obsidian 独自の ![[...]] は生テキストとして残る。画像ファイル自体も上がらない
  • テキストは 5GB まで通る。容量が問題になるのは Notion AI に画像を渡す経路のほうで、こちらは 200MB 程度で弾かれる
  • インポート前に、埋め込み箇所・集中しているファイル・実体の無い参照の3つを数える。作業量の見積もりと、後の切り分けが両方ここで決まる
  • 画像は本文から参照されているものだけを平坦な zip に詰めればよい。筆者の場合 294MB が 34.2MB になった
  • Notion AI へのプロンプトは「![[...]] を探す」「ファイル名で照合する」「見つからなかったものを報告する」の3点を明示すると安定する
  • 復旧後に残った参照は「入れ忘れ」「元から切れていた」「形式的に無理」に切り分ける。Step1 を通していれば突き合わせるだけで済む

採らなかった方法

インポート前に ![[image.png]]![image.png](attachments/image.png) へ一括置換する。 標準の Markdown 記法にしてしまえばインポータが解決してくれる可能性があり、うまくいけば Notion AI の工程が丸ごと不要になる。ただし相対パスの解決規則が期待どおりかは未検証で、今回は試していない。Vault の一部で小さく試してから全体に適用するのが安全だろう。

zip を分割して複数回インポートする。 容量の問題はテキスト側では起きないので、必要になる場面は限られる。分割するとインポート単位ごとに別のトップレベルページができ、階層の組み直しが発生する。また zip をまたいでノートと添付が分かれると照合が壊れるので、切る位置にも気を配ることになる。

参考

共有