iT邦幫忙

2025 iThome 鐵人賽

DAY 6
0
AI & Data

Notion遇上LLM:30天打造我的AI知識管理系統系列 第 6

【Day 6】實作抓取 Notion Page / Block:以 Python Basic 筆記為例

  • 分享至 

  • xImage
  •  

Day 5 我們學會了如何抓取 Notion Database 的 Schemarows,並將其清洗成乾淨的 JSON。但光是抓到 Database 還不夠,因為 Database 內的每一筆 row,都可能有更豐富的內容(Page):課程筆記、代碼範例、重點摘要等。

如果能透過 API 把這些 Block 轉換成結構化且乾淨的 JSON,我們就能做更多進一步的應用:

  • 進行向量化(Embedding),並建構 RAG 知識庫
  • 讓 LLM 自動整理重點、生成學習筆記

👉 本篇目標:

  • 以「Python Basic 學習筆記」為例,實際解析每個學習主題頁面內的筆記。
  • 介紹 Database → Page → Block 的階層結構。
  • 使用 Python 抓取 PageBlock 的內容。

1. 範例資料介紹 - Python Basic 學習筆記

我在 Notion 建立了一個 inline database:Python_Basic,主要用來整理 Python Basic 的學習重點,本篇會以這個學習筆記示範如何抓取PageBlock 的內容。

  • Database 結構
    • 每一列(row)代表Python Basic 中的一個學習主題 (Scope)
    • 主要欄位:
      • ScopeName:主題名稱,例如 Variables、Data Types
      • Status:學習進度(Todo / In Progress / Done)
        https://ithelp.ithome.com.tw/upload/images/20250920/20178104dDeufTYmFM.png
        這樣設計的好處是可以快速掌握進度,但真正的內容不在這張表,而是點進去的每個 Page
  • Page 結構
    • Database 中的每個 row 背後其實就是一個獨立的 Page
    • 例如打開 Page: Variables 變數,會看到:
      • 標題:Python Variables Tutorial
      • 說明文字:介紹變數的用途與規則
      • 程式碼區塊:
        x = 5
        name = "John"
        is_student = True
        
        這些內容在 API 裡會被拆成一個一個 Block。

2. Notion Database / Page / Block 的關係

在 Notion API 裡,這三個概念的關係如下:

  • Database:表格型的集合,用來存放多筆 Page
  • PageDatabase 的一筆資料(row)其實就是一個 Page,可以打開看到詳細內容。
  • BlockPage 內的最小單位(文字、圖片、代碼區塊、todo list 都是 Block)。

以我們的範例資料來說,結構大致會是這樣:

Database: Python_Basic
 └── Page: Variables 變數
       ├── Block: 標題 "Python Variables Tutorial"
       ├── Block: 文字敘述
       ├── Block: 程式碼區塊
       └── Block: 清單項目

https://ithelp.ithome.com.tw/upload/images/20250920/20178104VFZVfErSzX.png

如果你想更直觀理解 Block,可以參考官方文件:
👉 Block basics: build the foundation for your team’s pages

3. 實作 - 抓取 Page 與 Block

3.1 抓 DB rows(跨頁分批)

延續 Day 5 的程式,我們已經能取得 Database rows,每一筆 row 會有一個 page_id,接下來,我們就可以利用這個 page_id 去抓 Page 的內容。

  • API
    POST https://api.notion.com/v1/databases/{database_id}/query
    
  • 技術要點
    • 支援 分頁 (pagination),每次最多取 100 筆。
    • 使用 start_cursor 搭配 has_more 參數來一次抓完所有 rows。
    • 回傳的每一筆 row 本質上是 Page object,包含 metadata 與 properties。
  • fetch_learning_database.py 程式碼
    import requests, os
    from dotenv import load_dotenv
    
    load_dotenv()
    TOKEN = os.getenv("NOTION_TOKEN")
    BASE = "https://api.notion.com/v1"
    HEADERS = {
        "Authorization": f"Bearer {TOKEN}",
        "Notion-Version": "2022-06-28",
        "Content-Type": "application/json",
    }
    
    def query_database_all(database_id: str, page_size: int = 100) -> list:
        url = f"{BASE}/databases/{database_id}/query"
        payload, results = {"page_size": page_size}, []
        while True:
            res = requests.post(url, headers=HEADERS, json=payload)
            res.raise_for_status()
            data = res.json()
            results.extend(data.get("results", []))
            if not data.get("has_more"):
                break
            payload["start_cursor"] = data["next_cursor"]
        return results
    

