各局皆様、こんにちは。アマチュア無線局、JS2IIUです。
以前の記事(過去記事:Twitter APIのテスト(Postmanを使って))で試したTwitter APIを使って、特定のキーワードに関連するツイートを検索したいと思います。最初はVisual Basicで作ろうとしたのですが断念してC#に乗り換えました。
- 前提
- Visual Studio 2022
- WPFアプリ、C#でつくる
- Twitter APIが使える状態になっている(過去記事参照:Twitter API V2 を使えるようにする)
目次
アプリ完成イメージ
アプリのイメージはこのような感じです。画面下のほうにキーワード入力用のテキストボックスがあり、その入力内容に関連するツイートを検索します。Twitterと書いてあるボタンを押すと実際にTwitter APIにリクエストを送ります。レスポンスが返ってきたら画面中央のテキストボックスに表示します。(図はデバッグ中の画面)
API
APIの仕様についてはTwitter APIの公式ドキュメントか、Postmanに登録されているTwitter Public workspaceを参考にすると早いです。プログラムを書く前にエンドポイントやクエリストリングに関する情報を仕入れておきます。以下の内容をHttpClientを使って実現します。
https://api.twitter.com/2/tweets/search/recent?query=has:media KTWR&max_results=50&tweet.fields=author_id&expansions=attachments.media_keys&media.fields=preview_image_url&user.fields=name
HttpClientのヘッダ情報
Twitter API へアクセスするには、AutorizationとUser-Agentのヘッダが必要なので、以下のようにヘッダを指定します。Bearer Tokenを何らかの方法で読み込む必要があります。いけないやり方ですが、別の場所で文字列代入してます。
using System.Net.Http;
var client = new HttpClient();
// HttpClient Header
client.DefaultRequestHeaders.Add("Authorization", $"BEARER {BEARER_TOKEN}");
client.DefaultRequestHeaders.Add("User-Agent", "C# App");
クエリストリング
URLの?以降の部分がクエリストリング(Query String)です。Dictionaryクラスを使って必要なKEY、VALUEを入れます。一番目の項目queryのVALUEに入っているQS_Searchは、キーワードのテキストボックスの値を読んで、検索のキーワードとする、その文字列変数です。中身はhas:media KTWRのような内容です。
// Query String
var queryString = new Dictionary<string, string>()
{
// { "query", "has:media KTWR" },
{ "query", QS_Search },
{ "tweet.fields", "author_id" },
{ "expansions", "attachments.media_keys" },
{ "media.fields", "preview_image_url" },
{ "user.fields", "name" },
{ "max_results", "50" }
};
Dictionaryを実際のクエリストリングの形式に変換するのはFormUrlEncodeContent().ReadAsStringAsync()を使います。
var content = await new FormUrlEncodedContent(queryString).ReadAsStringAsync();
検索結果が多い場合の対応方法
クエリストリングの最後にある項目max_resultsは、一度のリクエストで何件のツイートを取り込むかを指示するためのKEYです。Twitter APIのドキュメントでは、デフォルトが10、最大100を指定できるとされています。max_resultsの値を超えてツイートが検索された場合は、next_tokenが返ってきます。続きを検索したい場合には、クエリストリングにKEY: “next_token”、VALUEに得られたnext_tokenのVALUEで追加すればOKです。
Every pagination implementation will involve parsing next_tokens from the response payload, and including them in the ‘next page’ search request. See below for more details on how to construct these ‘next page’ requests.
https://developer.twitter.com/en/docs/twitter-api/tweets/search/integrate/paginate
To retrieve the next 10 Tweets, this next_token is added to the original request. The request would be:
https://api.twitter.com/2/tweets/search/recent?query=snow&next_token=b26v89c19zqg8o3fobd8v73egzbdt3qao235oql
The process of looking for a next_token and including it in a subsequent request can be repeated until all (or some number of) Tweets are collected, or until a specified number of requests have been made. If data fidelity (collecting all matches of your query) is key to your use case, a simple “repeat until request.next_token is null” design will suffice.
https://developer.twitter.com/en/docs/twitter-api/tweets/search/integrate/paginate
HttpClientからのAPIアクセス
HttpClient.GetAsync メソッドを使います。Microsoftのドキュメントが参考になりました。
GetAsync(String) | 指定された URI に GET 要求を非同期操作として送信します。 |
GetAsync(String)のStringの部分に以下の内容を入れ込めばAPIアクセスが成功するはずです。
https://api.twitter.com/2/tweets/search/recent?query=has:media KTWR&max_results=50&tweet.fields=author_id&expansions=attachments.media_keys&media.fields=preview_image_url&user.fields=name
実際にはエンドポイントのURLとクエリストリングの間に?が必要となりますので、以下のようにして合体させた文字列とすればOKです。3行目はレスポンスで帰ってきた文字列をテキストボックスに代入する処理です。responseはTask型なので、ReadAsStringAsyncで文字列に変換します。
const string TW_BASEADDRESS = "https://api.twitter.com/2/tweets/search/recent";
var response = await client.GetAsync(TW_BASEADDRESS +"?"+ content);
OutTextBox.Text = await response.Content.ReadAsStringAsync();
コード全体
コメントでクエリストリングに関連する細かいことを書き込んでしまっていますが・・・全体はこんな感じです。エラー処理、完全にさぼっています。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Net.Http;
namespace API_C
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
const string INIT_TEXT = "API Test by JS2IIU";
const string BEARER_TOKEN = "ここにトークンを入れる";
const string TW_BASEADDRESS = "https://api.twitter.com/2/tweets/search/recent";
public MainWindow()
{
InitializeComponent();
// TextBoxの文字列
OutTextBox.Text = INIT_TEXT;
}
private async void TW_API_Button_Click(object sender, RoutedEventArgs e)
{
// Search word
var SearchWord = KW_TextBox.Text;
string QS_Search = "has:media " + SearchWord;
// OutTextBox.Text = QS_Search;
//static readonly HttpClient client = new HttpClient();
var client = new HttpClient();
// Base Address
// client.BaseAddress = new Uri(TW_BASEADDRESS);
// HttpClient Header
client.DefaultRequestHeaders.Add("Authorization", $"BEARER {BEARER_TOKEN}");
client.DefaultRequestHeaders.Add("User-Agent", "C# App");
// Query String
var queryString = new Dictionary<string, string>()
{
// Required. Query for matching Tweets. Up to 512 characters.
// { "query", "has:media KTWR" },
{ "query", QS_Search },
// attachments,author_id,context_annotations,created_at,entities,geo,
// id,in_reply_to_user_id,lang,non_public_metrics,organic_metrics,
// possibly_sensitive,promoted_metrics,public_metrics,referenced_tweets,source,text,withheld
{ "tweet.fields", "author_id" },
// Comma-separated list of fields to expand. Expansions enable requests to expand an ID
// into a full object in the includes response object.
// Allowed values: attachments.poll_ids,attachments.media_keys,author_id,geo.place_id,in_reply_to_user_id,referenced_tweets.id
// Default values: none
{ "expansions", "attachments.media_keys" },
// duration_ms,height,media_key,non_public_metrics,organic_metrics,
// preview_image_url,promoted_metrics,public_metrics,type,url,width
{ "media.fields", "preview_image_url" },
// created_at,description,entities,id,location,name,pinned_tweet_id,profile_image_url,
// protected,public_metrics,url,username,verified,withheld
{ "user.fields", "name" },
// The maximum number of search results to be returned by a request.
// A number between 10 and the system limit (currently 100).
// By default, a request response will return 10 results.
{ "max_results", "50" }
};
// Call asynchronous network methods in a try/catch block to handle exceptions.
try
{
var content = await new FormUrlEncodedContent(queryString).ReadAsStringAsync();
// var strQS = TW_BASEADDRESS + "?" + content;
var response = await client.GetAsync(TW_BASEADDRESS +"?"+ content);
OutTextBox.Text = await response.Content.ReadAsStringAsync();
}
catch
{
// Take some action here
}
}
private void EXIT_Button_Click(object sender, RoutedEventArgs e)
{
Application.Current.Shutdown();
}
}
}
プログラムを実行したところ
うまくいきました。KTWR、フレンドシップラジオに関係するツイートが一気に見られるようになりました。次はこの文字列から、必要な情報だけを抽出するところですが・・・今回はここまででおしまいにします。
参考
- Twitter Public Workspace on Postman
- HttpClient でリクエストヘッダを設定する (C#)
- C# 今更ですが、HttpClientを使う
- C#でGETやPOSTを使ってWeb API(OneDriveAPI)をつっつく
- GitHubのTwitter APIサンプルコード
- C# WPFアプリケーションの終了方法あれこれ
- Recent search pagination (Twitter API Doc)
最後まで読んでいただきありがとうございました。