Notion APIで大量のページをリレーション付きで一括投稿する

Notion APIで大量のページをリレーション付きで一括投稿する

最新の更新日
Jan 1, 2025 2:20 PM
記事作成日
Sep 11, 2024 11:28 AM
Category
Notion
Target Keywords

vol.

Podcast

Super.soによるNotionサイト構築をご支援させていただく際、「リレーション付きの大量ページでも問題なくNotionが動作するか」が懸念事項としてあがりました。

そこで、Notionのパフォーマンステストのため、約20,000件のページをリレーション付きの状態で入稿し、問題なく動作するかどうかの検証・テストを実施。結果としては問題なく動作することが確認できました。

本記事では、上記パフォーマンステストを実施した際、Notion API経由で大量ページをリレーション付きで投稿しましたので、その方法について詳細をご共有します。

Notion APIで特定のデータベースにページを一括投稿(リレーション付き)

image

どのようにして20,000件ものページをデータベースに一括投稿したか、について方法をご共有いたします。

Notion APIを利用し、Pythonで容易にAPIを扱えるようにしてくれる公式のSDknotion-sdk-pynotion-sdk-py にてサンプルデータを大量入稿させました。

notion-sdk-pyの利用にあたって、 【保存版】Python Notion 操作 (notion-sdk-py編) #GitHub - Qiita の記事がよくまとまっており、大変助けてもらいました🙏

Notion APIを用いると、通常のNotion操作では難しい大量のデータに対する操作、編集、整形が可能となります。

たとえば、NotionデータベースAPI操作: 主キー(タイトル列)を変更・入れ替えてデータベースを複製する の記事では通常Notionの仕様上不可となっている主キーの入れ替えをNotion APIからの操作によって実現しています。

また、追加とは反対にデータベース内のページを一括削除させることもできます。 Notionデータベースのレコード行を一括削除する(API, Python)

Notion APIでは複数のページを一括アップロード(POST)することは許可されていない

Notion APIでは複数のページを一括アップロード(POST)することはできません。

According to Notion support this is not possible. The feature has been requested but is not available on Oct. 20th 2022.

python - Updating multiple entries in a notion database with API - Stack Overflow

したがって厳密には一括投稿ではありませんが、プログラム実施単位でみると一括、という形となります。

そのため、for, while文などの繰り返し制御によってAPIによる1レコード投稿を繰り返して実施していくこととなります。

プログラム内では1POSTにつき3秒のストップをかけながら実施しているため、1ページ追加につきおよそ3秒+αかかります。

APIを利用する場合、インテグレーションを作成したうえ、該当ページおよびデータベースに対してAPI許可を与える必要があります。

参考: Notion APIの制限・注意点(メモ)

リレーション付きでページ作成する場合のプログラム流れ

リレーション付きでページを追加する場合、以下のような流れが必要となります。

  1. 新規ページを追加する先のデータベースを特定(データベースIDを取得)
  2. 新規ページに紐づける、リレーション先アイテムのIDを取得
  3. 新規ページのプロパティを定義(リレーション先IDや他カラムの情報を追加)
  4. 特定したデータベースへ新規ページを作成

0. notion-sdk-pyを使うための設定・準備

まずはライブラリを追加します。

# Ryeの場合
$ rye add notion-client

# pipの場合
$ pip install notion-client

Notionクライアントを作成します。

import os
from notion_client import Client

client = Client(auth=os.environ["NOTION_TOKEN"])

1. あらかじめ関連するデータベースIDを取得しておく

データベースIDは一度作成されてしまえば固定であり、毎回取得しておく必要はないため、あらかじめ取得しておいて定数として定義しておきます。

