【開発日記】2025.12.18 ~「X API」を使用してブックマーク取得~

こんにちは~ NRLH LAB の なぎ です。

今日は、

「自分の X のアカウントからブックマーク取得をしてみたよ~」

というのを開発日記として残したいと思います。

経緯

今、うちのAIアシスタントに新しい機能を実装していて、その中でXのアカウントの情報が欲しかったので実装をしてみました。

↓うちのAIアシスタントについて

note.com

XのAPIに触るのは始めてなので、色々調べながら...

方法

言語: Python

ライブラリ: tweepy

API: X API V2 Free

実装

自分のアカウントから 「ブックマーク」 を取得します。

以下を参考にしました。

infomisc.blog

Authentication — tweepy 4.14.0 documentation

qiita.com

コードを表示する

from logging import getLogger
import webbrowser
import json
import requests

import tweepy

import config


ResponseType = requests.Response


# 「ブックマーク」データクラス
class Bookmark:
    def __init__(self, id: str, text: str):
        self._id = id
        self._text = text

    @property
    def id(self):
        return self._id
    
    @property
    def text(self):
        return self._text
    
    def __str__(self):
        return f"'id': '{self.id}', `text`: '{self.text}'"


# X API の認証のスコープ
# https://docs.x.com/fundamentals/authentication/oauth-2-0/authorization-code
SCOPES = ["tweet.read", "users.read", "offline.access", "bookmark.read"]


logger = getLogger(__name__)


def get_bearer_tokens(is_PKCE = True) -> dict[str, str]:
    if is_PKCE:
        # OAuth 2.0 Flow with PKCE による認証
        oauth2_user_handler = tweepy.OAuth2UserHandler(
            client_id=config.X_OAUTH2_CLIENT_ID,
            redirect_uri="https://x.com/",
            scope=SCOPES,
            client_secret=config.X_OAUTH2_CLIENT_SECRET
            )
        authorization_url = oauth2_user_handler.get_authorization_url()
        logger.info(f"認証フロー(OAuth 2.0 Flow with PKCE)のため、認証URLをブラウザで起動します。: {authorization_url}")
        webbrowser.open(authorization_url)
        logger.info("【要入力】アプリにアクセスを許可した後、リダイレクトされたURLをここに入力してください。")
        response_url = input()
        token = oauth2_user_handler.fetch_token(response_url)
        return {"bearer_token": token["access_token"]}
    else:
        return {"bearer_token": config.X_OAUTH_BEARER_TOKEN}


def make_client(is_PKCE = True):
    """
    X API を呼び出すためのクライアント(tweepy.Client)を作成する

    Parameters
    ----------
    is_PKCE: bool
        「OAuth 2.0 Flow with PKCE」での認証フローでクライアントを作成するかどうか。

    Returns
    -------
    client: tweepy.Client
        作成したクライアント(tweepy.Client)のインスタンス
    """
    bearer_tokens = get_bearer_tokens(is_PKCE)
    return tweepy.Client( bearer_token=bearer_tokens["bearer_token"]
                        , consumer_key=config.X_API_KEY
                        , consumer_secret=config.X_API_KEY_SECRET
                        , access_token=config.X_OAUTH_ACCESS_TOKEN
                        , access_token_secret=config.X_OAUTH_ACCESS_TOKEN_SECRET
                        , return_type=ResponseType
                        )


def get_bookmarks_response(client: tweepy.Client):
    CACHE_FILEPATH = ".\\aia300\\data\\x\\cache\\get_bookmarks.json"
    response = None
    try:
        # 公式では max_results <= 800 と書いてあるが、400 Bad Requestエラー
        # 1 ~ 100 とレスポンスで返ってきた(Free版だから?)
        response = client.get_bookmarks(max_results=100).json()

        # キャッシュ(json形式でファイルに保存)
        with open(CACHE_FILEPATH, 'w', encoding='utf-8') as file:
            json.dump(response, file, indent=2, ensure_ascii=False)

    except tweepy.TooManyRequests:
        # リクエスト数overなので、キャッシュから取得
        logger.warning("リクエスト数overのため、過去に取得した最新のデータを参照します。")
        with open(CACHE_FILEPATH, 'r', encoding='utf-8') as file:
            response = json.load(file)

    return response["data"]


