ユーザーから「Xの投稿のURL」を受け取って処理する機能を作ると、最初に躓くのがURLの表記ゆれです。同じ投稿を指していても、貼られてくる文字列は何種類もあります。
パーサーを素直に書くと、テストで使った1パターンだけ通って、実際に使われ始めた途端に「このURLは対応していません」を連発します。
実際に貼られてくるパターン
手元で見かけた範囲でも、これだけあります。
https://x.com/user/status/1234567890123456789
https://twitter.com/user/status/1234567890123456789
https://mobile.twitter.com/user/status/1234567890123456789
https://x.com/user/status/1234567890123456789?s=20&t=AbCdEf
https://x.com/i/status/1234567890123456789
https://x.com/i/web/status/1234567890123456789
https://x.com/user/status/1234567890123456789/photo/1
https://x.com/user/status/1234567890123456789/video/1
ドメインが2種類、サブドメイン付きがあり、共有時に付くクエリパラメータがあり、ユーザー名の代わりに i が入る形式があり、末尾にメディアの位置を示すパスが付くこともあります。
さらに、モバイルアプリの共有からコピーすると前後に改行や説明文がくっついてくることもあります。貼り付け欄に入ってくるのは「URLだけ」とは限りません。
結局必要なのは投稿IDひとつ
ここで方針を決めると楽になります。このURLから欲しいのは投稿IDだけです。ドメインもユーザー名もクエリも、IDを取り出すための飾りでしかありません。
ユーザー名は特にあてになりません。i に置き換わることがあるうえ、そもそも投稿者がハンドルを変更すれば古いURLのユーザー名部分は実体と一致しなくなります。それでもIDが同じなら同じ投稿を指します。
つまり正規化の仕事は「URLをきれいにすること」ではなく、入力から19桁前後の数値IDを1つ確実に取り出すことです。
function extractPostId(input) {
const m = String(input).match(/(?:x|twitter)\.com\/(?:i\/(?:web\/)?)?[^/]+\/status(?:es)?\/(\d+)/);
return m ? m[1] : null;
}
これで上のパターンは全部通ります。statuses も許しているのは古いURLが残っているためです。クエリも末尾の /photo/1 も、マッチ対象の外にあるので勝手に無視されます。
やりすぎないほうがいい正規化
逆に、やらないほうがいいこともあります。
短縮URLを自前で展開しない。 t.co が挟まっている場合に、サーバー側で先にリダイレクトを追いかけて実体を得たくなりますが、ユーザーの入力を起点に任意のURLへリクエストを飛ばす処理になります。入口を絞るか、素直に対応外として返すほうが安全です。
IDの見た目で弾かない。 桁数で厳密にチェックしたくなりますが、IDの桁は将来増えます。数値であること以上の制約を入れると、いずれ正しいURLを弾く側に回ります。
エラーは3つに分ける
実装上いちばん効くのはこれでした。失敗を全部「無効なURLです」にまとめないことです。
- URLとして解釈できなかった — 別サイトのURL、ただの文章、空欄
- 投稿は特定できたが、対象が取得できない — 削除済み、非公開アカウント
- 投稿は取得できたが、メディアが含まれていない — テキストだけの投稿
1番目はユーザーの貼り間違いなので貼り直してもらえば解決します。2番目は何度貼り直しても解決しません。3番目はそもそも取りに行く対象がありません。同じメッセージで返すと、ユーザーは2番目と3番目でも貼り直しを繰り返すことになります。
実際のツールでの扱い
私が公開しているX Video Downloaderでも、入力の扱いはこの方針です。x.com と twitter.com の両方を受け付け、共有時に付くクエリや末尾のメディアパスは無視して投稿IDだけを見ます。取得できるのは公開されている投稿だけで、非公開アカウントや削除済みの投稿は対象外です。Xとは無関係の個人プロジェクトです。
まとめ
URLの表記ゆれは「後で対応する」と思っていると、問い合わせの形でまとめて返ってきます。入力から取り出したいものをIDひとつに絞ってしまえば、パターンが増えても正規表現の外側の話になるので、あとが楽になります。