3.2 抓 Page blocks(含 children 展開)

  • API
    GET https://api.notion.com/v1/blocks/{block_id}/children
    
  • 技術要點
    • BlockPage 的最小單位(文字、標題、程式碼、清單...)。
    • Block 可能有子層 (has_children=true),必須用遞迴方式展開。
  • 常見 Block type
    • paragraph
    • heading_1/2/3
    • code
    • to_do
    • bulleted_list_item / numbered_list_item
  • fetch_learning_blocks.py 程式碼:
    import os, requests
    from dotenv import load_dotenv
    load_dotenv()
    
    TOKEN = os.getenv("NOTION_TOKEN")
    BASE = "https://api.notion.com/v1"
    HEADERS = {
        "Authorization": f"Bearer {TOKEN}",
        "Notion-Version": "2022-06-28",
        "Content-Type": "application/json",
    }
    
    def fetch_block_children(block_id: str, page_size: int = 100) -> list:
        url = f"{BASE}/blocks/{block_id}/children"
        params, results = {"page_size": page_size}, []
        while True:
            res = requests.get(url, headers=HEADERS, params=params)
            res.raise_for_status()
            data = res.json()
            results.extend(data.get("results", []))
            if not data.get("has_more"):
                break
            params["start_cursor"] = data["next_cursor"]
        return results
    
    def fetch_page_blocks_recursive(page_id: str) -> list:
        def expand(block):
            if block.get("has_children"):
                kids = fetch_block_children(block["id"])
                block["children"] = [expand(k) for k in kids]
            return block
        roots = fetch_block_children(page_id)  # page 本身作為起點
        return [expand(b) for b in roots]
    

3.3 解析多型別 Block(heading/paragraph/list/code…)

  • API 結果的資料型態
    • 每個 block 的內容多半放在 rich_text 陣列。
    • Code 區塊會有 language 與 text。
    • To-do block 會有 checked 屬性。
  • 技術要點
    • 用 join_text 將多個 rich_text 合併為單一純文字字串。
    • 根據不同 type 做解析:
      • paragraph → 純文字
      • heading_2 → 標題文字
      • code → 包含 language 與程式碼本身
      • to_do → 附加 checked 狀態
  • parse_learning_blocks.py 程式碼
    def join_text(rich_text_arr):
        if not rich_text_arr: return ""
        out = []
        for rt in rich_text_arr:
            if "plain_text" in rt:
                out.append(rt["plain_text"])
            elif "text" in rt and rt["text"]:
                out.append(rt["text"].get("content", ""))
        return "".join(out).strip()
    
    def parse_block(block):
        btype = block["type"]
        data = block[btype]
    
        # 大多數型別都有 rich_text:heading_*、paragraph、bulleted_list_item、to_do…
        if "rich_text" in data:
            item = {"type": btype, "text": join_text(data["rich_text"])}
            if btype == "to_do":
                item["checked"] = data.get("checked", False)
            if block.get("children"):
                item["children"] = [parse_block(c) for c in block["children"]]
            return item
    
        # code 類型:取語言與內容
        if btype == "code":
            return {
                "type": "code",
                "language": data.get("language"),
                "code": join_text(data.get("rich_text", [])),
                "children": [parse_block(c) for c in block.get("children", [])] or None
            }
    
        # 其它型別先原樣保留(例如 divider、image…)
        item = {"type": btype, "raw": data}
        if block.get("children"):
            item["children"] = [parse_block(c) for c in block["children"]]
        return item
    
    def parse_blocks(blocks):
        return [parse_block(b) for b in blocks]
    

3.4 主流程控制

