跳到正文

PHP 版 Twitter API:用 cURL、Guzzle 和 Laravel 获取 X 数据

在 PHP 中获取 Twitter/X 数据,用 cURL 或 Guzzle 发送 HTTPS 请求,把 API 密钥放在请求头里,解码 JSON,再沿着游标逐页读取;官方 X API 每读取一条帖子收费 $0.005。

  • 更新于
  • 发布于
  • 阅读约 12 分钟

简短回答: X 只提供 Python 和 TypeScript 的官方 SDK,所以 PHP 要通过 TwitterOAuth 这样的社区客户端访问官方 X API,并且需要开发者账号和预付额度:每条帖子 $0.005,从 2026 年 11 月 22 日起连作者一起读取是 $0.015。读取公开数据可以用 Sorsa API:一个密钥,每次请求 20 条推文,每 1,000 条 $0.07 起;100 次免费请求可以获取 2,000 条。

接口
POST /user-tweets
每页条数
每次请求 20 条推文
价格
每 1,000 条推文 $0.07 起

#X API 有官方的 PHP SDK 吗?

没有。X 只发布了 Python 和 TypeScript 的官方 SDK,它的工具页面列出了三个 X 并不维护的 PHP 社区客户端:twitter-api-v2-php、bird-elephant 和 TwitterOAuth(见 X 的工具和库页面(在新标签页打开))。三者中 TwitterOAuth 安装量最大,每月约 24 万次 Composer 安装。它在 2026 年 11 月 1 日发布的 9.0.0 版只支持 v2 API,要求 PHP 8.3 或更高版本。

这三个都是官方 X API 的客户端,所以它们发出的每个调用都需要开发者账号、应用的密钥和预付额度。新开发者没有免费额度,X 对响应里返回的每个对象计费:每条帖子 $0.005,每个用户 $0.010,每月最多读取 300 万条帖子(见 X 的价格表(在新标签页打开))。从 2026 年 11 月 22 日起,通过扩展字段带出的作者和媒体按相同费率计费(见 X 在 10 月 23 日发布的通知(在新标签页打开)),所以常见的 TwitterOAuth 搜索会为附带的每个作者付费:

TwitterOAuth
use Abraham\TwitterOAuth\TwitterOAuth;

$x = new TwitterOAuth($apiKey, $apiSecret, null, $bearerToken);
$posts = $x->get('tweets/search/recent', [
    'query' => 'laravel lang:en',
    'max_results' => 100,
    'expansions' => 'author_id',
]);
// 100 posts: $0.50. From Nov 22, 2026, with authors: up to $1.50

旧的回答会把你引向更早的东西。Google 对这个搜索的第一个结果是 J7mbo/twitter-api-php,它是 v1.1 调用的封装,GitHub 仓库已归档,最后一次改动是在 2020 年 4 月。官方路线当前的所有费率、上限和日期都在 X API 价格一文里。

官方路线在第一次调用前需要什么

  • 在 console.x.com 开通的开发者账号,用 X 账号开通,并同意 X 的开发者协议。
  • 一个应用,以及它的 API 密钥、API 密钥密文和 bearer 令牌,X 只显示一次(见 X 的接入指南(在新标签页打开))。
  • 提前购买的额度,按返回的每条帖子和每个用户扣费。
  • 发帖等操作需要的 OAuth 1.0a 用户令牌,也就是访问令牌及其密文;TwitterOAuth 的四参数构造函数接受它们。

读取公开帖子和用户资料用不到这些。下一节用一个密钥和多数 PHP 构建已内置的 cURL 扩展来完成读取。

#用一个 API 密钥在 PHP 中获取推文

