📝

【2024年版】Notionで重複したページを削除するPythonスクリプト

2024/05/21に公開

重複したページを削除するPythonスクリプト

このページの目的

現在、EvernoteからNotionにデータを移し替えてる最中なのであるが、Notionのインポート処理はたまにノートの中でページを重複させてコピーすることがある。

その場合、手作業で一つづつ重複したページを削除するか、NotionのAPIを利用してPython等で整理するぐらいしか方法がないので、サクッと作成してみた。

下準備

Notion Integration Tokenの取得

Notion Integration Tokenが必要になります。下記のページより、Notionを操作するのに必要になるトークンを取得してください。

https://www.notion.so/my-integrations

ここで設定した名前が接続先でしようするアプリ名になります。

DBにプログラムから操作する権限を与える

次に、重複しているページが存在してるDBのIDが必要になります。こちらはそのDBのURLの下記の部分がDBのIDになります。

https://www.notion.so/1111112333333aaabbbbb?v=ccccddddeeeeeffff8888833333

上記の「1111112333333aaabbbbb」の部分がDBのIDです。

その次に、そのDBの三点リーダーで、そのDBの設定を開いて、「コネクト」 > 「接続先」を開きます。

こちらに先ほどNotion Integration Tokenで作成したアプリ名があるはずなので、APIからこのDBページを操作するための権限を与えるために許可します。

Pythonスクリプト

スクリプトの実行にはrequestsが必要です。仮想環境を作ってpipでインストールしてください。

import requests
import time

# Notion APIの設定
NOTION_API_URL = "https://api.notion.com/v1"
DATABASE_ID = "{{重複を整理したいDBのID}}"
NOTION_TOKEN = "{{先ほど取得したNotion Integration Token}}"

headers = {
    "Authorization": f"Bearer {NOTION_TOKEN}",
    "Content-Type": "application/json",
    "Notion-Version": "2022-06-28"
}

def get_all_database_pages(database_id):
    url = f"{NOTION_API_URL}/databases/{database_id}/query"
    pages = []
    has_more = True
    next_cursor = None

    while has_more:
        data = {}
        if next_cursor:
            data["start_cursor"] = next_cursor
        
        response = requests.post(url, headers=headers, json=data)
        response.raise_for_status()
        result = response.json()
        
        pages.extend(result.get("results", []))
        has_more = result.get("has_more", False)
        next_cursor = result.get("next_cursor", None)
        
        if has_more:
            time.sleep(0.5)  # 少し待機して次のリクエストを送信
        
    return pages

def delete_page(page_id):
    url = f"{NOTION_API_URL}/blocks/{page_id}"
    print(f"Deleting page with ID: {page_id}")
    while True:
        response = requests.delete(url, headers=headers)
        if response.status_code == 429:
            print(f"Rate limit reached. Retrying in 1 minute...")
            time.sleep(60)
        elif response.status_code != 200:
            print(f"Failed to delete page: {response.status_code}, {response.text}")
            response.raise_for_status()
        else:
            break
    print(f"Deleted page: {page_id}")

def find_and_delete_duplicates(pages):
    titles_and_dates = {}
    batch_size = 5
    batch_count = 0

    for page in pages:
        properties = page.get("properties", {})
        name_property = properties.get("名前", {})
        title_list = name_property.get("title", [])
        created_time = properties.get("作成日時", {}).get("created_time", "")

        if title_list:
            title = title_list[0].get("text", {}).get("content", "")
            print(f"Processing title: {title}, Created time: {created_time}")
            if title in titles_and_dates and titles_and_dates[title] == created_time:
                print(f"Duplicate found: {title}, Page ID: {page['id']}")
                delete_page(page["id"])
                batch_count += 1
                if batch_count >= batch_size:
                    print("Batch limit reached, waiting for 1 minute...")
                    time.sleep(60)
                    batch_count = 0
            else:
                titles_and_dates[title] = created_time
        else:
            print("No title found for page:", page["id"])

def main():
    pages = get_all_database_pages(DATABASE_ID)
    find_and_delete_duplicates(pages)

if __name__ == "__main__":
    main()

このスクリプトでやってること

ページの取得

def get_all_database_pages(database_id):
    url = f"{NOTION_API_URL}/databases/{database_id}/query"
    pages = []
    has_more = True
    next_cursor = None

    while has_more:
        data = {}
        if next_cursor:
            data["start_cursor"] = next_cursor

        response = requests.post(url, headers=headers, json=data)
        response.raise_for_status()
        result = response.json()

        pages.extend(result.get("results", []))
        has_more = result.get("has_more", False)
        next_cursor = result.get("next_cursor", None)

        if has_more:
            time.sleep(0.5)  # 少し待機して次のリクエストを送信

    return pages

ページネーションに対応しつつ、全てのページのデータをpagesに保存して返します。

ページの削除

def delete_page(page_id):
    url = f"{NOTION_API_URL}/blocks/{page_id}"
    print(f"Deleting page with ID: {page_id}")
    while True:
        response = requests.delete(url, headers=headers)
        if response.status_code == 429:
            print(f"Rate limit reached. Retrying in 1 minute...")
            time.sleep(60)
        elif response.status_code != 200:
            print(f"Failed to delete page: {response.status_code}, {response.text}")
            response.raise_for_status()
        else:
            break
    print(f"Deleted page: {page_id}")

ページを削除する関数。レートリミットエラーが発生した場合には1分間待機して再試行します。

重複ページの検出と削除

def find_and_delete_duplicates(pages):
    titles_and_dates = {}
    batch_size = 5
    batch_count = 0

    for page in pages:
        properties = page.get("properties", {})
        name_property = properties.get("名前", {})
        title_list = name_property.get("title", [])
        created_time = properties.get("作成日時", {}).get("created_time", "")

        if title_list:
            title = title_list[0].get("text", {}).get("content", "")
            print(f"Processing title: {title}, Created time: {created_time}")
            if title in titles_and_dates and titles_and_dates[title] == created_time:
                print(f"Duplicate found: {title}, Page ID: {page['id']}")
                delete_page(page["id"])
                batch_count += 1
                if batch_count >= batch_size:
                    print("Batch limit reached, waiting for 1 minute...")
                    time.sleep(60)
                    batch_count = 0
            else:
                titles_and_dates[title] = created_time
        else:
            print("No title found for page:", page["id"])

重複するタイトルと作成日時のページを検出して削除します。バッチ処理を用いて、一度に多くのリクエストを送信しないようにしています。

備考

無料ユーザだとAPIの利用制限が割と厳しいのだけど、一応、5個削除するたびに1分の停止をしてるので引っかからないとは思う。

Discussion