ふくふくHukuhuku Inc.
EP.30Data Fetch 9分公開: 2026-09-02

Notion のデータベースを取る ── 型はあるが、形が入れ子

表計算ソフトと違って列に型があるので壊れにくい。ただし応答が深く入れ子になっていて、値を取り出すだけで一手間かかります。

#Notion#Python#API
執筆 / 監修
松尾 亮合同会社ふくふく 代表社員

データ基盤・データパイプライン構築 / BI / 生成 AI 活用支援を専門とするエンジニア (28 年)。 本記事は AI 利用ポリシーに基づき、生成 AI の補助で執筆 → 人間が監修・編集して公開しています。

プロフィール詳細
シェア

業務の管理表が Notion に置かれている、という状況も増えています。表計算ソフトと違い、列に型が定義されているので、日付の列に文字列が入るようなことは起きません。取得側から見ると扱いやすい部類です。

代わりに、の応答が深く入れ子になっています。1つの値を取り出すのに、いくつも階層を降りることになります。型ごとに構造が違うので、列の型を知らないと値を取り出せません

型ごとに取り出し方が違う

テキストの列と、選択肢の列と、日付の列で、応答の中の形が違います。素直に書くと、列ごとに条件分岐が並ぶことになります。型と取り出し方の対応表を1つ作って、そこに閉じ込めるのが見通しの良い形です。

型ごとの取り出し方を1か所にまとめる
Python
def extract_value(prop: dict):    """Notion のプロパティ 1 つから、素の値を取り出す。    未知の型が来たら None にせず、型名を返して気づけるようにする。"""    t = prop.get("type")
    if t == "title":        return "".join(x["plain_text"] for x in prop.get("title", []))    if t == "rich_text":        return "".join(x["plain_text"] for x in prop.get("rich_text", []))    if t == "number":        return prop.get("number")    if t == "select":        sel = prop.get("select")        return sel["name"] if sel else None    if t == "multi_select":        return [x["name"] for x in prop.get("multi_select", [])]    if t == "date":        d = prop.get("date")        return d["start"] if d else None    if t == "checkbox":        return prop.get("checkbox")
    return f"<未対応の型: {t}>"      # None にすると気づけない
未知の型を None にしない

対応していない型を黙って None にすると、その列だけ全部空になっているのに気づけません。型名を文字列で返しておけば、値を見た瞬間に「対応していない型が来ている」と分かります。

ページングは必ず必要になる

1回の要求で返る件数には上限があります。件数が少ないうちは1回で取り切れてしまうので、の処理を書かないまま動いてしまいます。そして件数が上限を超えた日から、静かに一部しか取れなくなります。

これは EP.02 で扱った取りこぼしの典型です。最初から続きを取る処理を書いておくのが唯一の対策になります。件数が少ないうちは1周で終わるだけで、害はありません。

続きが無くなるまで回す。件数が少なくても同じコードで動く
Python
import requests

def query_all(database_id: str, token: str, page_size: int = 100):    """カーソルを追って全件取る。上限を置いて無限ループを防ぐ。"""    url = f"https://api.notion.com/v1/databases/{database_id}/query"    headers = {        "Authorization": f"Bearer {token}",        "Notion-Version": "2022-06-28",        "Content-Type": "application/json",    }    cursor = None    for _ in range(1000):        body = {"page_size": page_size}        if cursor:            body["start_cursor"] = cursor
        resp = requests.post(url, headers=headers, json=body, timeout=(5, 30))        resp.raise_for_status()        data = resp.json()
        yield from data.get("results", [])
        if not data.get("has_more"):            return        cursor = data.get("next_cursor")
    raise RuntimeError("ページ数の上限に達した")

認証と権限の絞り方

取得には 相当の権限が要ります。連携用の設定を作り、読み取りたいデータベースだけを共有するのが基本です。全体を共有すると、意図しない範囲まで読めてしまいます。

この鍵はなどから読み込み、コードに直接書かないでください。EP.04 で扱ったとおり、一度コミットすると履歴から消えません。誤って入れてしまったら、鍵そのものを作り直すのが唯一の対処です。

また、があるので、大量のページを取るときは間隔を空ける必要があります。上のコードのように続きを追う形なら、1回ごとにわずかに待つだけで十分収まります。

人が構造を変えることは変わらない

型が定義されている点は表計算ソフトより安全ですが、列そのものが増減するのは同じです。誰かが列を追加し、別の誰かが名前を変える。これは なので、取得側で検知する必要があります。

検知は単純で、必要な列が存在するかを毎回確かめるだけです。無ければ止める。あるいは、EP.14 で扱ったように形を宣言しておけば、同じことが自動で行われます。

業務ツールをデータ源にするときの共通の注意点は、相手が業務のために使っているということです。取得のために運用を制約するのは本末転倒なので、変わることを前提に組んでください。次回は、更新を追うのに向いた形式を扱います。

業務ツールから取るときは、相手の運用を止めないことが最優先です。取得のために入力規則を増やすのは、たいてい長続きしません。

型がある点は確かに楽ですが、それは値の形が保証されるという意味であって、列そのものが安定するわけではありません。ここを取り違えると、油断したところで壊れます。

取得の対象が業務ツールになると、データ基盤の話ではなく現場の運用の話になります。誰がいつ何を入力しているかを知らずに設計すると、動くけれど使われないものができあがります。

取得の設計を決める前に、そのデータベースを誰がどう使っているかを一度見せてもらってください。設計の前提が分かるだけで、後から作り直す量が大きく減ります。

シェア

この記事の感想を教えてください

あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。

シリーズの外も探す:

まずは、現状を聞かせてください。

要件が固まっていなくて大丈夫です。現状診断と方針提案までを無料でお手伝いします。

無料相談フォームへ hello [at] hukuhuku [dot] co [dot] jp