開発情報・ナレッジ

投稿者: 株式会社ゴンドラ 2025年8月21日 (木)

SPIRALからX(旧Twitter)に連携し、ツイートを取得する方法

SPIRALからxの投稿を取得する方法。

概要

Webフォームから指定したX (旧Twitter) ユーザーIDの投稿を取得し、任意で指定したキーワードを含む投稿を絞り込んで表示するPHPツールです。
X、SPIRALとの連携を実現することで、キャンペーン等で応募者が指定のハッシュタグを含む投稿を行ったかを確認したり、投稿内容をDBへ値を保存したりすることが可能です。
デモサイト

ソースコード

まずはソースコードをご覧ください。

<?php
//<!-- SMP_DYNAMIC_PAGE DISPLAY_ERRORS=ON NAME=x.php -->
// X API のBearer Token

$bearerToken = '';

// ① TwitterユーザーID(数値)を取得する関数
function getTwitterNumericId($screenName, $bearerToken) {
    $url = "https://api.twitter.com/2/users/by/username/{$screenName}";
    $headers = [
        "Authorization: Bearer {$bearerToken}"
    ];

    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $response = curl_exec($ch);
    curl_close($ch);

    $data = json_decode($response, true);
    return $data['data']['id'] ?? null;
}

// ② 最新ツイートを取得
function fetchLatestTweets($userId, $bearerToken, $maxResults = 10) {
    // 投稿日時も取得するために tweet.fields パラメータを追加
    $url = "https://api.twitter.com/2/users/{$userId}/tweets?max_results={$maxResults}&tweet.fields=created_at";
    $headers = [
        "Authorization: Bearer {$bearerToken}"
    ];

    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $response = curl_exec($ch);
    curl_close($ch);

    $data = json_decode($response, true);
    return $data['data'] ?? [];
}


// フォームの入力値を保持するための変数を初期化
$inputTwitterId = '';
$inputKeyword = '';
$resultHtml = '';