要从 PHP 读取 X 数据,可以用 Sorsa API,一款 Twitter(X)API 与爬虫:用一个 ApiKey 请求头代替 OAuth,所有套餐(包括免费账号)都是每秒 20 次请求,时间线和搜索每 1,000 条推文 $0.07 起。每个响应都是普通 JSON,json_decode 会把它转成数组,每条推文里都带着作者的完整资料。它从不要求登录 X,所以你的 X 账号不会有任何风险。

  1. 获取密钥。 注册后,在控制台复制你的 API 密钥。前 100 次请求免费,无需绑卡,永不过期。

  2. 别把密钥写进代码。 把它设为环境变量(Laravel 则放进 .env),并且只在服务端 PHP 里调用 API:放在浏览器 JavaScript 里的密钥,每个访客都能看到。

    Terminal
    export SORSA_API_KEY="your_key"  # PowerShell: $env:SORSA_API_KEY="your_key"
    
  3. 获取某个账号的最新推文。 把用户名发送到 POST /user-tweets(在新标签页打开),密钥放在 ApiKey 请求头里。先在终端里试一下:

    Terminal
    curl -X POST "https://api.sorsa.io/v3/user-tweets" \
      -H "ApiKey: YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"username": "php_release_notes"}'
    
  4. 从 PHP 调用。 直接用 cURL 不需要任何 Composer 包。用 Guzzle 时,base_uri 要以斜杠结尾,路径不要以斜杠开头,否则 /v3 这一段会被丢掉。

    <?php
    $key = getenv('SORSA_API_KEY') ?: 'YOUR_API_KEY';
    
    $ch = curl_init('https://api.sorsa.io/v3/user-tweets');
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => [
            'ApiKey: ' . $key,
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS => json_encode(['username' => 'php_release_notes']),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 30,
    ]);
    $body = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    
    $data = json_decode($body, true, flags: JSON_THROW_ON_ERROR);
    if ($status !== 200) {
        exit("HTTP $status: {$data['message']}\n");
    }
    foreach ($data['tweets'] as $tweet) {
        echo $tweet['created_at'], ' ', mb_substr($tweet['full_text'], 0, 60), "\n";
    }
    
  5. 读取这一页。 每个响应最多包含 20 条推文,后面还有数据时会带上 next_cursor。下面的值是按文档格式给出的演示数据。

    Response200 OK
    {
      "tweets": [
        {
          "id": "1985432176540981234",
          "full_text": "PHP 8.5.12 is out with two security fixes. Upgrade today.",
          "created_at": "2026-11-01T18:42:07Z",
          "lang": "en",
          "likes_count": 42,
          "retweet_count": 7,
          "reply_count": 3,
          "quote_count": 1,
          "view_count": 3810,
          "bookmark_count": 5,
          "is_reply": false,
          "user": {
            "id": "1592041833471827968",
            "username": "php_release_notes",
            "display_name": "PHP Release Notes",
            "followers_count": 1840,
            "verified": false
          }
        }
      ],
      "next_cursor": "DAABCgABF7Y8kQ3wAAA"
    }
    

每条推文都带着各项计数和作者,所以 PHP 脚本不需要设置扩展字段或字段清单。下面是多数脚本会读取的字段:

字段含义
id推文 ID:19 位数字,以字符串形式发送
full_text帖子的完整文本
created_atISO 8601 格式的 UTC 时间,可直接交给 new DateTimeImmutable()
likes_count, retweet_count, reply_count, quote_count互动数
view_count, bookmark_count浏览数和收藏数
user作者的资料:username、followers_count 等 19 个字段
quoted_status, retweeted_status被引用或被转发的推文,结构相同
  • id

    含义
    推文 ID:19 位数字,以字符串形式发送
  • full_text

    含义
    帖子的完整文本
  • created_at

    含义
    ISO 8601 格式的 UTC 时间,可直接交给 new DateTimeImmutable()
  • likes_count, retweet_count, reply_count, quote_count

    含义
    互动数
  • view_count, bookmark_count

    含义
    浏览数和收藏数
  • user

    含义
    作者的资料:username、followers_count 等 19 个字段
  • quoted_status, retweeted_status

    含义
    被引用或被转发的推文,结构相同

#带重试的统一请求函数

让每次调用都经过同一个函数,只重试重试能解决的情况。429 表示你的密钥在一秒内发起了超过 20 次请求,5xx 或连接中断则是临时故障;两者都会自行恢复。sorsa() 最多重试四次,分别等待 1、2、4 和 8 秒,并把 GET 参数以重复键的形式发送,这正是后面的批量接口读取的格式。本指南后面的内容都建立在它之上。

sorsa.php
<?php
const SORSA_API = 'https://api.sorsa.io/v3';
const RETRY = [429, 500, 502, 503, 504];

