見出し画像

NotionからEsaへの移行

ドキュメント共有ツールをNotionからEsaに移行しました。EsaにはNotionのデータを直接インポートする機能はないため、APIを使用する必要があります。この記事では、NotionからエクスポートしたデータをPythonを使ってEsaにインポートする方法を紹介します。

なお、うちの環境では画像などの添付ファイルがほとんどなかったので、ページデータのみをインポートする方法に焦点を当てています。


▍Notionでのエクスポート

Notionでは、データをワークスペース全体または特定のページごとにエクスポートできます。

エクスポートを行う際には、[サブページを含める] および [サブページのフォルダーを作成] のオプションを有効にしてください。ワークスペース全体をエクスポートする場合、[サブページを含める] オプションは表示されません。
また、本記事ではページのみを対象にしているので、対象コンテンツを[ファイルや画像以外] としています。

Notionのエクスポート画面(ページの場合)

エクスポートの詳細な手順については、以下の公式ヘルプを参照ください。

エクスポートされたデータはzip形式でダウンロードされます。このファイルを展開(解凍)すると、「Export」という名前を含むフォルダが生成されます。

▍NotionのページとEsaの記事の対応

手順の前に、Notionのページ構造とEsaの記事構造の対応について説明します。

■ Notionのページ

Notionで以下のようなページ構造があるとします。

my page
  ├─ child1
  │   └─ grandchild1-1
  └─ child2

これを「my page」でエクスポートした場合、以下のようなファイル構造が作られます。

UUID1_Export-UUID2
├─ my page UUID3.md
└─ my page UUID3
    ├─ child1 UUID4.md
    ├─ child1 UUID4
    │   └─ grandchild1-1 UUID6.md
    └─ child2 UUID5.md
  • UUIDはエクスポート時に自動で付与されます。

  • .md はマークダウン形式のファイルを示します。これがNotionのページに該当します。

  • 親ページ内に子ページが含まれる場合、親ページと同名のフォルダが作成されます。

■ Esaの記事

Notionのページ構造をそのままEsaに移行する場合、以下の構造が考えられます。

my page
my page/child1
my page/child1/grandchild1-1
my page/child2

しかし、Esaでは最上位の「my page」は (no category) に区分されてしまうため、対処が求められます。

うちでは、以下のようにルートページをREADMEとして扱うことにしました。

my page/README 
my page/child1
my page/child1/grandchild1-1
my page/child2

この対応方法は一例ですので、異なる方法を希望する場合は後述するコードを適宜変更してください。

READMEの扱いなど、Esaの記事構造については、以下の公式ヘルプを参照ください。

▍Esaへのインポート

■ 環境準備

コードでは pathlib モジュールを使用しているため、Python 3.4 以降の環境が必要です(開発環境は 3.11.6)。また、HTTPリクエストの送信には requests ライブラリを使用しているので、事前にインストールが必要です。

ファイルの配置場所は任意ですが、PythonファイルとNotionのエクスポートデータを同じフォルダに配置するとファイル指定がスムーズになります。

■ コードの内容

以下のコードは、指定されたNotionのエクスポートフォルダからデータを読み込み、それをEsaにインポートするためのものです。なお、エラー処理は実装していないのでご容赦ください。

import requests
from pathlib import Path

def post_to_esa(api_url, api_token, name, body_md, category, wip=False, user='esa_bot'):
    """
    Esa APIで新しい投稿を作成するためのPOSTリクエストを送信する。

    Args:
        api_url (str): Esa APIのURL。
        api_token (str): Esa APIトークン。
        name (str): 投稿の名前。
        body_md (str): 投稿のMarkdownコンテンツ。
        category (str): 投稿のカテゴリ。
        wip (bool): 投稿が作成中(WIP)かどうか。デフォルトはFalse。
        user (str): 投稿者のユーザー名。デフォルトは'esa_bot'。
    """
    headers = {
        'Authorization': f'Bearer {api_token}',
        'Content-Type': 'application/json'
    }

    post_data = {
        'post': {
            'name': name,
            'body_md': body_md,
            'category': category,
            'wip': wip,
            'user': user,
        }
    }
    
    response = requests.post(api_url, headers=headers, json=post_data)
    if response.status_code == 201:
        print(f'投稿成功: {category}/{name}')
    else:
        print(f'投稿失敗: {category}/{name}')
        print(response.text)

def extract_name(item):
    """
    ファイルまたはディレクトリの名前からUUIDを除外して名前を抽出する。

    Args:
        item (str): ファイルまたはディレクトリの名前。

    Returns:
        str: UUIDを除外した名前。
    """
    return item.rsplit(' ', 1)[0]

