Closed7
NotionからObsidianへ移行する

NotionからObsidianへ
notionからobsidianへデータの移行作業メモ
移行する動機
- AIとメモを連携させたかったけど、notion AIだと満足できなかった
- ツェッテルカステンを参考にメモを管理することは前からやっていて、Obsidianに興味はあった
- Obsidianでプレーンテキストで管理して、AIと連携してつかうことを考えた
- ...と考えて移行作業をしてたら Notion MCPが正式リリースされたので、そっちでもよかったかもしれない
- でも移行作業しちゃったので、このままObsidianを使ってみる

Notionから移行する懸念点
環境
- Notion内の記事数は3000ぐらい
- Mac, Androidで利用したい
調査
懸念点
- 検索性
- 曖昧検索ができない
- 部分一致、完全一致ができる
- 画像の管理
- webクリップ
- Obsidian Web Clipper
-
Obsidian Web Clipper × AI でWebページを自動要約&ストックしてみた | DevelopersIO
- AIで要約しながらストックもできるらしい
- 端末間同期
- 無料でいくつかある
- 公式の有料プランなら間違いなさそう

Notion -> Obsidian 移行ツール
調査
- Noiton → Obsidian への移行
- データのインポート - Obsidian 日本語ヘルプ - Obsidian Publish
- Import from Notion - Share & showcase - Obsidian Forum
- connertennery/Notion-to-Obsidian-Converter: Converts exported Notion notes to work with Obsidian.
- Notion 2 Obsidian Migration Instructions - Share & showcase - Obsidian Forum
- visualcurrent/Notion-2-Obsidan: Conversion routines to convert all Notion .md exports to full Obsidian compatibility
公式のツールを使う
Import from Notion - Obsidian Help
エラー対応
- obsidianでcreated_atがinvalid
- Notionで日本語の設定のまま日付のメタデータを出力したら、invalidになった
- Notionの設定を英語にして出力したら正常に表示された
- 何個か取り込みエラーになった記事があった
- エラーメッセージを全文見る方法がわからず、ページ名が見切れていて不明
- 移行できなかったページは一旦無視する

端末間同期
調査
- Obsidian、同期方法の比較|penchi
- Obsidianの同期について - Mac系とAndroid|teatown
- スマホのObsidianをGitで同期(2024.11)
- ObsidianをGitでAndroidと同期する(ターミナルエミュレーターなし)
- Obsidianを導入するならSyncをおすすめする話
RemotlySaveを使ってみる
mac, android間で無料で同期しようと思うとRemotlySaveが良さそうなので使ってみる
dropboxで同期してみる
- 絵文字制限と字数制限があるっぽい
- レート制限もきつそうでアップロードが進まない
onedriveに変えてみる
- dropboxよりはレート制限が緩そう
- こちらも絵文字制限はありそう
デバッグ方法
- 開発者ツールを表示させるとエラーの詳細が見える
- 他にいい方法ある?
- 429エラーはレート制限なので、時間置いて再度実行
- 5分ごとに同期設定にして放置する
- 400エラーは対象ページに不正ななにかが含まれている
- タイトルに絵文字などが入っている
- 拡張子がないファイルもエラーになった
- obsidian上で表示されてない同フォルダ内にあるファイルも同期されるので注意
タイトルの絵文字を削除するpythonスクリプト(生成AI製)
# ファイル名のお絵文字を削除する
import emoji
import glob
import os
# 絵文字リスト
emojis = emoji.UNICODE_EMOJI
# 指定フォルダから、全てのアイテムをリストアップ
path = './path/to/dir/'
allItems = glob.glob(path+'**', recursive=True)
allItems.sort()
# 絵文字が含まれていれば、それを削除する関数
def renameItems(items):
for i in items:
renamed = ''
is_emoji = False
for j in i:
if j not in emojis['en']:
renamed = renamed + j
else:
is_emoji=True
if not is_emoji:
continue
try:
print(i)
print(renamed)
print("")
os.rename(i, renamed)
except FileNotFoundError:
return 1
return 0
# 関数の実行
x = renameItems(allItems)
if x == 1:
print('もう一度実行してください')
else:
print('完了')
長いファイル名を切り落とすpythonスクリプト(生成AI製)
import os
# Obsidianのノートファイル名を一定の長さで切り落とす関数
def truncate_obsidian_file_names(vault_path, max_length):
# 指定されたディレクトリ内のすべてのファイルを取得
for root, dirs, files in os.walk(vault_path):
for file in files:
# print(file)
# Markdownファイルのみを処理
if file.endswith('.md'):
file_path = os.path.join(root, file)
# ファイル名と拡張子を分離
name, ext = os.path.splitext(file)
# ファイル名が指定された長さを超える場合
if len(name) > max_length:
# 新しいファイル名を作成(拡張子は保持)
truncated_name = name[:max_length] + ext
new_file_path = os.path.join(root, truncated_name)
# ファイル名変更
os.rename(file_path, new_file_path)
print(f"名前変更: {file} → {truncated_name}")
# 使用例
# Obsidianのvaultパスを指定
vault_path = "./path/to/dir/"
# 最大ファイル名長を指定(拡張子を除く)
max_length = 100
# 関数を実行
truncate_obsidian_file_names(vault_path, max_length)

