nato243 weblog.n-jitter brand iconweblog.n-jitter
テクノロジー

Notionページの内容をPrintしてみる(Go)

2024.10.05
Golangやってみた
Notionページの内容をPrintしてみる(Go) アイキャッチ

最近、Notion API系でなんかできないかな、ということを考えてます。
Googleスプレッドシートをソースにコードを書く、みたいなDBとしての利用はNotionでもすぐにできそうなのは思いつきますが、Notionの場合はなんらかの簡易的なHeadless CMSとしても使えそうですね。

  • Notionにはデータベースがある
  • データベースのアイテム(ページ)には各種任意のプロパティを付与できる
  • アイテム(ページ)はコンポーネント(=Notionのブロック)によってはMarkdownやHTMLに平易に変換可能

なため、CMSとしての治安が最小限でよければ充分に利用可能なはずです。
今回はその素振りとして、NotionのページをAPI経由で取得するところまでやってみます。

簡単なページを用意する

今回解析するNotionのページ内容はざっくり以下のようなものです。

一般的なHTMLというかMarkdown等で表現可能なプロパティだけいったん使っています。

まずはインテグレーション定義

API Tokenを取得するために、適当なインテグレーションを定義しておきます。
会社のNotionなら管理者に作ってもらったほうが良いですね。

その後、対象のページ以下に認可を与えるためにこのインテグレーションを特定ページ以下に招待しておきます。

Golangでの簡単な取得例

それでは実際に取得していきましょう。
Golangの場合、jomei/notionapi というライブラリが便利そうでした。
Claudeあたりにこのライブラリを見せつつ、最低限の内容をPrintしてみます。

package main

import (
	"context"
	"fmt"
	"github.com/jomei/notionapi"
	"log"
	"os"
	"strings"
)

func main() {

	// Notion APIのトークンを環境変数から取得
	token := os.Getenv("NOTION_TOKEN")
	if token == "" {
		log.Fatal("NOTION_TOKEN environment variable is not set")
	}

	// Notion クライアントの初期化
	client := notionapi.NewClient(notionapi.Token(token))

	const PageID = `c632803bc4f54f04b6729358470cfd0f`

	pageData, err := client.Page.Get(context.Background(), notionapi.PageID(PageID))
	if err != nil {
		log.Fatalf("Error querying database: %v", err)
	}
	pageProps := pageData.Properties
	title := getPageTitle(pageProps)
	fmt.Printf("Title: %s\n", title)
	// 他のpropsを取得して表示
	printPageProperties(pageProps)
	fmt.Println(`----------------------------------------`)
	printPageContent(client, notionapi.BlockID(PageID))

}

func printPageContent(client *notionapi.Client, pageID notionapi.BlockID) {
	fmt.Println("Page Content:")

	var cursor notionapi.Cursor
	for {
		blocks, err := client.Block.GetChildren(context.Background(), pageID, &notionapi.Pagination{
			StartCursor: cursor,
			PageSize:    100,
		})
		if err != nil {
			log.Printf("Error getting page content: %v\n", err)
			return
		}

		for _, block := range blocks.Results {
			printBlock(client, block, "  ")
		}

		if !blocks.HasMore {
			break
		}
		cursor = notionapi.Cursor(blocks.NextCursor)
	}
}

func getPageTitle(properties notionapi.Properties) string {
	for _, prop := range properties {
		if titleProp, ok := prop.(*notionapi.TitleProperty); ok {
			if len(titleProp.Title) > 0 {
				return titleProp.Title[0].PlainText
			}
			return ""
		}
	}
	return ""
}

func printPageProperties(properties notionapi.Properties) {
	fmt.Println("Page Properties:")
	for key, prop := range properties {
		fmt.Printf("  %s: ", key)
		switch v := prop.(type) {
		case *notionapi.TitleProperty:
			if len(v.Title) > 0 {
				fmt.Println(v.Title[0].PlainText)
			}
		case *notionapi.RichTextProperty:
			if len(v.RichText) > 0 {
				fmt.Println(v.RichText[0].PlainText)
			}
		case *notionapi.NumberProperty:
			fmt.Println(v.Number)
		case *notionapi.SelectProperty:
			if v.Select.Name != "" {
				fmt.Println(v.Select.Name)
			}
		case *notionapi.MultiSelectProperty:
			var names []string
			for _, option := range v.MultiSelect {
				names = append(names, option.Name)
			}
			fmt.Println(strings.Join(names, ", "))
		case *notionapi.DateProperty:
			if v.Date != nil {
				fmt.Printf("Start: %v, End: %v\n", v.Date.Start, v.Date.End)
			}
		case *notionapi.CheckboxProperty:
			fmt.Println(v.Checkbox)
		default:
			fmt.Println("[Unsupported property type]")
		}
	}
	fmt.Println()
}