def get_db_names_and_ids_from_query(client, query: str) -> list:
    """
    Search for databases in Notion using a query string and 
    return a list of dictionaries containing database names and IDs.

    Args:
        client: The Notion client object.
        query (str): The search query string.

    Returns:
        list: A list of dictionaries, each containing 'db_title' and 'db_id' for found databases.
        If no databases are found, returns an empty list.
    """
    results = client.search(query=query).get("results")

    # Dict in List for db_info. [{db_title: db_id}, ...]
    db_names_ids = []
    db_count = 0

    for result in results:
        # Check if the result is a database
        if result["object"] == "database":
            db_count += 1
            db_info = {}
            db_info["db_title"] = result["title"][0]["plain_text"]
            db_info["db_id"] = result["id"]
            db_names_ids.append(db_info)
    
    if db_count:
        print(f"Found {db_count} databases.")
        return db_names_ids
    else:
        print(f"No databases found with query: {query}")
        return []


# Retrieve Parent DB
parent_db_name = "元DB"
parent_db_results = get_db_names_and_ids_from_query(client, parent_db_name)
PARENT_DB_ID = parent_db_results[0]["db_id"]

2. リレーション先アイテムのIDを取得

元DBにアイテムをアップロードする際、リレーションとして紐づけるアイテムを特定したうえでアップロードする必要があります。

リレーション先アイテムとは、 Notionパフォーマンステスト でいうところの「リレーション先DB_A」と「リレーション先DB_B」(子DB)のなかのアイテムとなります。

まずは子DB自体のIDを取得

これら子DBのなかのアイテムのIDを一覧で取得するために、まずは子DB自体のIDを取得します。

# Retrieve child DBs
child_db_results = get_db_names_and_ids_from_query(client, "リレーション先")

# Store
CHILD_DB_A_ID = db_results[0]["db_id"]
CHILD_DG_B_ID = db_results[1]["db_id"]

子DBのIDにもとづき、子DB内のページ情報をすべて取得

子DBのIDを取得できたら、IDをもとにDB内のページ情報をすべて取得します。

取得したページ情報一覧から、ページのIDのみをそれぞれ抜き出して辞書inリストの形で保持します。

def get_all_pages_info_from_db(client, database_id: str) -> list:
    """
    Retrieves all information from a specified Notion database.

    Args:
        client: The Notion client object.
        database_id (str): The ID of the Notion database to query.

    Returns:
        list: A list of dictionaries, each containing all information for a page in the database.
    """
    response = client.databases.query(
        **{
            "database_id": database_id,
        }
    )

    return response["results"]


# 子DB_AのIDをもとに、DB内のページ情報をすべて取得
# Get all pages info from child_db_A
pages_info_A = get_all_pages_info_from_db(client, CHILD_DB_A_ID)

# 取得したページ情報一覧から、ページのIDのみをそれぞれ抜き出して辞書inリストの形で保持
# child_db_A_pages: {item_1: page_id, item_2: page_id, ...}
child_db_A_pages = {
    page["properties"]["Name"]["title"][0]["text"]["content"]: page["id"]
    for page in pages_info_A
}

# 子DB_Bについても繰り返す
# Get all pages info from child_db_B
pages_info_B = get_all_pages_info_from_db(client, CHILD_DB_B_ID)

# child_db_B_pages: {item_A: page_id, item_B: page_id, ...}
child_db_B_pages = {
    page["properties"]["Name"]["title"][0]["text"]["content"]: page["id"]
    for page in pages_info_B
}

マルチセレクト項目の定義。指定したカラーでリストを作成

今回はランダムで6つの項目から選択されるようにしたいため、まずは各セレクト項目ごとに定義します。

下記のように各セレクト項目の定義を行うに際して、別途セレクト項目ごとのIDを取得しておく必要があります。(下記各セレクト項目の’id’内容はダミー)

### Multi Select Property

# Define the single tag property by a each color
gray = {
    'color': 'gray',
    'description': None,
    'id': 'gray_id',
    'name': 'gray'
}

orange = {
    'color': 'orange',
    'description': None,
    'id': 'orange_id',
    'name': 'orange'
}