/** Build a query string; a list becomes a repeated key: a=1&a=2. */
function query(array $params): string
{
    $parts = [];
    foreach ($params as $name => $value) {
        foreach ((array) $value as $one) {
            $parts[] = rawurlencode($name) . '=' . rawurlencode($one);
        }
    }
    return implode('&', $parts);
}

/** Send one request; retry 429, 5xx and network errors up to 4 times. */
function sorsa(string $method, string $path, array $params = []): array
{
    $url = SORSA_API . $path;
    $headers = ['ApiKey: ' . (getenv('SORSA_API_KEY') ?: 'YOUR_API_KEY')];
    $options = [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30];

    if ($method === 'GET') {
        $url .= $params ? '?' . query($params) : '';
    } else {
        $headers[] = 'Content-Type: application/json';
        $options[CURLOPT_POSTFIELDS] = json_encode($params);
    }
    $options[CURLOPT_HTTPHEADER] = $headers;

    for ($attempt = 0; ; $attempt++) {
        $ch = curl_init($url);
        curl_setopt_array($ch, $options);
        $raw = curl_exec($ch);
        $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); // 0: no answer
        $again = in_array($status, [0, ...RETRY], true);
        if (!$again || $attempt === 4) {
            break;
        }
        sleep(2 ** $attempt); // 1, 2, 4, 8 seconds
    }

    if ($raw === false) {
        throw new RuntimeException('Network error: ' . curl_error($ch));
    }
    $data = json_decode($raw, true, flags: JSON_THROW_ON_ERROR);
    if ($status !== 200) {
        throw new RuntimeException("HTTP $status: " . ($data['message'] ?? ''));
    }
    return $data;
}

401 或 403 永远不值得重试:说明密钥有误或缺失,或者额度已用完,message 字段会写明是哪种。失败的请求不会计费,所以重试不花钱;Sorsa API 的所有接口都只读,因此重复发送 POST 不会重复创建任何内容。这个函数可在 PHP 8.0 及更高版本上运行。它没有调用 curl_close(),因为从 PHP 8.0 起这个函数已经没有任何作用,cURL 句柄现在会自动关闭。

#用 next_cursor 翻页读取结果

游标是通往下一页结果的不透明键:把 next_cursor 原样传回去,响应里没有它时就停止。时间线和 POST /search-tweets(在新标签页打开) 每页最多返回 20 条推文,而且一页可能少于 20 条但后面仍有数据,所以不能靠条数判断是否已到末尾。生成器每次交出一页,不管任务跑多久,内存占用都保持平稳。

PHP
require 'sorsa.php';

/** Yield pages of a POST endpoint until the cursor runs out. */
function pages(string $path, array $body, int $maxPages = 100): Generator
{
    for ($i = 0; $i < $maxPages; $i++) {
        $page = sorsa('POST', $path, $body);
        yield $page;
        if (empty($page['next_cursor'])) {
            return;
        }
        $body['next_cursor'] = $page['next_cursor'];
    }
}

foreach (pages('/search-tweets', ['query' => 'laravel lang:en']) as $page) {
    foreach ($page['tweets'] as $tweet) {
        echo $tweet['id'], ' @', $tweet['user']['username'], "\n";
    }
}

同一个游标在运行期间要保持查询和 order 不变,并把 $maxPages 当作预算:100 页就是 100 次请求,最多 2,000 条推文。GET 接口则把游标放在查询字符串里。GET /followers(在新标签页打开) 每页最多返回 200 份用户资料:

PHP
$params = ['username' => 'php_release_notes'];
$followers = [];
do {
    $page = sorsa('GET', '/followers', $params);
    array_push($followers, ...$page['users']);
    $params['next_cursor'] = $page['next_cursor'] ?? null;
} while ($params['next_cursor']);

有两个任务各有完整的脚本:下载某个用户的全部推文可以从保存的游标续传整条时间线,导出 Twitter 粉丝的指南则把粉丝列表写入 CSV。

#每次请求获取 100 条推文或用户资料