def get_bookmarks(client: tweepy.Client) -> list[Bookmark]:
    """
    ブックマーク取得(最大: 最新100個)

    Parameters
    ----------
    client: tweepy.Client
        クライアント

    Returns
    -------
    bookmarks: list[Bookmark]
        取得したブックマークのリスト

    Notes
    -----
    X API Free をしているので、リクエスト上限は "1 request / 15 min" です。
    https://developer.x.com/en/portal/products

    上限に達していた場合は、過去に取得した最新データ(ローカル保存)を返します。
    """
    response = get_bookmarks_response(client)
    bookmarks = []
    for item in response:
        bookmarks.append(Bookmark(id=item["id"], text=item["text"]))
    return bookmarks


# X API 呼び出し処理を実装したクラス
class X:
    def __init__(self):
        self._client = make_client()
        
    @property
    def client(self):
        return self._client
    
    def get_bookmarks(self):
        return get_bookmarks(self.client)

取得したデータ(一部)

{
  "data": [
    {
      "text": "今回の記事(https://t.co/50qVWkXhMU)はこのRPのプロンプトを使わせていただき、校正しました。これとてもおすすめです! https://t.co/Z9NBp7x1z6",
      "id": "2000914967563002107",
      "edit_history_tweet_ids": [
        "2000914967563002107"
      ]
    },
    {
      "text": "独学でモデリングを覚えた方にぜひ読んでほしい、\n ゲーム業界のキャラモデルが“実際どんな仕様で作られてるか” をまとめた記事を書きました。\n今回はスマホ向けのローポリ寄りキャラを例に、\n仕様・データ管理・工程・スケジュール感まで具体的に紹介しています。\n\nあくまで一例なので、",
      "id": "1994053792317501859",
      "edit_history_tweet_ids": [
        "1994053792317501859"
      ]
    },
    {
      "text": "【Unity】汎用的なジャンプ機構の実装例 - コヨーテタイム・先行入力・多段ジャンプ・可変ジャンプを共通化する | watabe_h\nhttps://t.co/qOkdBkMj8l",
      "id": "1996234273922125882",
      "edit_history_tweet_ids": [
        "1996234273922125882"
      ]
    },
    {
      "text": "【インタビュー】サイバーエージェントのゲーム事業部には「プロジェクト付け」でも「研究開発」でもない、グラフィックやシステム含めた“技術課題拾いまくりエンジニア組織”があるそうだ。なぜ?なんのために?求人中らしいので話を聞いた [AD]\nhttps://t.co/lwL1mxrZVw https://t.co/FsNLg2bQDt",
      "id": "2000400943247142959",
      "edit_history_tweet_ids": [
        "2000400943247142959"
      ]
    },
    {
      "text": "言語化が下手な人は5つのタイプに分けられる|すてぃお @suthio_ https://t.co/xRW7WqkUG4",
      "id": "2000723816125988910",
      "edit_history_tweet_ids": [
        "2000723816125988910"
      ]
    },
    {
      "text": "【『原神』級のゲームはこうして生まれた──PCオンライン黎明期から原神・Black Myth誕生までの20年と日本の勝ち筋】\n 中国ゲームがなぜ高品質を量産できるのか。20年の歴史を紐解き、日本メーカーの現実的な勝ち筋を考察してみた\nhttps://t.co/Tr7JtJxJP5",
      "id": "1999677432455119204",
      "edit_history_tweet_ids": [
        "1999677432455119204"
      ]
    }
  ],
  "meta": {
    "result_count": 6
  }
}



ポイント

認証

ブックマーク取得には、「OAuth 2.0 Flow with PKCE」 を使用して認証していないといけないようでした。

tweepyリファレンス を読みその通りにコードを書けばよいのですが...

access_token = oauth2_user_handler.fetch_token(
    "Authorization Response URL here"
)
client = tweepy.Client("Access Token here")

この部分、tweepy.Client()は、

def __init__(self
        , bearer_token=None
        , consumer_key=None
        , consumer_secret=None
        , access_token=None
        , access_token_secret=None
        , *, return_type=Response, wait_on_rate_limit=False
    ):

と定義されていますが、bearer_token=access_tokenのデータをセットするのが正しいです。(ややこしい)