func printBlock(client *notionapi.Client, block notionapi.Block, indent string) {
	switch b := block.(type) {
	case *notionapi.ParagraphBlock:
		fmt.Printf("%sParagraph: %s\n", indent, b.GetRichTextString())
	case *notionapi.Heading1Block:
		fmt.Printf("%sHeading 1: %s\n", indent, b.GetRichTextString())
	case *notionapi.Heading2Block:
		fmt.Printf("%sHeading 2: %s\n", indent, b.GetRichTextString())
	case *notionapi.Heading3Block:
		fmt.Printf("%sHeading 3: %s\n", indent, b.GetRichTextString())
	case *notionapi.BulletedListItemBlock:
		fmt.Printf("%s• %s\n", indent, b.GetRichTextString())
	case *notionapi.NumberedListItemBlock:
		fmt.Printf("%s1. %s\n", indent, b.GetRichTextString())
	case *notionapi.ToDoBlock:
		checkbox := "[ ]"
		if b.ToDo.Checked {
			checkbox = "[x]"
		}
		fmt.Printf("%s%s %s\n", indent, checkbox, b.GetRichTextString())
	case *notionapi.ToggleBlock:
		fmt.Printf("%sToggle: %s\n", indent, b.GetRichTextString())
	case *notionapi.CodeBlock:
		fmt.Printf("%sCode (%s):\n", indent, b.Code.Language)
		texts := b.Code.RichText
		fmt.Println("```")
		for _, text := range texts {
			fmt.Println(text.PlainText)

		}
		fmt.Println("```")

	case *notionapi.QuoteBlock:
		fmt.Printf("%sQuote: %s\n", indent, b.GetRichTextString())
	case *notionapi.CalloutBlock:
		fmt.Printf("%sCallout: %s\n", indent, b.GetRichTextString())
	case *notionapi.ImageBlock:
		fmt.Printf("%sImage: %s\n", indent, b.Image.GetURL())
	case *notionapi.BookmarkBlock:
		fmt.Printf("%sBookmark: %s\n", indent, b.Bookmark.URL)
	default:
		fmt.Printf("%s[Unsupported block type: %s]\n", indent, block.GetType())
	}

	if block.GetHasChildren() {
		children, err := client.Block.GetChildren(context.Background(), block.GetID(), nil)
		if err != nil {
			log.Printf("Error getting child blocks: %v\n", err)
			return
		}
		for _, child := range children.Results {
			printBlock(client, child, indent+"  ")
		}
	}
}

結果は以下でした

Title: 記事タイトル
Page Properties:
  名前: 記事タイトル
  ID: [Unsupported property type]
  状態: 前

----------------------------------------
Page Content:
  Paragraph: 
  Heading 2: H2
  Paragraph: h3のコンテンツ
  Paragraph: h3のコンテンツ
  Code (javascript):
```
const a = 100;

function hoge (){
        return 1000;
}
```
  Paragraph: 
  Heading 3: h3
  Paragraph: h3のコンテンツ
  Paragraph: 
  Image: https://prod-files-secure.s3.us-west-2.amazonaws.com/1422afef-ef18-4209-b105-9507d3caacc1/0bff6871-59ce-4405-9d16-94ec34204c43/download.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45HZZMZUHI%2F20241005%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20241005T073646Z&X-Amz-Expires=3600&X-Amz-Signature=ab53ade9643030d7c7f065a71b3851245db1cb8e856f62a55bc16708ac5ad4fc&X-Amz-SignedHeaders=host&x-id=GetObject
  Paragraph: 
  Paragraph:

ちゃんととれてますね。
というか image URLは S3のus-west-2のpresign URLなんですね...
仮にNotionをHeadless CMSとして利用する場合、なんらかのタイミングで画像を自分のオブジェクトストレージに転写するなどの対応が必要そうです。