yellow = {
    'color': 'yellow',
    'description': None,
    'id': 'yellow_id',
    'name': 'yellow'
}

red = {
    'color': 'red',
    'description': None,
    'id': 'red_id',
    'name': 'red'
}

green = {
    'color': 'green',
    'description': None,
    'id': 'green_id',
    'name': 'green'
}

blue = {
    'color': 'blue',
    'description': None,
    'id': 'blue_id',
    'name': 'blue'
}

セレクト項目のパターンを生成するための関数を以下のように作成しました。

# Get the defined colors properties based on the provided color names.
def get_multi_select_properties_by_color_names(*color_names) -> list:
    """
    Get the defined colors based on the provided color names.
    If no color names are provided, return all defined colors.
    """
    color_dict = {
        'gray': gray,
        'orange': orange,
        'yellow': yellow,
        'red': red,
        'green': green,
        'blue': blue
    }

    # If no color names are provided, return all colors
    if not color_names:
        return list(color_dict.values())
    
    # Return the colors that match the provided color names
    result = []
    for name in color_names:
        if name in color_dict:
            result.append(color_dict[name])
        # If the color name is not found, print a warning message
        else:
            print(f"Warning: '{name}' is not a valid color name and will be ignored.")
    
    return result

関数を利用して適当なパターンでカラーリストを作成します。

## Define the Multi Select property value for the parent database
# Prepare some patterns
multi_select_properties_all = get_multi_select_properties_by_color_names()

multi_select_properties_pattern_1 = get_multi_select_properties_by_color_names('red', 'blue', 'green')

multi_select_properties_pattern_2 = get_multi_select_properties_by_color_names('blue', 'yellow', 'orange', 'gray')

multi_select_properties_pattern_3 = get_multi_select_properties_by_color_names('red', 'green', 'yellow')

3. 新規ページのプロパティを定義(リレーション先IDや他カラムの情報を追加)

新規追加するページのプロパティを定義します。どのようなプロパティを追加すればよいか?については Page properties に記述があります。

リッチテキストのプロパティについてだけは、 Rich text のページに詳細情報がありました。

下記page_propertyのうち、’リレーション先DB’のIDはリレーション先DBのIDではなく、元DBにおけるリレーションカラムのID(カラムID)です。

def create_page_property_2post_parent_db(post_title: str, post_text: str, multi_select_properties: list, child_page_A_ids: list, child_page_B_ids: list) -> dict:
    """
    Create a page property for the parent database.
    """
    page_property = {
        'Name': {
            'id': 'title',
            'type': 'title',
            "title": [{ "type": "text", "text": { "content": post_title } }]
        },
        'テキストフィールド': {
            'id': 'text_field_id',
            'type': 'rich_text',
            'rich_text': [{
                "type": "text",
                "text": {
                    "content": post_text,
                    "link": None
                }
            }],
        },
        'テスト複数セレクト': {
           'multi_select': multi_select_properties
        },
        'リレーション先DB_A': {
            # 元DBのリレーション列ID
            'id': 'relation_column_a_id',
            # リレーション先DBのページIDをリスト形式で追加
            'relation': [{"id": child_id} for child_id in child_page_A_ids],
            'type': 'relation'
        },
        'リレーション先DB_B': {
            'id': 'relation_column_b_id',
            'relation': [{"id": child_id} for child_id in child_page_B_ids],
            'type': 'relation'
        }
    }
    
    return page_property

# Example usage:
# page_prop = create_page_property("Post Title", "Post Text", [{"name": "Tag1"}, {"name": "Tag2"}], ["id1", "id2"], ["id3", "id4"])

4.特定したデータベースへ新規ページを作成(プログラム実行)

作成してきた内容をもとに、元DBへ新規ページを作成します。

途中、マルチセレクトを4パターンでランダム選択し、また新規ページに紐づけるリレーション先アイテムもランダムに1〜5個が選択されたうえで投稿されるようにしてあります。