如果你已经有推文 ID 或用户名,就分批发送。POST /tweet-info-bulk(在新标签页打开) 最多接受 100 个推文 ID 或链接,GET /info-batch(在新标签页打开) 最多接受 100 个用户名。批量响应不带游标:一个响应就包含整批数据。

PHP
$tweets = [];
foreach (array_chunk($tweetIds, 100) as $batch) {
    $res = sorsa('POST', '/tweet-info-bulk', ['tweet_links' => $batch]);
    array_push($tweets, ...$res['tweets']);
}

$profiles = [];
foreach (array_chunk($usernames, 100) as $batch) {
    $res = sorsa('GET', '/info-batch', ['usernames' => $batch]);
    array_push($profiles, ...$res['users']);
}

一次请求的价格与返回内容的多少无关,所以一批 100 条推文的价格和一条推文一样:每 1,000 条推文 $0.014 起,比官方 X API 的 $5.00 便宜 250 多倍。用这种方式,100 次免费请求就能覆盖 1 万条推文。

须知

GET /info-batch 把列表读作重复键,即 usernames=a&usernames=b,这是它的 OpenAPI 参考文档定义的格式。PHP 的 http_build_query() 写出的却是 usernames%5B0%5D=a&usernames%5B1%5D=b,Guzzle 的数组 query 选项和 Laravel 的 Http::get($url, $array) 也一样,因为它们构造查询的方式相同。请使用上面的 query() 辅助函数,或者会写出重复键的 GuzzleHttp\Psr7\Query::build()。

$params = ['usernames' => ['php_release_notes', 'laravel_weekly_digest']]; 方括号命名 http_build_query($params),或 Guzzle 的 query 选项 usernames%5B0%5D=php_release_notes &usernames%5B1%5D=laravel_weekly_digest 两个名为 usernames[0] 和 usernames[1] 的参数,是 PHP 自己的命名习惯 重复键 query($params),或 GuzzleHttp\Psr7\Query::build($params) usernames=php_release_notes &usernames=laravel_weekly_digest 一个参数 usernames,带两个值:GET /info-batch 规定的形式
PHP 的默认写法会生成带方括号的名称;批量接口读取的是重复出现的同一个名称,所以要自己构造这个查询字符串。

#以每秒 19 次请求并行跑批量任务

PHP 用 curl_multi 并行发送请求,只要工作能拆成相互独立的请求,一个进程就能达到 Sorsa API 每个密钥每秒 20 次请求的上限。批量请求可以干净地拆开,因为每一批都是独立的。游标链则无法拆分,因为每一页都需要上一页的游标,所以改为让不同的账号或不同的日期窗口并行运行。parallel() 每秒最多发起 19 个请求,比上限低一个,并把遇到 429 或 5xx 的请求重新排回队列。

PHP
/** POST many bodies side by side, starting at most $rate a second. */
function parallel(string $path, array $bodies, float $rate = 19): array
{
    $multi = curl_multi_init();
    $queue = array_keys($bodies);
    $tries = $results = [];
    $next = microtime(true);
    $running = 0;

    while ($queue || $running) {
        if ($queue && microtime(true) >= $next) {
            $key = array_shift($queue);
            $ch = curl_init(SORSA_API . $path);
            curl_setopt_array($ch, [
                CURLOPT_HTTPHEADER => [
                    'ApiKey: ' . (getenv('SORSA_API_KEY') ?: 'YOUR_API_KEY'),
                    'Content-Type: application/json',
                ],
                CURLOPT_POSTFIELDS => json_encode($bodies[$key]),
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_TIMEOUT => 30,
                CURLOPT_PRIVATE => (string) $key,
            ]);
            curl_multi_add_handle($multi, $ch);
            $next = max($next, microtime(true) - 1 / $rate) + 1 / $rate;
        }
        curl_multi_exec($multi, $running);
        while ($done = curl_multi_info_read($multi)) {
            $ch = $done['handle'];
            $key = curl_getinfo($ch, CURLINFO_PRIVATE);
            $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
            $tries[$key] = ($tries[$key] ?? 0) + 1;
            if (in_array($status, [0, ...RETRY], true) && $tries[$key] < 5) {
                $queue[] = $key; // back in line, it waits its turn
            } else {
                $results[$key] = json_decode(curl_multi_getcontent($ch), true);
            }
            curl_multi_remove_handle($multi, $ch);
        }
        curl_multi_select($multi, 0.01);
    }
    return $results;
}

