🖼️

【.NET】Twitter APIを使って推しの画像に溺れたい③

2022/05/30に公開

こんにちは。
会社の研修で忙しくてちょっと間が空きました。
あと、ヴィクトリアマイルを現地でみてきました。ソダシが抜け出したときに歓声が一段と大きくなって、みんなでアイドルホースを応援できる競馬はいいなぁと思いました(なお馬券はry

今回は「検索のチューニング」と「画像URL取得」です。

検索条件を詳細にする

前回検索で使ったGetSearchTweetsV2Iteratorメソッドを含むクラスは以下のように定義されています。

https://github.com/linvi/tweetinvi/blob/master/src/Tweetinvi/Client/Clients/V2/SearchV2Client.cs

クエリ指定

よく見ると1個目のGetSearchTweetsV2Iteratorの引数名がkeywordとかではなくqueryとなっています。
なぜqueryと名付けているかというと、ここには検索ワードだけでなく検索条件をいろいろ詰め込んで複雑な検索ができるようになっているからです。

クエリの組み立て方は以下に記載されています。
https://developer.twitter.com/en/docs/twitter-api/tweets/search/integrate/build-a-query#build

説明を参考にクエリを組み立てると "#ほしまちぎゃらりー -(is:retweet OR is:reply) has:images" となります。
意味は「本文に"#ほしまちぎゃらりー"を含む」かつ「リツイートおよびリプライのツイートではない」かつ「画像がある」です。

パラメータクラス指定

さらに2個目のGetSearchTweetsV2Iteratorの引数にはISearchTweetsV2Parametersインターフェイスが指定されています。

https://github.com/linvi/tweetinvi/blob/master/src/Tweetinvi.Core/Public/Parameters/V2/SearchesClientV2/SearchTweetsV2Parameters.cs

1つ1つのフィールドの説明は書いていませんが、おそらく以下のように使われます。

type field usage
DateTime EndTime この時刻までのツイートを取得
string Query 検索条件を指定するクエリ文字列(前セクションに記述の通り)
int PageSize Iterator検索時に一度に取得するツイート数(1~100 既定は100)
string SinceId 取得ツイートの最小ID
DateTime StartTime この時刻からのツイートを取得
string UntilId 取得ツイートの最大ID

ツイート時刻をStartTime~EndTimeで、ツイートIDをSinceId~UntilIdで絞り込めるようになっています。
PageSizeは基本指定しなくていいと思います。レスポンスサイズを小さくしたいときは100未満の数値を入れるのでしょう。
Queryは前セクションのクエリです。

実際に使ってみよう

Main.cs(Mainメソッドのみ)
TwitterClient userClient = new(COMSUMER_KEY, CONSUMER_KEY_SECRET, ACCESS_TOKEN, ACCESS_TOKEN_SECRET);

// このあとプロパティ設定を追加するのでパラメータクラス指定にする
SearchTweetsV2Parameters parameters = new("#ほしまちぎゃらりー -(is:retweet OR is:reply) has:images");
// イテレータ取得
var searchIterator = userClient.SearchV2.GetSearchTweetsV2Iterator(parameters);

List<string> csvLines = new();
int count = 0;

while (!searchIterator.Completed)
{
    // コンソールに進捗を出しておく
    count++;
    Console.WriteLine($"{count}ループ目");

    var searchPage = await searchIterator.NextPageAsync();
    var searchResponse = searchPage.Content;
    var tweets = searchResponse.Tweets;

    foreach(var tweet in tweets)
    {
        csvLines.Add(string.Format(
	    "{0}, {1}, {2}, {3}, {4}",
	    tweet.CreatedAt.ToLocalTime().ToString("yyyy/MM/dd hh:mm:ss"),
	    tweet.PublicMetrics.LikeCount,  // いいね数
	    tweet.PublicMetrics.RetweetCount,  // リツイート数
	    tweet.Text.Replace("\n", "").Replace(",", ""),
	    $"https://twitter.com/user/status/{tweet.Id}"  // ツイートURL
        ));
    }
}

Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
File.WriteAllLines(@"C:\Users\user\Downloads\TweetSeatchTest.csv", csvLines, Encoding.GetEncoding("Shift_JIS"));

結果

きちゃあああああああヾ(。>﹏<。)ノ゙✧*。

さっそく推しに溺れよう

いいね数で降順に並べ、一番いいほしまちぎゃらりーを見に行きます。

あああああかわいいいい!!!!すいちゃん大好き!!!!!!

画像ダウンロード用のURLを取得する

ツイートのURLはわかりましたが、画像をダウンロードするためのURLはまだわかりません。
次は画像URLもCSVファイルに出力できるように実装します。

メディアURLの在り処

https://developer.twitter.com/en/docs/twitter-api/data-dictionary/object-model/tweet

ツイート情報(Tweet object)には2つのプロパティがあります(上サイトのSample Response参照)。

  • data:ツイート本体の情報(ツイートID、ユーザーID、ツイート日時など)
  • includes:付随する情報(メディア情報、位置情報、関連するツイートなど)

これまで取得してきた情報はすべてdataに含まれるもので、紐づく画像URLはincludesに含まれています。

これを知らなかったために、結構時間を食わされました。

ツイート情報とメディア情報の紐づき

理想的には1つのツイートに複数のメディアが紐づいた状態でひとまとまりになり、それを配列で返してくれると嬉しいのですが、上記の通りそうはなっていません。
dataincludesに分かれているように、ツイート情報のみの配列と付随情報のみの配列で別々にデータがやってきます。
そのため各付随情報はどのツイートに紐づいているのかがわからないと使えません。

紐づきを担っているのがdata.attachments.media_keysです。

ツイートに付随するメディアキー

このキーを使ってincludes.mediaを検索します。
mediaの構造はMedia objectで定義されています。

https://developer.twitter.com/en/docs/twitter-api/data-dictionary/object-model/media

Media objectにmedia_keyがあり、これによりツイートに付随するメディアを特定できます。

付随情報が持つメディアキー

Tweetinviではどうなっているのか

APIのレスポンスと同じようにクラスが構成されています。
イテレータから取得したレスポンスデータは、JSONをデシリアライズしたものをひとまとめにした構造になっています。

https://github.com/linvi/tweetinvi/blob/master/src/Tweetinvi.Core/Public/Models/V2/Responses/TweetsV2Response.cs

実際に使ってみよう

Main.cs(Mainメソッドのみ)
TwitterClient userClient = new(COMSUMER_KEY, CONSUMER_KEY_SECRET, ACCESS_TOKEN, ACCESS_TOKEN_SECRET);

var parameters = new SearchTweetsV2Parameters("#ほしまちぎゃらりー -(is:retweet OR is:reply) has:images");
var searchIterator = userClient.SearchV2.GetSearchTweetsV2Iterator(parameters);

// K:MediaKey V:URL
Dictionary<string, string> imageUrls = new();
// メディアURLのリスト
List<string> urlList = new();

List<string> csvLines = new();
int count = 0;

// CSV Header
csvLines.Add("id,datetime,like,retweet,text,url1,url2,url3,url4");

while (!searchIterator.Completed)
{
    count++;
    Console.WriteLine($"{count}ループ目");

    var searchPage = await searchIterator.NextPageAsync();
    var searchResponse = searchPage.Content;
    // `media.includes.media`抜き出し
    var media = searchResponse.Includes.Media;
    // `data`抜き出し
    var tweets = searchResponse.Tweets;

    // MediaからMediaKeyとURLだけを詰め直し
    foreach(var medium in media)
    {
        imageUrls.Add(medium.MediaKey, medium.Url);
    }

    foreach(var tweet in tweets)
    {

	// ツイートに紐づくメディアを検索しリスト化
        foreach(var key in tweet.Attachments.MediaKeys)
        {
	    string url = imageUrls[key];
	    urlList.Add(url);
        }

        csvLines.Add(string.Format(
            "{0},{1},{2},{3},{4},{5}",
            tweet.Id,
            tweet.CreatedAt.ToLocalTime().ToString("yyyy/MM/dd hh:mm:ss"),
            tweet.PublicMetrics.LikeCount,
            tweet.PublicMetrics.RetweetCount,
            tweet.Text.Replace("\n", "").Replace(",", ""),
            String.Join(",", urlList)
        ));
	
	urlList.Clear();
    }

    imageUrls.Clear();
}

Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
File.WriteAllLines(@"C:\Users\user\Downloads\TweetSeatchTest.csv", csvLines, Encoding.GetEncoding("Shift_JIS"));

結果

よっしゃああああああ!!!これがほしかった!!!

では溺れましょう

https://twitter.com/DezHsieh/status/1529386800191512593

はぁ~~~miCometてぇてぇ~...😇

まとめ

  • イテレータからのレスポンスにAPIからのレスポンスをデシリアライズしたインスタンスが詰まっている
  • ツイート情報とメディアはmedia_keyで紐づいている

次回

ひとまず欲しい情報はすべて手に入りました。
次回は設定関連です。

Discussion