def process_root_directory(root_directory, api_url, api_token):
    """
    ルートディレクトリを処理し、各Markdownファイルをカテゴリとしてサブディレクトリを処理する。

    Args:
        root_directory (str): ルートディレクトリのパス。
        api_url (str): Esa APIのURL。
        api_token (str): Esa APIトークン。
    """
    root_path = Path(root_directory).resolve()
    
    # ルートディレクトリ直下のMarkdownファイルをカテゴリとして処理
    for item in root_path.iterdir():
        if item.is_file() and item.suffix == '.md':
            category_name = extract_name(item.stem)
            process_file(item, category_name, api_url, api_token, name_override='README')
    
    # 対応するサブディレクトリの処理を開始
    for item in root_path.iterdir():
        if item.is_dir():
            category_name = extract_name(item.stem)
            process_directory(item, category_name, api_url, api_token)

def process_file(file_path, category, api_url, api_token, name_override=None):
    """
    単一のMarkdownファイルを処理してEsaに投稿する。

    Args:
        file_path (Path): Markdownファイルのパス。
        category (str): 投稿のカテゴリ。
        api_url (str): Esa APIのURL。
        api_token (str): Esa APIトークン。
        name_override (str, optional): 投稿に使用する名前。デフォルトはNone。
    """
    with open(file_path, 'r', encoding='utf-8') as file:
        body_md = file.read()
        name = name_override if name_override else extract_name(file_path.stem)
        post_to_esa(api_url, api_token, name, body_md, category)

def process_directory(directory_path, current_category, api_url, api_token):
    """
    Markdownファイルとサブディレクトリを再帰的に処理する。

    Args:
        directory_path (Path): ディレクトリのパス。
        current_category (str): 現在処理しているカテゴリ。
        api_url (str): Esa APIのURL。
        api_token (str): Esa APIトークン。
    """
    for item in Path(directory_path).iterdir():
        if item.is_dir():
            new_category = f'{current_category}/{extract_name(item.stem)}'
            process_directory(item, new_category, api_url, api_token)
        elif item.is_file() and item.suffix == '.md':
            process_file(item, current_category, api_url, api_token)

if __name__ == '__main__':
    # EsaのAPIトークンとチーム名を設定
    ESA_API_TOKEN = 'your_esa_api_token'
    ESA_TEAM_NAME = 'your_esa_team_name'
    ESA_API_URL = f'https://api.esa.io/v1/teams/{ESA_TEAM_NAME}/posts'
    
    # インポート対象のデータのルートフォルダを指定
    root_directory = Path('your_notion_export_folder').resolve()
    
    # ルートディレクトリの処理を開始
    process_root_directory(root_directory, ESA_API_URL, ESA_API_TOKEN)

指定するパラメーターは以下です。

  • ESA_API_TOKEN:EsaのAPIのアクセストークン

  • ESA_TEAM_NAME:Esaのチーム名

  • root_directory:インポート対象のデータのルートフォルダ

既定では、記事の WIP は False(公開)で、投稿者は esa_bot と指定しています。

APIのパラメーターやリクエスト数の制限、制限の緩和については以下の公式ドキュメントを参照ください。特に、大量のページを一度にインポートする場合には必ずご覧ください

▍インポート後の対応

インポートした記事に対しては、いくつかの注意点があります。

・別ページへのリンク

Notion内の別ページへのリンクは、インポート後も元のリンク先を指しているため、正しく機能しません。これらのリンクは削除するか、またはEsaの適切な記事を指すように修正する必要があります。

・Notion特有の表現

Notionで利用できるトグル、コールアウト、列などの特殊な表現は、Esaでは再現できません。これらはEsaのマークダウン形式に合わせて修正または削除する必要があります。

特に列を利用して情報を構造化していた場合は注意が必要です。例えば、Notionで以下のように項目とその値を列で表示していたとします。

住所    東京都文京区...
電話    050-3612-...

マークダウンでは列の概念がないため、Esaでは以下のように項目が先に並び、次に値が続く形になります。

住所
電話
東京都文京区...
050-3612-...

元の構造が失われてしまうため、情報の並びを正しく修正する必要があります。

ツール特有の機能や表現は魅力的ですが、他のプラットフォームへの移行時には障壁となることもあります。このようなバランスを取ることは常に課題ですね。

▍おわりに

NotionのページをEsaに移行する方法を紹介しました。インポート方法のニーズはそれぞれ異なるかと思いますが、この内容が参考になれば幸いに思います。

ご精読いただき、ありがとうございました!


私たちのデジタル技術活用の記事は以下のマガジンにあります。ぜひご覧ください!

この記事が気に入ったらサポートをしてみませんか?