$bodies = array_map(fn ($batch) => ['tweet_links' => $batch],
    array_chunk($tweetIds, 100));
$tweets = array_merge(...array_column(parallel('/tweet-info-bulk', $bodies),
    'tweets'));
parallel('/tweet-info-bulk', $bodies) 100 批,每批 100 个推文 ID 同一 API 密钥上的一秒 每根条是一个在途请求 0 s 1 s 19 每秒启动 19 次, 比限制低一次, 即每密钥 20 次 每秒 1,900 条推文 19 批,每批 100 个 ID 1 万条推文约 5 秒 100 次请求,每秒启动 19 次 429 或 5xx:重新排队 等待空闲的启动名额 循环只在轮到启动时间时才加入新请求,不论之前的响应用了多久。
发起时间按固定节拍走,所以响应快慢都不会让密钥超过每秒 20 次请求。

按每秒 19 次发起,100 个批量请求约 5 秒就能处理 1 万条推文,也就是每秒 1,900 条。每个密钥只保留一个 parallel() 循环:两个进程同时运行它,合起来的发起速率会是上限的两倍。Guzzle 的 Pool 看起来适合做这件事,但它的 concurrency 选项限制的是同时在途的请求数,而不是每秒的发起次数,所以一池响应很快的请求仍可能超过每秒 20 次,靠重试来兜底。

#Laravel:用 Http 客户端发同样的调用

Laravel 的 Http 客户端封装了 Guzzle,所以 Laravel 应用不需要额外的包。一个宏就能设好基础 URL、密钥和一条只对 429、5xx 响应和连接中断重试的规则;之后每个控制器、任务或 Artisan 命令都调用 Http::sorsa()。

// config/services.php: 'sorsa' => ['key' => env('SORSA_API_KEY')],
// .env: SORSA_API_KEY=YOUR_API_KEY

// app/Providers/AppServiceProvider.php
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;

// inside the class
public function boot(): void
{
    Http::macro('sorsa', fn () => Http::baseUrl('https://api.sorsa.io/v3')
        ->withHeaders(['ApiKey' => config('services.sorsa.key')])
        ->timeout(30)
        ->retry(5, fn (int $attempt) => 1000 * 2 ** ($attempt - 1),
            fn (Throwable $e) => $e instanceof ConnectionException
                || ($e instanceof RequestException
                    && ($e->response->status() === 429
                        || $e->response->serverError()))));
}

retry() 在两次尝试之间分别等待 1、2、4 和 8 秒,次数用完或状态码不值得重试(例如 401 或 403)时抛出 RequestException,所以失败的调用永远不会进入缓存。耗时长的任务,比如几千个批量请求,应当放在队列任务或 Artisan 命令里运行,而不是放在页面请求里。

缓存的信息流按时钟消耗请求,而不是按访客。下面的规划器会把信息流的数量和缓存时间换算成每天和每月的请求数,以及能容纳它们的 Sorsa API 套餐。

网站上的订阅源数量(账号或搜索)
每个订阅源缓存
每天请求数
288

与流量无关

每月请求数
8,640

30 天

能容纳的 Sorsa API 套餐
Starter

10K 次请求/月,已用 86%

100 次免费请求可用
约 8 h

无需绑卡,永不过期

同一个 5 分钟内的访客读到的都是缓存副本,所以 100 次和 1M 次页面浏览消耗的请求数相同,都是每天 288 次请求。

请求数取决于缓存刷新的频率,与页面浏览量无关。

#一个 PHP 任务要花多少钱

Sorsa API 按请求收费,与接口返回多少内容无关:时间线和搜索每 1,000 条推文 $0.07 起,批量请求每 1,000 条 $0.014 起,失败的请求不计费。官方 X API 则按对象收费:每 1,000 条帖子 $5.00,从 2026 年 11 月 22 日起,如果代码扩展出每条帖子的作者(大多数 TwitterOAuth 搜索都是这样),每 1,000 条最高 $15.00。

你的 PHP 代码采集的推文数

官方 X API

$5,000