這支程式的主要目的,是把前面三支模組程式串成一條完整的流程,讓我們可以一鍵執行,直接從 Database 拉資料 → 抓 Page → 清理 Block → 輸出 JSON

  • 執行流程
    1. 呼叫 fetch_learning_database.py
      • 使用 Notion API POST /databases/{database_id}/query
      • 把 Database 內的 rows 全部抓下來(每個 row 對應一個 Page)。
    2. rows 拿第一個 Page ID
      • 在這裡我們先簡化,只選第一個 row 的 Page 來示範。
      • 未來可以擴充成跑全部 rows。
    3. 呼叫 fetch_learning_page.py
      • 使用 Notion API GET /blocks/{page_id}/children
      • 抓取該 Page 的所有 Blocks(支援遞迴展開子 block)。
    4. 呼叫 parse_learning_blocks.py
      • 把不同類型的 Block(paragraph、heading、code、to_do…)轉成乾淨 JSON。
      • e.g.
        {
          "type": "code",
          "language": "python",
          "text": "x = 5"
        }
        
    5. 輸出成檔案
      • 清理後的結果寫入 data/clean/page_blocks_learning.json
      • 方便後續檢視、除錯、以及進一步做 NLP 處理。
  • run_day6.py 程式碼
    import os, json, collections
    from dotenv import load_dotenv
    from fetch_learning_database import query_database_all
    from fetch_learning_blocks import fetch_page_blocks_recursive
    from parse_learning_blocks import parse_blocks
    
    load_dotenv()
    DB_ID = os.getenv("NOTION_DATABASE_ID_LEARNING")
    os.makedirs("data/clean", exist_ok=True)
    
    rows = query_database_all(DB_ID)
    print("Total rows:", len(rows))
    
    all_pages = []
    counter = collections.Counter()
    
    for row in rows:
        page_id = row["id"]
        blocks = fetch_page_blocks_recursive(page_id)   # 取齊所有 children
        parsed = parse_blocks(blocks)                   # 轉成統一結構
        all_pages.append({"page_id": page_id, "blocks": parsed})
        counter.update([b["type"] for b in blocks])
    
    out_path = "data/clean/page_blocks_learning.json"
    with open(out_path, "w", encoding="utf-8") as f:
        json.dump(all_pages, f, ensure_ascii=False, indent=2)
    
    print(f"Saved: {out_path}")
    print("Top block types:")
    for t, c in counter.most_common(10):
        print(f" - {t}: {c}")
    

3.5 執行結果

以本次範例資料的執行結果,執行python src/run_day6.py,在 Ternimal 看到:

Total rows: 8
Saved: data/clean/page_blocks_learning.json
Top block types:
 - paragraph: 161
 - heading_3: 73
 - bulleted_list_item: 71
 - column_list: 54
 - divider: 51
 - code: 19
 - heading_2: 10
 - child_database: 9
 - quote: 4
 - numbered_list_item: 3

打開 data/clean/page_blocks_learning.json,內容會像這樣:
每個 page_id 底下有 blocks,而 blocks 有多個對應的 typetext 等資料。
https://ithelp.ithome.com.tw/upload/images/20250920/20178104JBDxu8kKr1.png

4. 小結與下篇預告

到目前為止,我們已經完成了幾個重要的里程碑:

  • 成功抓取 Database rows,取得每一列對應的 Page ID,這是 Data Pipeline 的入口,讓我們知道有哪些「章節 / 頁面」需要進一步處理。
  • 透過 Page APIBlock API,把頁面內的筆記、段落、程式碼區塊都抽取出來。
  • 把這些 Block 解析成乾淨的 JSON 結構,方便後續使用。

在 Day 7,我們會進一步把這些模組串成一條完整的 Pipeline

  • 自動化:從多個 Database 抓取 rows → 取得 Page ID → 解析 Page → 展平成 Block。
  • 批次處理:一次處理多個 Database,形成更大的資料集合。
  • 統一輸出:將清理後的內容轉成標準化 JSON,方便後續存入資料庫或做 RAG。

上一篇
【Day 5】實作抓取 Notion Database:以釜山旅遊行程表為例
下一篇
【Day 7】Notion 自動化 Pipeline 設計:Database → Page → Block
系列文
Notion遇上LLM:30天打造我的AI知識管理系統7
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言