Obsidianを整理していく
プロパティを整理する
Notionで使っていたプロパティを整理する
- 作成日と更新日をプロパティとして記録したい
不要な画像の整理
違うノートの同じ画像が重複して保存されるので、重複を排除したい
ハッシュで同一画像を判別し、ファイル名が短い方を採用し、片方の画像へのリンクをファイル名が短い方にリネームする(AIで出したコード、動作ちょっとおかしいかも)
# 重複した画像を削除し、リンク元を書き換える
import os
import re
from PIL import Image
import numpy as np
import shutil
# Obsidianのvaultのルートディレクトリを指定
VAULT_ROOT = "./"
# 画像が保存されているディレクトリ(通常は'attachments'や'images'など)
IMAGE_DIR = os.path.join(VAULT_ROOT, "Attachments")
# マークダウンファイルが保存されているディレクトリ
MARKDOWN_DIR = VAULT_ROOT
def calculate_image_hash(file_path):
"""画像のハッシュを計算する関数"""
try:
img = Image.open(file_path)
# 画像をNumPy配列に変換
img_array = np.array(img)
return img_array
except Exception as e:
print(f"画像の読み込みエラー: {file_path} - {e}")
return None
def find_duplicate_images():
"""重複画像を見つける関数"""
image_dict = {} # ハッシュ値をキー、ファイルパスのリストを値とする辞書
duplicates = [] # 重複画像のペアを格納するリスト
# 画像ファイルを検索
for root, _, files in os.walk(IMAGE_DIR):
for file in files:
if file.lower().endswith(('.jpg', '.jpeg', '.png', '.gif')):
file_path = os.path.join(root, file)
img_array = calculate_image_hash(file_path)
if img_array is not None:
# NumPy配列をバイト列に変換してハッシュ化
img_hash = hash(img_array.tobytes())
if img_hash in image_dict:
image_dict[img_hash].append(file_path)
else:
image_dict[img_hash] = [file_path]
# 重複画像を見つける
for img_hash, file_paths in image_dict.items():
if len(file_paths) > 1:
# ファイル名の長さでソート
file_paths.sort(key=lambda x: len(os.path.basename(x)))
# 最初の要素(ファイル名が最も短いもの)を保持し、残りを重複として扱う
keep_file = file_paths[0]
for duplicate in file_paths[1:]:
duplicates.append((keep_file, duplicate))
return duplicates
def update_markdown_links(keep_file, duplicate_file):
"""マークダウンファイル内のリンクを更新する関数"""
keep_filename = os.path.basename(keep_file)
duplicate_filename = os.path.basename(duplicate_file)
# マークダウンファイルを検索
for root, _, files in os.walk(MARKDOWN_DIR):
for file in files:
if file.lower().endswith('.md'):
file_path = os.path.join(root, file)
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# 重複画像へのリンクを検索して置換
# Markdownのリンクパターン: 
pattern = r'!\[.*?\]\(.*?' + re.escape(duplicate_filename) + r'(?:\s+".*?")?\)'
replacement = lambda m: m.group(0).replace(duplicate_filename, keep_filename)
new_content = re.sub(pattern, replacement, content)
# Obsidian形式のリンクパターン: ![[path/to/image]]
pattern = r'!\[\[.*?' + re.escape(duplicate_filename) + r'.*?\]\]'
replacement = lambda m: m.group(0).replace(duplicate_filename, keep_filename)
new_content = re.sub(pattern, replacement, new_content)
# 変更があれば保存
if new_content != content:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(new_content)
print(f"更新されたファイル: {file_path}")
except Exception as e:
print(f"ファイル処理エラー: {file_path} - {e}")
def main():
"""メイン関数"""
print("重複画像を検索中...")
duplicates = find_duplicate_images()
if not duplicates:
print("重複画像は見つかりませんでした。")
return
print(f"{len(duplicates)}個の重複画像が見つかりました。")
for keep_file, duplicate_file in duplicates:
print(f"保持: {keep_file}")
print(f"削除: {duplicate_file}")
# マークダウンファイル内のリンクを更新
update_markdown_links(keep_file, duplicate_file)
# 重複画像を削除
try:
os.remove(duplicate_file)
print(f"削除完了: {duplicate_file}")
except Exception as e:
print(f"削除エラー: {duplicate_file} - {e}")
print("処理が完了しました。")
if __name__ == "__main__":
main()
壊れたリンクの整理
壊れたリンクを探してくれるプラグインを使う
空白が大文字と小文字で違ってインポートされていて、リンクが切れていたものを修正する(AIで出したコード)
import re
import os
import glob
def extract_broken_links(broken_links_file):
"""ブロークンリンクファイルからリンクを抽出する"""
broken_links = []
with open(broken_links_file, 'r', encoding='utf-8') as f:
content = f.read()
# リンクパターンを抽出
# "in [[ファイル名]]" の形式を探す
pattern = r'- \[\[(.*?)\]\] in \[\[(.*?)\]\]'
matches = re.findall(pattern, content)
for broken_link, parent_file in matches:
broken_links.append((broken_link, parent_file))
return broken_links
def find_correct_filenames(vault_path):
"""Obsidianボールト内の全ファイル名を取得"""
all_files = []
# マークダウンファイルを再帰的に検索
for filename in glob.glob(f"{vault_path}/**/*.md", recursive=True):
# ファイル名のみを抽出(パスなし)
basename = os.path.basename(filename)
# 拡張子を除去
name_without_ext = os.path.splitext(basename)[0]
all_files.append(name_without_ext)
return all_files
def find_best_match(broken_link, all_filenames):
"""ブロークンリンクに最も近いファイル名を見つける"""
# 完全一致があれば返す
if broken_link in all_filenames:
return broken_link
# スペースの違いを無視して比較
for filename in all_filenames:
if re.sub(r'\s+', ' ', broken_link) == re.sub(r'\s+', ' ', filename):
return filename
# スペースの数が違うだけの場合
if broken_link.replace(' ', '') == filename.replace(' ', ''):
return filename
return None
def fix_broken_links(vault_path, broken_links_file, output_file):
"""ブロークンリンクを修正する"""
broken_links = extract_broken_links(broken_links_file)
all_filenames = find_correct_filenames(vault_path)
fixes = []
for broken_link, parent_file in broken_links:
correct_link = find_best_match(broken_link, all_filenames)
if correct_link and correct_link != broken_link:
fixes.append({
'parent_file': parent_file,
'broken_link': broken_link,
'correct_link': correct_link
})
# 修正内容をファイルに書き出す
with open(output_file, 'w', encoding='utf-8') as f:
f.write("# Obsidian リンク修正レポート\n\n")
if not fixes:
f.write("修正が必要なリンクは見つかりませんでした。\n")
else:
f.write("## 修正が必要なリンク\n\n")
for fix in fixes:
f.write(f"### ファイル: {fix['parent_file']}\n")
f.write(f"- 誤: [[{fix['broken_link']}]]\n")
f.write(f"- 正: [[{fix['correct_link']}]]\n\n")
return fixes
def apply_fixes(vault_path, fixes):
"""修正を実際のファイルに適用する"""
fixed_count = 0
for fix in fixes:
parent_file_pattern = f"{vault_path}/**/*{fix['parent_file']}*.md"
parent_files = glob.glob(parent_file_pattern, recursive=True)
for file_path in parent_files:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# リンクを修正
pattern = r'\[\[' + re.escape(fix['broken_link']) + r'\]\]'
replacement = f"[[{fix['correct_link']}]]"
new_content = re.sub(pattern, replacement, content)
if new_content != content:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(new_content)
fixed_count += 1
return fixed_count
def main():
# 設定
vault_path = "./" # Obsidianボールトのパスを指定
broken_links_file = "./broken links output.md" # ブロークンリンクファイル
output_file = "link_fixes_report.md" # 修正レポートファイル
# 修正内容を生成
fixes = fix_broken_links(vault_path, broken_links_file, output_file)
print(f"{len(fixes)}個の修正候補が見つかりました。詳細は{output_file}を確認してください。")
# 修正を適用するか確認
apply_fix = input("修正を適用しますか?(y/n): ")
if apply_fix.lower() == 'y':
fixed_count = apply_fixes(vault_path, fixes)
print(f"{fixed_count}個のファイルが修正されました。")
else:
print("修正は適用されませんでした。")
if __name__ == "__main__":
main()
Notionのギャラリービューを再現する
読書メモは書影が見えるページが欲しい
dataviewやCSSを駆使すれば作れるっぽいがもっと手軽に作りたい
プラグインを使う

Obsidian運用方針
- タグは使わずMOCを作る
- notionでタグデータベースをつかっていた
- Notionでタグライブラリーを作るとめちゃくちゃ捗った|こにゃ
- 各ページから関連するタグに関連を紐づけるイメージ
- obsidianのタグに置き換えたがなんかイメージと違う
-
Obsidian で MOC を作るなら逆引きインデックス方式がおすすめ|MaybeFix
- この記事をみてやりたかったのはMOCだと思った
- 興味のある領域ごとにMOCを作り、ページと紐づける
- notionでタグデータベースをつかっていた

Obsidianに移行した感想
- レスポンスが早く、きびきび動いていい感じ
- ノートを一括で編集するときに独自でスクリプト書いて修正できるのは自由度高くてよい
- Excalidrawが使えるのをしらなかったが、見た目が良くて好き
このスクラップは5ヶ月前にクローズされました