## Define other properties for the parent database
# Create multiple page properties for the parent database
num_pages = 20000  # Number of pages to create

for i in range(num_pages):
    # Randomly select multi_select_properties
    multi_select_properties = random.choice([
        multi_select_properties_all,
        multi_select_properties_pattern_1,
        multi_select_properties_pattern_2,
        multi_select_properties_pattern_3
    ])

    # Randomly select child_page_A_ids (1 to 5 items)
    child_A_ids = random.sample(list(child_db_A_pages.values()), random.randint(1, 5))

    # Randomly select child_page_B_ids (1 to 5 items)
    child_B_ids = random.sample(list(child_db_B_pages.values()), random.randint(1, 5))

    page_property = create_page_property_2post_parent_db(
        post_title=f"Performance Test {i+1}",
        post_text=f"This is performance test {i+1} for the bulk upload.",
        multi_select_properties=multi_select_properties,
        child_page_A_ids=child_A_ids,
        child_page_B_ids=child_B_ids
    )

    # Post a single page to the parent database with retry logic
    for attempt in range(5):
        try:
            response = client.pages.create(
                parent={"database_id": PARENT_DB_ID},
                properties=page_property
            )
            break  # If successful, break out of the retry loop
        except Exception as e:
            if attempt < 4:  # If it's not the last attempt
                print(f"Error occurred: {e}. Retrying in 60 seconds...")
                time.sleep(60)
            else:
                error_message = f"Failed to create page after 5 attempts. Error: {e}"
                raise RequestException(error_message) from e

    # Print the result
    print(f"Successfully created page {i+1}")

    # Insert a pause after each page creation
    time.sleep(3)

# Output the result
print(f"Successfully created {num_pages} pages!")

以上で、Notion API経由でリレーション付きで一括ページ投稿をすることが可能です!

ソースコード全体

以下にソースコード全体を示します。

実行用ファイル

import os
from pprint import pprint
import random
import time

from notion_client import Client

# Import my functions
import get_all_pages_info_from_db
import get_multi_select_properties_by_color_names
import create_page_property_2post_parent_db

client = Client(auth=your_notion_token)

# Database IDs for testing
PARENT_DB_ID = 'your_parent_db_id'
CHILD_DB_A_ID = 'your_child_db_a_id'
CHILD_DB_B_ID = 'your_child_db_b_id'

# Get all pages info from child_db_A
pages_info_A = get_all_pages_info_from_db(client, CHILD_DB_A_ID)

# child_db_A_pages: {item_1: page_id, item_2: page_id, ...}
child_db_A_pages = {
    page["properties"]["Name"]["title"][0]["text"]["content"]: page["id"]
    for page in pages_info_A
}

# Get all pages info from child_db_B
pages_info_B = get_all_pages_info_from_db(client, CHILD_DB_B_ID)

# child_db_B_pages: {item_A: page_id, item_B: page_id, ...}
child_db_B_pages = {
    page["properties"]["Name"]["title"][0]["text"]["content"]: page["id"]
    for page in pages_info_B
}

## Define the Multi Select property value for the parent database
# Prepare some patterns
multi_select_properties_all = get_multi_select_properties_by_color_names()

multi_select_properties_pattern_1 = get_multi_select_properties_by_color_names('red', 'blue', 'green')

multi_select_properties_pattern_2 = get_multi_select_properties_by_color_names('blue', 'yellow', 'orange', 'gray')

multi_select_properties_pattern_3 = get_multi_select_properties_by_color_names('red', 'green', 'yellow')


## Define other properties for the parent database
# Create multiple page properties for the parent database
num_pages = 20000  # Number of pages to create