采集 100 万 条推文

每条帖子 $0.005

Sorsa API

$70 起

采集 100 万 条推文

每 1,000 条 $0.07 起

作者资料就在每条推文里

每 1,000 次请求的费率从 Starter 的 $4.90 到 Hyperscale 的 $1.40 不等,每档套餐对所有接口一视同仁。查看剩余额度不花钱:GET /key-usage-info(在新标签页打开) 永远不会计入你的请求次数。

PHP
$usage = sorsa('GET', '/key-usage-info');
echo $usage['remaining_requests'], ' left until ', $usage['valid_until'], "\n";

#把 TwitterOAuth 代码迁移到 Sorsa API

TwitterOAuth 的大多数读取调用都对应 Sorsa API 的一个接口,而且代码不再需要扩展字段和字段清单,因为每条推文已经带有作者和各项计数。下表把常用的 v2 路径和对应接口配成了对;如果要迁移整个代码库,从 Twitter/X API 迁移的指南列出了每个 v2 接口和响应字段的对应关系。

TwitterOAuth 调用Sorsa API 接口有什么变化
get('tweets/search/recent')POST /search-tweets一个接口同时覆盖近期搜索和归档搜索,可回溯到 2006 年
get("users/$id/tweets")POST /user-tweets只需用户名,也没有 3,200 条帖子的上限
get('tweets'),带 idsPOST /tweet-info-bulk每次请求 100 个 ID 或链接
get('users/by'),带 usernamesGET /info-batch每次请求 100 个用户名
get("users/$id/followers")GET /followers每页 200 个粉丝
post('tweets')无只读:发帖请走官方 X API
  • get('tweets/search/recent')

    Sorsa API 接口
    POST /search-tweets
    有什么变化
    一个接口同时覆盖近期搜索和归档搜索,可回溯到 2006 年
  • get("users/$id/tweets")

    Sorsa API 接口
    POST /user-tweets
    有什么变化
    只需用户名,也没有 3,200 条帖子的上限
  • get('tweets'),带 ids

    Sorsa API 接口
    POST /tweet-info-bulk
    有什么变化
    每次请求 100 个 ID 或链接
  • get('users/by'),带 usernames

    Sorsa API 接口
    GET /info-batch
    有什么变化
    每次请求 100 个用户名
  • get("users/$id/followers")

    Sorsa API 接口
    GET /followers
    有什么变化
    每页 200 个粉丝
  • post('tweets')

    Sorsa API 接口
    无
    有什么变化
    只读:发帖请走官方 X API

字段名也会变:text 变成 full_text,public_metrics->like_count 变成 likes_count,author_id 加 includes->users 变成 user 数组,meta->next_token 变成 next_cursor。TwitterOAuth 返回的是对象,而 json_decode($body, true) 给出的是本指南使用的数组。用户时间线还能越过最新 3,200 条帖子的界限,而官方时间线到这里就停了(在新标签页打开)。

#今天就发出你的第一个 PHP 请求

用 PHP 读取 X 数据只需要 cURL 扩展、一个密钥和四个简短的函数:负责重试的 sorsa()、负责游标的 pages()、配合批量接口实现每次请求 100 条的 array_chunk(),以及实现每秒 19 次请求的 parallel()。建议从 Sorsa API 开始,它是专为大规模读取 X 公开数据而打造的 Twitter API:时间线或搜索每次请求 20 条推文,每 1,000 条 $0.07 起,批量每次 100 条,每 1,000 条 $0.014 起,另有 100 次无需绑卡、永不过期的免费请求。同样的调用在其他语言中的写法,见 Python 版 Twitter API和 Node.js 版 Twitter API指南。

#常见问题

PHP 应该用哪个 Twitter API?

用 PHP 读取 X 数据,建议用 Sorsa API:用 ApiKey 请求头里的一个密钥代替 OAuth,返回可直接交给 json_decode 的普通 JSON,时间线和搜索每 1,000 条推文 $0.07 起,批量请求每 1,000 条 $0.014 起,所有套餐都是每秒 20 次请求,搜索可回溯到 2006 年,而且无需登录 X,你的账号不会有风险。如果还要发帖,可以搭配官方 X API。

