ブログ用タグ: X(Twitter)

  • X(Twitter)への投稿字数を調整する

    概要

    LOTD(今日のリスト)を X(Twitter) に投稿する際、140字に切り詰める処理を実装した。

    背景

    これまで外部サービスを使っていたLOTDのXへの自動投稿を、お手製のプログラムに切り替えました。下記のような内容を、メール配信と同じタイミングで投稿します。

    [今日のリスト] 【タイトル】: 【抜粋】
    【パーマリンク】
    【ハッシュタグ化したタグ】

    たとえば「4つの元型(ユング)」であれば次のような感じです。

    [今日のリスト] 4つの元型(ユング): 『元型は、カール・グスタフ・ユングが提唱した概念。世界中の神話や夢や芸術などに普遍的に見られるパターンのこと。元型は、個人的な無意識ではなく、人類に共通する「集合的無意識」から発生するとされる。』
    https://listfreak.com/list/20747
    #ユング #人格 #元型 #無意識 #脳科学

    しかし、これをTwitter APIに送ると403エラーが返ってきます。不親切なことに「字数が長いよ」ではなく「投稿は許されない」という抽象的なエラーメッセージだったので、字数オーバーであること自体に気づくのに時間がかかりました。

    投稿が長い場合には、抜粋で調整しようと思います。たとえば下記のように。

    [今日のリスト] 4つの元型(ユング): 『元型は、カール・グスタフ・ユングが提唱した概念。世界中の神話や夢や芸術などに普遍的に見られるパターンのこと。元型は、個人的な無意識ではなく、人類に共通する「集合的無意識」から発生す…
    https://listfreak.com/list/20747
    #ユング #人格 #元型 #無意識 #脳科学

    内容

    「字数」と「重み付き字数」

    Xでは日本語140字(characters)、英語280字まで投稿できると言われます。これは日本語1字に2字ぶんの「重み」(weight)が付けられているからです。

    なので「あいuえお」の字数(length)は5字ですが、Xの数え方では9字です。ややこしいので、Xの数え方に従った字数を「重み付き字数」と称します。「あいuえお」の字数は5字、「重み付き字数」は9字です。

    全角=2字、半角=1字というわけでもない

    PHPには半角文字を 1、全角文字を 2 として字幅を数える mb_strwidth() や、同じ数え方で指定した字幅で文字列を丸める mb_strimwidth() があります。しかしPHPでは字数 1 と判定される「…」が重み付き字数では2になるなど、字の数え方が X とすこし異なります。

    twitter-text がルールブック

    X の公式サイトによると、ポストの字数制限は twitter-text というオープンソースライブラリで提供しているとのこと。しかし残念ながらPHPのライブラリはありません。そこで構成ファイルだけを借りて軽量なクラスを作りました。

    class LF_Twitter_Text {
    
      // twitter-text configuration
      private static $tt = null;
    
      // 初期化
      public static function init() {}
    
      // 構成内容の取得
      public static function get() {}
    
      // 文字列の重み付き字数を数える
      public static function count_str( $string ) {}
    
      // 文字列の後ろを指定された重み付き字数ぶん切り落とす
      public static function truncate( $string, $length ) {}
    
      // 渡された1文字の重み付き字数を返す
      private static function count_char( $char ) {}
    }

    初期化メソッドは次のようにしました。構成情報をファイルに置くと置き場所を考えるのが面倒だしめったに変わる情報ではないのでハードコード。versionが上がったらJSON部分を入れ替えます。

    public static function init() {
    
      // 重複して呼ばれ得るので、すでに値が入っていればそれを返す
      if ( self::$tt ) {
        return self::$tt;
      }
    
      self::$tt = json_decode( '
    {
      "version": 3,
      "maxWeightedTweetLength": 280,
      "scale": 100,
      "defaultWeight": 200,
      "emojiParsingEnabled": true,
      "transformedURLLength": 23,
      "ranges": [
        {
          "start": 0,
          "end": 4351,
          "weight": 100
        },
        {
          "start": 8192,
          "end": 8205,
          "weight": 100
        },
        {
          "start": 8208,
          "end": 8223,
          "weight": 100
        },
        {
          "start": 8242,
          "end": 8247,
          "weight": 100
        }
      ]
    }'
      );
    }

    字数カウントの核となる、重み付き字数のカウント部分はこのような感じにしました。大部分が日本語、次が英数字などいわゆる半角文字なので、その順番にヒットして処理を終了できるようにちょっと工夫をしました。

    private static function count_char( $char ) {
    
      // UTF-8のコードポイントを取得
      if ( false === $code = mb_ord( $char, 'UTF-8' ) ) {
        return false;
      }
    
      // 大部分を占める日本語をまず判定
      // ranges 配列の最後のメンバーのendとまず比較する
      if ( end( self::$tt->ranges )->end < $code ) {
        return self::$tt->defaultWeight / self::$tt->scale;
      }
    
      // 次に多いと思われるメジャーな半角文字から判定する
      foreach ( self::$tt->ranges as $range ) {
        if ( $range->start <= $code && $code <= $range->end ) {
          return $range->weight / self::$tt->scale;
        }
      }
    
      // 残りは全て2字幅
      return self::$tt->defaultWeight / self::$tt->scale;
    }

    これらのメソッドを使って下記のような感じで投稿する文字列を作成しました。

    // Twitter-Text configuration の内容を取得する
    $tt = LF_Twitter_Text::get();
    
    // パート1: [今日のリスト] タイトル: 抜粋
    $xpost_part1       = '[今日のリスト] '.$post->post_title.': '.$post->post_excerpt;
    $width_part1       = LF_Twitter_Text::count_str( $xpost_part1 );
    
    // パート2: (改行)permalink(改行)ハッシュタグ
    $xpost_part2       = PHP_EOL.$post_url.PHP_EOL.$hash_tags_str;
    // パート2の文字幅を計算する。URLは固定長文字列をダミーとしてあてがう
    $xpost_part2_dummy = PHP_EOL.str_repeat( '-', $tt->transformedURLLength ).PHP_EOL.$hash_tags_str;
    $width_part2       = LF_Twitter_Text::count_str( $xpost_part2_dummy );
    
    // 必要ならパート1の文字数を削る
    if ( 0 < $over_width = $width_part1 + $width_part2 - $tt->maxWeightedTweetLength ) {
      $xpost_part1 = LF_Twitter_Text::truncate( $xpost_part1, $over_width );
    }
    
    // X投稿
    $xpost = $xpost_part1.$xpost_part2;
  • Xに投稿する機能を作る

    概要

    LOTDをX(Twitter)に自動ポストするために使っていたサービスが停止されたので代替機能を作りました。

    経緯

    LOTD(今日のリスト)は、システム的には「lotd というカテゴリに入っている(唯一の)投稿」であり、毎日 0:00 に入れ替えています。このような仕様にしたのは、WordPressがカテゴリごとに RSS フィードを作ってくれるからでした。

    当サイトをWordPressに乗せ換えた当時はフィードをツイートしてくれる無料サービスの選択肢が多かったため、LOTDだけのフィードさえ作れれば、追加の開発なしで LOTD の tweet bot が実現するのは容易でした。

    dlvr.it はそのような配信サービスの一つでした。毎日いい感じに巡回してきてくれて、フィードが更新されていたらツイートしてくれました。Twitter が X になり、ツイートがポストと呼ばれるようになっても、ちゃんと動いてくれました。しかし残念ながら運営者から「1週間後に free plan はおしまいにするよ」というメールが来ました。ずいぶん急な話です。

    いくつかプラグインを検討したのですが、特定のカテゴリの投稿だけを毎日ポストしてくれる軽量プラグインが見あたらず、自作することにしました。

    Twitter APIライブラリのインストール

    PHPでXに投稿するためのライブラリとしては TwitterOAuth が超定番のようです。公式ページによれば composer という依存関係管理ツールで次のようにインストールするのがおすすめとのこと。

    composer require abraham/twitteroauth

    しかしこの方法では 0.5.4 という、かなり古いバージョンの TwitterOAuth がインストールされてしまいます。どうも composer が参照するPHPのバージョンが古く、そのバージョンのPHPで動くバージョンの TwitterOAuth がインストールされている模様。さすが依存関係管理ツール。

    当サイトはレンタルサーバー上で動いているので、/usr/bin の下にはいろいろなバージョンのPHPが混在しています。古いバージョンでも機能すればよいのですが、このバージョンは Twitter API v2 に対応していないようなので、新しい TwitterOAuth をインストールしなければならないようです。

    あれこれ調べた結果、構成ファイル composer.json を以下のように作り、composer install することで、新しいバージョンの TwitterOAuth をインストールできました。

    {
        "config": {
            "platform": {
                "php": "8.2"
            }
        },
        "require": {
            "abraham/twitteroauth": "^7.0"
        }
    }

    実のところもっとも時間がかかったのは、この新しいバージョンの TwitterOAuth をインストールする作業でした。いったん環境が整えば、次のようにかなり少ない手数でポストできます。

    // TwitterOAuthインスタンスの作成
    $X = new TwitterOAuth(
      '(API Key)', 
      '(API Key Secret)',
      '(Access Token)',
      '(Access Token Secret)'
    );
    
    // API v2 を指定
    $X->setApiVersion( '2' );
    
    // 投稿内容
    $post = '[今日のリスト] ';
    
    // ツイートを投稿
    $response = $X->post( 'tweets', [ 'text' => $post ] );
    
    // 成功なら 201 (Created) が返ってくる
    if ($connection->getLastHttpCode() !== 201) {
    	// エラー処理
    }

    この処理を、LOTDのメール配信の次に実施することにしました。投稿内容もRSSフィードの転送でなくゼロから組み立てるので、リストに付けたタグをXのハッシュタグにするなど、工夫もしやすくなりました。

    [lf_load_syntax_highlighter]