for i in range(num_pages):
    # Randomly select multi_select_properties
    multi_select_properties = random.choice([
        multi_select_properties_all,
        multi_select_properties_pattern_1,
        multi_select_properties_pattern_2,
        multi_select_properties_pattern_3
    ])

    # Randomly select child_page_A_ids (1 to 5 items)
    child_A_ids = random.sample(list(child_db_A_pages.values()), random.randint(1, 5))

    # Randomly select child_page_B_ids (1 to 5 items)
    child_B_ids = random.sample(list(child_db_B_pages.values()), random.randint(1, 5))

    page_property = create_page_property_2post_parent_db(
        post_title=f"Performance Test {i+1}",
        post_text=f"This is performance test {i+1} for the bulk upload.",
        multi_select_properties=multi_select_properties,
        child_page_A_ids=child_A_ids,
        child_page_B_ids=child_B_ids
    )

    # Post a single page to the parent database with retry logic
    for attempt in range(5):
        try:
            response = client.pages.create(
                parent={"database_id": PARENT_DB_ID},
                properties=page_property
            )
            break  # If successful, break out of the retry loop
        except Exception as e:
            if attempt < 4:  # If it's not the last attempt
                print(f"Error occurred: {e}. Retrying in 60 seconds...")
                time.sleep(60)
            else:
                error_message = f"Failed to create page after 5 attempts. Error: {e}"
                raise RequestException(error_message) from e

    # Print the result
    print(f"Successfully created page {i+1}")

    # Insert a pause after each page creation
    time.sleep(3)

# Output the result
print(f"Successfully created {num_pages} pages!")

関数ファイル(使用しなかった関数も含みます)

def get_all_pages_info_from_db(client, database_id: str) -> list:
    """
    Retrieves all information from a specified Notion database.

    Args:
        client: The Notion client object.
        database_id (str): The ID of the Notion database to query.

    Returns:
        list: A list of dictionaries, each containing all information for a page in the database.
    """
    response = client.databases.query(
        **{
            "database_id": database_id,
        }
    )

    return response["results"]


# Retrieve a list of page IDs of records existing in the database
def get_page_ids_from_db(client, database_id):
    """
    Retrieves a list of page IDs from a specified Notion database.

    Args:
        client: The Notion client object.
        database_id: The ID of the Notion database to query.

    Returns:
        list: A list of page IDs from the database.
    """
    response = client.databases.query(
        **{
            "database_id": database_id,
        }
    )

    # Extract only results from the whole response
    results = response["results"]
    page_ids = []
    for result in results:
        # Append each page ID to the list
        page_ids.append(result["id"])

    print(f"read_pages_from_database completed. (len={len(page_ids)})")
    return page_ids


def get_page_ids_and_titles_from_db(client, database_id: str) -> list: 
    """
    Retrieves page IDs and titles from a specified Notion database.

    Args:
        client: The Notion client object.
        database_id (str): The ID of the Notion database to query.

    Returns:
        list: A list of dictionaries, each containing 'page_id' and 'page_title' for each page in the database.
              If a page has no title, 'page_title' will be None.
    """
    response = client.databases.query(
        **{
            "database_id": database_id,
        }
    )

    # Extract only results from the whole response
    results = response["results"]
    page_datum = []
    for result in results:
        page_id = result["id"]
        try:
            page_title = result["properties"]["Name"]["title"][0]["plain_text"]
        except IndexError:
            page_title = None
        page_datum.append({"page_id": page_id, "page_title": page_title})

    return page_datum


def get_db_names_and_ids_from_query(client, query: str) -> list:
    """
    Search for databases in Notion using a query string and return a list of dictionaries containing database names and IDs.

    Args:
        client: The Notion client object.
        query (str): The search query string.

    Returns:
        list: A list of dictionaries, each containing 'db_title' and 'db_id' for found databases.
        If no databases are found, returns an empty list.
    """
    results = client.search(query=query).get("results")

    # Dict in List for db_info. [{db_title: db_id}, ...]
    db_names_ids = []
    db_count = 0

    for result in results:
        # Check if the result is a database
        if result["object"] == "database":
            db_count += 1
            db_info = {}
            db_info["db_title"] = result["title"][0]["plain_text"]
            db_info["db_id"] = result["id"]
            db_names_ids.append(db_info)
    
    if db_count:
        print(f"Found {db_count} databases.")
        return db_names_ids
    else:
        print(f"No databases found with query: {query}")
        return []