def __init__(self
        , bearer_token=None           # 〇ここに oauth2_user_handler.fetch_token()["access_token"] をセット
        , consumer_key=None
        , consumer_secret=None
        , access_token=None           # ×ここではない
        , access_token_secret=None
        , *, return_type=Response, wait_on_rate_limit=False
    ):

access_token=の方に渡してしまっていてここでかなり詰まりました...


キャッシュ

X の API はプランによってリクエストが制限されています。

https://developer.x.com/en/portal/products

Freeプラン(無料プラン)では一部APIが利用できなかったり、リクエスト数の制限が厳しいです。

今回の「ブックマーク取得」は 1リクエスト / 15分 / 1ユーザー となっています。

データの取得に失敗しても1リクエスト消費扱いになるので、

レスポンスはキャッシュ(ファイルに保存)し、制限によってデータが取得できなかった場合はそのファイルから読み込む

という形にしました。(今回の機能ではそこまでリアルタイムでなくてもよいので)

def get_bookmarks_response(client: tweepy.Client):
    CACHE_FILEPATH = ".\\aia300\\data\\x\\cache\\get_bookmarks.json"
    response = None
    try:
        response = client.get_bookmarks(max_results=100).json()

        # キャッシュ(json形式でファイルに保存)
        with open(CACHE_FILEPATH, 'w', encoding='utf-8') as file:
            json.dump(response, file, indent=2, ensure_ascii=False)

    except tweepy.TooManyRequests:
        # リクエスト数overなので、キャッシュから取得
        logger.warning("リクエスト数overのため、過去に取得した最新のデータを参照します。")
        with open(CACHE_FILEPATH, 'r', encoding='utf-8') as file:
            response = json.load(file)

    return response["data"]



おわりに

ということで、

「Xからブックマークを取得してみたよ~」

という日記でした。

XのAPI君、もうちょっと仲良くしてほしいな~チラッ

実際に今開発している機能にどう組み込んだかはまた後日日記にしたいと思います。

ではまた。

「ゲームエンジン調査隊」という記事(シリーズ)を作成中・・・

こんにちは~ NRLH LAB の なぎ です。

最近やっていることの報告として「ゲームエンジン調査隊」という記事(シリーズ)を書き始めたことについて書きたいと思います。

※まだ記事は作成中で公開してません...><

ゲームエンジン調査隊」とは

簡単に言えば、

ゲームプログラマである私が世の中にある様々なゲームエンジンを触ってみて、その過程や感想を記事にしていこう!

という企画です。

ゲームエンジンというと「Unity」や「Unreal Engine」が有名ですが、他にも「Godot」「Game Maker」「cocos2dx」「RPGツクール」「ティラノビルダー」のように様々なゲーム開発ソフト(ライブラリ)があります。

私もこれらの存在は知っているのですが使ったことがなく、UnityやUnreal Engineで開発できるので使う機会もなく...

という感じで興味はあるけど手は出せていない、こう思っている方は私以外にも多いんじゃないでしょうか。

なのでそういう方向けに「ざっくりこんな感じだったよ~」って伝える読み物があったらおもしろいかなと思い記事を書き始めました。

寝る前とか通勤の合間とかにちょろっと読んで「ふ~んそんな感じなんだ」ってなってくれればいいかなくらいでゆる~く書いています。

裏テーマ

ここまでが主軸のコンセプトで、それ以外にも個人的な意図があります。


~ 1. 同一の人が書いた解説記事の作成 ~

調べればそれぞれのゲームエンジンの使い方、解説はいくらでもでてきます。

ただ、それは単にそのツールの使い方を説明しているだけで、他のエンジンと比較して 「こういう特徴がある」「こういう良さがある」「こういうシチュエーションに対して役に立つ」 という視点で解説している記事は少ないかなと思います。

「Unityは使えるけどUnreal Engineはわからない。Unreal Engineってどんな感じなんだろう。」

と思ったときにざっくりUnityと比較して全体像を掴める記事があったら有用かなと。

実際今この記事を作成している時点でも、まだ私は「Godot」や「RPGツクール」がどういったものかはよく知らず、

「どういう感じなんだろう(特にゲームプログラマ目線で)」という気持ちをサッと解決してくれる、気軽に読める記事があったらな~

と思っているので、自分で書くことにしました。


~ 2. スキルアップ

これは単なる私のゲームプログラマとしての知見を深めスキルアップに繋げるためです。