PHP 有免费的 Twitter API 吗?

官方 X API 对新开发者没有免费额度:TwitterOAuth 本身免费,但每次读取都要消耗预付额度,每条帖子 $0.005。Sorsa API 为每个账号送 100 次免费请求,永不过期,无需绑卡。从 PHP 调用时,它们可以通过时间线或搜索获取 2,000 条推文,用每批 100 条的批量请求则最多能获取 1 万条。完整的情况见 Twitter API 免费吗。

Twitter(X)用 PHP 吗?

不用。Twitter 最初基于 Ruby on Rails 上线,2010 年起把后端迁到了 JVM,服务用 Scala 和 Java 编写,详见 Twitter 工程博客(在新标签页打开)。X 在 2023 年以及 2026 年 1 月再次公开的推荐系统代码主要是 Scala、Java、Rust 和 Python,完全没有 PHP。不过 PHP 作为客户端依然很好用:X 数据会以 JSON 的形式通过 HTTPS 送到 PHP 脚本里。

能用 Sorsa API 在 PHP 里发推文吗?

不能。Sorsa API 从产品设计上就是只读:它读取公开的帖子、用户资料和粉丝,从不发帖、点赞、关注或发送消息,所以一个密钥无法对任何 X 账号执行操作。要在 PHP 里发帖,请使用带 OAuth 1.0a 用户令牌的官方 X API,例如 TwitterOAuth 的 post('tweets', ...),每条帖子 $0.015,而读取仍交给 Sorsa API。

能在 WordPress 里使用 Sorsa API 吗?

可以。WordPress 自带的 HTTP 函数就能发出同样的请求:用 wp_remote_post() 带上 ApiKey 请求头和 JSON 请求体,再用 wp_remote_retrieve_body() 和 json_decode() 处理结果。用 set_transient() 把结果保存几分钟,这样页面浏览不会变成请求;密钥放在 wp-config.php 或环境变量里,千万不要放在浏览器会加载的脚本里。

Sorsa API 有 Composer 包吗?

不需要包:Sorsa API 是普通的 REST 加 JSON,所以 cURL 扩展、Guzzle 或 Laravel 的 Http 客户端就能覆盖全部 45 个接口,本指南里的函数加起来约 100 行。api.sorsa.io/v3/swagger.json 上的 OpenAPI 参考文档列出了每个接口,所以如果你想要带类型的 PHP 客户端,可以用 openapi-generator 这样的生成器来生成。

#我们如何核实

我们根据 Sorsa API 线上的 OpenAPI 参考文档(45 个接口)以及它关于速率限制和分页的文档编写了本指南里的每个请求,阅读日期为 2026 年 11 月 2 日。这一轮我们没有 Sorsa API 密钥,所以展示的响应使用的是演示值。我们在 PHP 8.4.27、Guzzle 8.3.1 和 Laravel 的 illuminate/http 13 上,对着一个按文档格式应答、并且每秒超过 20 次请求就返回 429 的本地模拟服务运行了每个 PHP 代码块:最初的几个请求、重试、两种游标循环、每批 100 条的批量请求、查询字符串、Laravel 宏,以及 parallel()。后者以每秒 19 次的速度发出了 100 个批量请求,每轮最多出现一次 429,并且都由它的重试恢复。同一天,我们还读了 X 的价格表、工具页面、时间线文档和扩展字段通知,以及 Packagist 上 TwitterOAuth、Guzzle 和 Laravel 的发布版本。

Sorsa API 团队撰写。 我们开发和运营 Sorsa API,这是只读的 Twitter/X API 与爬虫,2022 年起运行于生产环境(前身为 TweetScout API),累计处理请求超 60 亿次。 关于 Sorsa API

更新内容
  • 从头重写:更新为官方 X API 当前价格(含 11 月 22 日起生效的扩展字段计费)、TwitterOAuth 9.0.0,并给出只用一个 API 密钥的 PHP 方案:cURL 和 Guzzle 请求、重试、游标、用重复查询键发送的每批 100 条的批量请求、用 curl_multi 实现的每秒 19 次并行请求、带缓存的 Laravel Http 客户端,以及 TwitterOAuth 方法对照表。
  • 首次发布。