def get_db_properties_from_db_title_and_id(client, db_title: str, db_id: str) -> dict:
    """
    Retrieve the properties of a Notion database using its title and ID.
    CAUTION: The related database IDs that is retrieved are the 'Database ID's, not the 'Page ID's.

    Args:
        client: The Notion client object.
        db_title (str): The title of the database to search for.
        db_id (str): The ID of the database to match.

    Returns:
        dict: A dictionary containing the properties of the found database.
        If no matching database is found, returns an empty dictionary.
    """
    results = client.search(query=db_title).get("results")

    for result in results:
        if result["object"] == "database" and result['id'] == db_id:
            return result["properties"]
    
    print(f"No database found with title: {db_title} and id: {db_id}")
    return {}

マルチセレクト定義および関数ファイル

### Multi Select Property

# Define the single tag property by a each color
gray = {
    'color': 'gray',
    'description': None,
    'id': 'gray_id',
    'name': 'gray'
}

orange = {
    'color': 'orange',
    'description': None,
    'id': 'orange_id',
    'name': 'orange'
}

yellow = {
    'color': 'yellow',
    'description': None,
    'id': 'yellow_id',
    'name': 'yellow'
}

red = {
    'color': 'red',
    'description': None,
    'id': 'red_id',
    'name': 'red'
}

green = {
    'color': 'green',
    'description': None,
    'id': 'green_id',
    'name': 'green'
}

blue = {
    'color': 'blue',
    'description': None,
    'id': 'blue_id',
    'name': 'blue'
}


# Get the defined colors properties based on the provided color names.
def get_multi_select_properties_by_color_names(*color_names) -> list:
    """
    Get the defined colors based on the provided color names.
    If no color names are provided, return all defined colors.
    """
    color_dict = {
        'gray': gray,
        'orange': orange,
        'yellow': yellow,
        'red': red,
        'green': green,
        'blue': blue
    }

    # If no color names are provided, return all colors
    if not color_names:
        return list(color_dict.values())
    
    # Return the colors that match the provided color names
    result = []
    for name in color_names:
        if name in color_dict:
            result.append(color_dict[name])
        # If the color name is not found, print a warning message
        else:
            print(f"Warning: '{name}' is not a valid color name and will be ignored.")
    
    return result

新規ページPOST用プロパティ

def create_page_property_2post_parent_db(post_title: str, post_text: str, multi_select_properties: list, child_page_A_ids: list, child_page_B_ids: list) -> dict:
    """
    Create a page property for the parent database.
    """
    page_property = {
        'Name': {
            'id': 'title',
            'type': 'title',
            "title": [{ "type": "text", "text": { "content": post_title } }]
        },
        'テキストフィールド': {
            'id': 'text_field_id',
            'type': 'rich_text',
            'rich_text': [{
                "type": "text",
                "text": {
                    "content": post_text,
                    "link": None
                }
            }],
        },
        'テスト複数セレクト': {
           'multi_select': multi_select_properties
        },
        'リレーション先DB_A': {
            'has_more': False,
            'id': 'relation_column_A_id',
            'relation': [{"id": child_id} for child_id in child_page_A_ids],
            'type': 'relation'
        },
        'リレーション先DB_B': {
            'has_more': False,
            'id': 'relation_column_B_id',
            'relation': [{"id": child_id} for child_id in child_page_B_ids],
            'type': 'relation'
        }
    }
    
    return page_property

# Example usage:
# page_prop = create_page_property("Post Title", "Post Text", [{"name": "Tag1"}, {"name": "Tag2"}], ["id1", "id2"], ["id3", "id4"])

Category: 📚Notion

Tags: 🐍Python

📜
Scripts