ゲームエンジンがこれだけ世の中にあるということは様々な意図があるはずです。

例えば、

  • 「2Dゲームの開発に特化」
  • RPGゲームの開発に特化」
  • 「プログラムを書かなくてもゲームが簡単に作れる」

みたいに。

これらを実現するためには機能として色々用意していたり工夫していたりすると思うので、それらを吸収して今後のゲーム開発に活かせたらな

という意図です。


~ 3. ゲームを作りながらC++を勉強できるゲームエンジンを探す ~

ゲームを作るとして代表的なのが「Unity」なのですが、UnityはC#」でプログラミングします。

ただ、ゲームプログラマとしてちゃんとした技術を身に着けるにはC++」の習得は必須だと思います。

じゃあ

「「Unreal Engine」なら「C++」で開発できるからそれでいいじゃん。」

と思うかもしれないですが、「Unreal Engine」の「C++」は正確にはUnreal C++」と呼ばれ、普通のC++よりも "難しい" 言語です。

Unreal Engine側もC++ができる前提で話を進めてくる印象なので、

C++初学者が勉強のために「Unreal Engine」でゲームを作る」

のは個人的にはオススメしません

C++の勉強目的であれば

  • 通常のC++で書ける
  • ゲーム開発に必要な最低限の機能

このような環境でゲームを作りながら勉強するのがよいと思います。

で、現状それに該当するものは何かと言われるとパッとは出てこないんですよね...

一応「DXライブラリ」だったり「cocos2dx」だったりがそれに近いかなとは思うのですが、「これがいい!」とはっきりとは答えられなくて...

ゲームエンジンが普及する前はこれらでC++で開発するのが当たり前だったのですが、現代はゲームエンジンが普及して間口が広がったので、ツクール系やUnity(C#)から入ってくる人も多いと思います

そういった人達向けのC++の勉強を進めるロードマップと考えると...微妙かも?と感じたので、何かいいものはないかなと探す目的がこの企画にはあるという感じです。

(現状既にそれっぽいものは見つけているので、早いとこ触って記事にしたいと思います)



おわりに

色々書いていたら長くなってしまいました。

絶賛記事作成中なのですが、思ったより書きたいことが多くボリュームが大きくなりそうです。

がんばって書いていますが最低限の内容で公開できるようになるのもしばらく先になりそうです。

公開したら告知します。

まぁ NRLH LAB の活動として長く続けていこうと思うので、長期シリーズとして少しずつ進めていこうと思います。 (他にも活動でやりたいこと沢山あるので...)


ちなみに記事自体は Zenn の本という機能を使って作成していく予定です。 Zenn の方も記事書いていくのでこちらもよろしくお願いします。

zenn.dev

自己紹介

はじめまして!NRLH LABなぎ です!

最近、「NRLH LAB」という名で活動を始めたので、まずは自己紹介をしたいと思います!

NRLH LAB とは

  • NRLH はアルファベットそのまま「エヌ、アール、エル、エイチ」と読みます。
  • LAB は 「ラボ」と読みます。
  • 私が活動する上での活動名のようなものです。
  • 組織名のほうが近いかも(私1人しかいませんが)。

私について

  • 個人名は「なぎ」といいます。
  • 年齢等は非公開です。
  • 一応、以前、ゲーム会社でプログラマをしていました。(まだまだ未熟ですが...)

活動について

NRLH LABは、プログラミング能力等を活かして、他のクリエイターさんの力になれるような活動をしていきます。

具体的には、

  • Zenn、Qiita などに解説記事を投稿
  • ツール等の作成
  • 他エンジニア(クリエイター)の方の直接サポート
  • ゲーム開発のお手伝い
  • その他様々な形での貢献

などなど

しばらくは実績と信頼づくりのために無償で活動していきますが、そのうちちょっとしたお仕事とかも受けられるようにしていきたいですね。

活動場所

ここ(はてなブログ)は主に日々の活動の日記や記録を書いていこうと思います。

活動自体はX(旧Twitter)Discord、それから、ZennQiitaNoteYoutube 等で行っていく予定です。

ぜひこちらもよろしくお願いします。

おわりに

ここまで読んでくださりありがとうございます。

活動等かなり不定期になるとは思いますが、しっかりとみなさんの力になれるようがんばりますので、これからよろしくおねがいします!!