// フォームからPOSTリクエストがあった場合の処理 (twitter_id のみ必須)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['twitter_id'])) {
    
    // 入力値をサニタイズして変数に格納
    $inputTwitterId = htmlspecialchars(trim($_POST['twitter_id']), ENT_QUOTES, 'UTF-8');
    // キーワードは存在すれば格納、なければ空文字
    $inputKeyword = isset($_POST['keyword']) ? htmlspecialchars(trim($_POST['keyword']), ENT_QUOTES, 'UTF-8') : '';
    
    // 処理で使う変数に代入
    $screenName = $inputTwitterId;
    $targetKeyword = $inputKeyword;

    // `@`記号が先頭にあれば除去
    if (strpos($screenName, '@') === 0) {
        $screenName = substr($screenName, 1);
    }

    // 応募番号は仮で設定
    $entryNumber = 'XYZ' . time(); 

    ob_start(); // 出力バッファリングを開始

    // 表示用のヘッダーを作成(キーワードが空欄でない場合のみ表示)
    $headerText = "処理結果:@{$screenName}";
    if (!empty($targetKeyword)) {
        $headerText .= " (キーワード: {$targetKeyword})";
    }
    echo "<h4>{$headerText}</h4>";

    $numericId = getTwitterNumericId($screenName, $bearerToken);

    if (!$numericId) {
        echo "<p style='color: red;'>ユーザーIDの取得に失敗しました。ユーザーが存在しないか、非公開アカウントの可能性があります。</p>";
    } else {
        echo "<p>数値ID: {$numericId} のツイートを取得します...</p>";
        $tweets = fetchLatestTweets($numericId, $bearerToken);
        $foundTweets = [];

        if (empty($tweets)) {
            echo "<p>このユーザーのツイートは見つかりませんでした。</p>";
        } else {
            foreach ($tweets as $tweet) {
                // キーワードが空、またはキーワードが含まれている場合に true となる
                if (empty($targetKeyword) || strpos($tweet['text'], $targetKeyword) !== false) {
                    $foundTweets[] = $tweet;
                }
            }
        }

        if (empty($foundTweets)) {
            // キーワードがある場合とない場合でメッセージを分ける
            if (!empty($targetKeyword)) {
                echo "<p>キーワード「{$targetKeyword}」を含むツイートは見つかりませんでした。</p>";
            } else {
                echo "<p>ツイートは見つかりませんでした。</p>"; // 基本的に通らないはずだが念のため
            }
        } else {
            echo "<p style='color: green;'>" . count($foundTweets) . "件の該当ツイートが見つかりました。</p>";
            echo "<ul>";
            foreach ($foundTweets as $tweet) {
                $tweetDate = new DateTime($tweet['created_at']);
                $formattedDate = $tweetDate->format('Y-m-d H:i:s');
                echo "<li>";
                echo "<strong>投稿日時:</strong> " . htmlspecialchars($formattedDate, ENT_QUOTES, 'UTF-8') . "<br>";
                echo "<strong>内容:</strong> " . nl2br(htmlspecialchars($tweet['text'], ENT_QUOTES, 'UTF-8'));
                echo "</li>";
            }
            echo "</ul>";
        }
    }

    $resultHtml = ob_get_clean(); // バッファの内容を取得し、バッファをクリア
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>X投稿 取得ツール</title>
    <style>
        body { font-family: sans-serif; line-height: 1.6; color: #333; max-width: 800px; margin: 20px auto; padding: 0 15px; }
        .container { padding: 20px; border: 1px solid #ddd; border-radius: 8px; background-color: #f9f9f9; }
        h1 { text-align: center; color: #1DA1F2; }
        form { display: flex; flex-direction: column; gap: 15px; margin-bottom: 20px; }
        .form-group { display: flex; flex-direction: column; }
        label { margin-bottom: 5px; font-weight: bold; }
        input[type="text"] { padding: 10px; border: 1px solid #ccc; border-radius: 4px; font-size: 16px; }
        button { padding: 12px 20px; background-color: #1DA1F2; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; }
        button:hover { background-color: #0c85d0; }
        .results { margin-top: 20px; padding: 15px; border: 1px solid #eee; border-radius: 8px; background-color: #fff; }
        .results h4 { margin-top: 0; }
        .results ul { padding-left: 20px; list-style-type: none; }
        .results li { margin-bottom: 15px; padding-bottom: 15px; border-bottom: 1px solid #eee; }
        .results li:last-child { border-bottom: none; }
    </style>
</head>
<body>
    <div class="container">
        <h1>X投稿 取得ツール</h1>
        <p>指定したTwitter IDの直近10件の投稿を検索します。キーワードは任意で、空欄の場合は全件取得します。</p>
        
        <form action="" method="POST">
            <div class="form-group">
                <label for="twitter_id">Twitter ID (必須)</label>
                <input type="text" id="twitter_id" name="twitter_id" placeholder="例: google (@は不要)" value="<?php echo htmlspecialchars($inputTwitterId, ENT_QUOTES, 'UTF-8'); ?>" required>
            </div>
            <div class="form-group">
                <label for="keyword">検索キーワード (任意)</label>
                <input type="text" id="keyword" name="keyword" placeholder="例: #キャンペーン" value="<?php echo htmlspecialchars($inputKeyword, ENT_QUOTES, 'UTF-8'); ?>">
            </div>
            <button type="submit">投稿を検索</button>
        </form>
        
        <?php if (!empty($resultHtml)): ?>
            <div class="results">
                <?php echo $resultHtml; ?>
            </div>
        <?php endif; ?>
    </div>
</body>
</html>
      

主な機能

Web UIによる操作: ブラウザからXのユーザーIDとキーワードを入力して実行します。
キーワード検索: 投稿本文に特定の文字列(ハッシュタグなど)が含まれているかをチェックします。キーワードが空の場合は、ユーザーの直近の投稿を全て取得します。

Xとの連携手順

Bearer Token作成手順参考記事

1.X Developerアカウントの準備
X Developer Portalにアクセスし、開発者アカウントを登録します。すでにアカウントがある場合は不要です。

2.アプリの作成とBearer Tokenの取得
Developer Portal内で新しいアプリ(App)を作成します。
作成したアプリの「Keys and tokens」セクションに移動し、「Bearer Token」を生成・コピーします。

3.スクリプトへの設定
コピーしたBearer Tokenを、PHPスクリプトの以下の箇所に設定します。

// X API のBearer Token
$bearerToken = 'ここに取得したBearer Tokenを貼り付けます';


利用するAPIエンドポイント
https://api.twitter.com/2/users/by/username/{$screenName}

ユーザー名(@google など)から、後続の処理で必要となるユニークな数値IDを取得するために使用します。

https://api.twitter.com/2/users/{$userId}/tweets

取得した数値IDを元に、そのユーザーの直近の投稿を取得するために使用します。

処理内容

ユーザーがフォームを送信してから結果が表示されるまでの、スクリプト内部の処理フローは以下の通りです。

1.ユーザーによる情報入力

利用者はWebブラウザでツールにアクセスし、フォームに「XユーザーID(必須)」と「検索キーワード(任意)」を入力して送信します。

2.ユーザーIDの変換 (getTwitterNumericId関数)

入力されたユーザーID(スクリーンネーム)を元に、X API users/by/username へリクエストを送信します。
APIから返却された、ユーザー固有の数値IDを取得します。
このIDは、特定のユーザーの投稿を取得する際に必須となります。ユーザーが存在しない、または非公開の場合はエラーとなります。

3.投稿データの取得 (fetchLatestTweets関数)

取得した数値IDを使い、X API users/:id/tweets へリクエストを送信します。
対象ユーザーの直近の投稿(最大10件)を、投稿日時(created_at)と共に取得します。

4.キーワードによる絞り込み

取得した最大10件の投稿データをループで1件ずつ評価します。
キーワードが入力されている場合: PHPのstrpos関数を使い、投稿本文(text)にキーワードが含まれているか判定します。含まれている投稿のみを結果として保持します。

キーワードが入力されていない場合: この判定処理をスキップし、取得した全ての投稿を結果として保持します。

5.結果の画面表示

絞り込み後の投稿データをHTMLとして整形し、ブラウザ画面に表示します。
該当する投稿が見つかった場合はその内容と投稿日時をリスト表示し、見つからなかった場合はその旨をメッセージで表示します。

注意事項

このツールを利用する上での注意事項を以下にまとめます。

1.APIの利用制限

X APIには、一定時間内にリクエストできる回数に上限(レートリミット)が定められています。
短時間に多くのユーザーIDを連続して処理すると、API制限に達して一時的に情報を取得できなくなる可能性があります。
大規模なキャンペーンで多数の応募者をチェックする場合は、時間を空けながら少しずつ実行するなどの工夫が必要です。

2.Bearer Tokenの管理について

X APIには、一定時間内にリクエストできる回数に上限(レートリミット)が定められています。
短時間に多くのユーザーIDを連続して処理すると、API制限に達して一時的に情報を取得できなくなる可能性があります。
大規模なキャンペーンで多数の応募者をチェックする場合は、時間を空けながら少しずつ実行するなどの工夫が必要です。

3.検索対象となる投稿の範囲について

このツールは、対象ユーザーの直近10件の投稿のみを取得してチェックします。
応募者がキャンペーンに関する投稿をした後、10件以上新しい投稿をしてしまうと、このツールでは検知できません。
より厳密なチェックが必要な場合は、APIのリクエスト数を増やす(最大100件)か、特定のキーワードで投稿を検索する「Search API」の利用を検討する必要があります。

4.凍結・非公開アカウントの考慮

Xのユーザーがアカウントを非公開(鍵アカウント)に設定している場合や、アカウントが凍結・削除されている場合、API経由で投稿を取得することはできません。
スクリプトは「ユーザーIDの取得に失敗しました」というエラーを表示しますが、その原因が非公開なのか、ユーザー名のタイプミスなのかを区別することはできません。

これにて実装完了です。
ご自身のSPIRAL環境に貼り付けてx連携をお試しください。

解決しない場合はこちら コンテンツに関しての
要望はこちら