😊

Twitter API v2 Volume streams(sampleStream)の(Javaでの)使用方法

2022/11/26に公開

Twitter APIがv2に切り替わったことで全ツイート(の1%)を取得できるStreaming APIもVolume streamsに変更され、公式ドキュメントにも誤りがあるなど対応が大変だったので使用方法の解説です。

Twitter API v2の登録方法

以下のページで解説されてます。APIの使用目的など英語で書かなければいけないところが少しあるので注意してください。
【2022年】TwitterAPI v2 の仕様まとめ・セットアップ方法

公式Java SDKの使用

TwitterのJava SDKはTwitter4Jが有名で、現在もツイートの投稿などは行えるのですがVolume streamsは使えず、今後も対応予定はなく、また現在はTwitter自ら公式Java SDK(Github: twitter-api-java-sdk)を出してるのでこちらを使用します。インストール方法などはGithubに載ってるのでそちらを見てください。

Gradleのインストール(Windows)

ビルドにGradleを使用している場合、最新版を使う必要がありますがWindowsでツールを使ってインストールするのは面倒というかどうも不可能なようなので手動でインストールしてください。
また、私は久しぶりに使ったせいかかなりやり方が変わっていたので最新版のドキュメント(Building Java Applications Sample)を読むことをオススメします。

エラーが出る場合

キャッシュ(C:\Users\<あなたのユーザ名>\.gradle\cachesフォルダ)削除で直る可能性があります(Gradle could not start your build - Stack Overflow)。

Gradleの便利機能

Volume streamsが動くかどうか最低限の確認がしたい場合などは他のファイルやフォルダを無視してビルドすることができます。

build.gradle
sourceSets {
    main {
        java {
            srcDir 'src/main/java'
            exclude 'trend_detect/viewer', 'trend_detect/RetweetQueue.java'
        }
    }
}

この例ではsrcDirにソースフォルダを記載して、除外したいフォルダ、ファイル名をexcludeに記載しています。

他のGradleプロジェクトをインポートする

他のGradleプロジェクトをインポートする時は、そのプロジェクトのsettings.gradlerootProject.nameが例えばJapaneseTwitterSpamfilterV2の場合、以下の様にします。

settings.gradle
include ':JapaneseTwitterSpamfilterV2'
project(':JapaneseTwitterSpamfilterV2').projectDir = new File('..\\SpamFilterV2\\lib')

projectDirのパスはインポートしたいプロジェクトのbuild.gradleがあるところ(settings.gradleではなく)を指定する必要があります。

Volume streams(sampleStream)を使う場合の注意点

公式ドキュメントにはサンプルコードなどが載ってますが現在(2022/11/26)少し問題があります。

  1. new BufferedReader(new InputStreamReader(result));と書いてあるところがありますが、new BufferedReader(new InputStreamReader(result, "UTF-8"));と文字コードを付けないと文字化けします。

  2. サンプルコードに

    // Set the params values
    Integer backfillMinutes = 56; // Integer | The number of minutes of backfill requested.
    Set<String> tweetFields = new HashSet<>(Arrays.asList()); // Set<String> | A comma separated list of Tweet fields to display.
    Set<String> expansions = new HashSet<>(Arrays.asList()); // Set<String> | A comma separated list of fields to expand.
    Set<String> mediaFields = new HashSet<>(Arrays.asList()); // Set<String> | A comma separated list of Media fields to display.
    Set<String> pollFields = new HashSet<>(Arrays.asList()); // Set<String> | A comma separated list of Poll fields to display.
    Set<String> userFields = new HashSet<>(Arrays.asList()); // Set<String> | A comma separated list of User fields to display.
    Set<String> placeFields = new HashSet<>(Arrays.asList()); // Set<String> | A comma separated list of Place fields to display.
    try {
            InputStream result = apiInstance.tweets().sampleStream()
             .backfillMinutes(backfillMinutes)
             .tweetFields(tweetFields)
             .expansions(expansions)
             .mediaFields(mediaFields)
             .pollFields(pollFields)
             .userFields(userFields)
             .placeFields(placeFields)

と取得できる情報を色々指定できるように書かれてますが、実際にはtweetFieldsに指定したものしか取得できず、また、backfillMinutesを指定するとエラーが起きます。
edit_history_tweet_idsを指定して無くてもそれが見つからないとIllegalArgumentExceptionエラーが起きるときがあります。
3. サンプルコードを動かすと

java.io.FileNotFoundException: sdk.properties (�w�肳�ꂽ�t�@�C�������‚���܂���B)
	at java.base/java.io.FileInputStream.open0(Native Method)
	at java.base/java.io.FileInputStream.open(FileInputStream.java:216)
	at java.base/java.io.FileInputStream.<init>(FileInputStream.java:157)
	at java.base/java.io.FileInputStream.<init>(FileInputStream.java:111)
	at com.twitter.clientlib.SDKConfig.<clinit>(SDKConfig.java:38)
	at com.twitter.clientlib.api.TwitterApi.init(TwitterApi.java:75)
	at com.twitter.clientlib.api.TwitterApi.<init>(TwitterApi.java:50)
	at trend_detect.TrendAnalyzerBoot.mainProcess(TrendAnalyzerBoot.java:98)
	at trend_detect.TrendAnalyzerBoot.main(TrendAnalyzerBoot.java:52)

というエラーメッセージが出ますが動作自体は正常にします。

以上の点を踏まえるとサンプルコードは次のようになります。

TwitterApi apiInstance = new TwitterApi(
                    new TwitterCredentialsBearer(
                            "<Bearer Token>"
                    ));
System.out.println();
// Set the params values
Set<String> tweetFields = new HashSet<>(Arrays.asList("id", "author_id", "created_at", "lang", "source", "text", "entities", "public_metrics")); // Set<String> | A comma separated list of Tweet fields to display.
Set<String> expansions = new HashSet<>(Arrays.asList()); // Set<String> | A comma separated list of fields to expand.
Set<String> mediaFields = new HashSet<>(Arrays.asList()); // Set<String> | A comma separated list of Media fields to display.
Set<String> pollFields = new HashSet<>(Arrays.asList()); // Set<String> | A comma separated list of Poll fields to display.
Set<String> userFields = new HashSet<>(Arrays.asList()); // Set<String> | A comma separated list of User fields to display.
Set<String> placeFields = new HashSet<>(Arrays.asList()); // Set<String> | A comma separated list of Place fields to display.
try {
	InputStream result = apiInstance.tweets().sampleStream()
			.tweetFields(tweetFields)
			.expansions(expansions)
			.mediaFields(mediaFields)
			.pollFields(pollFields)
			.userFields(userFields)
			.placeFields(placeFields)
			.execute();
	try {
		Type localVarReturnType = new TypeToken<StreamingTweetResponse>() {}.getType();
		BufferedReader reader = new BufferedReader(new InputStreamReader(result, "UTF-8"));
		String line = reader.readLine();
		while (line != null) {
			if (line.isEmpty()) {
				System.out.println("==> Empty line");
				line = reader.readLine();
				continue;
			}
			//System.out.println(response != null ? response.toString() : "Null object");
			try {
				StreamingTweetResponse response = JSON.getGson().fromJson(line, localVarReturnType);
				if (response != null) {
					Tweet tweet = response.getData();
                    //処理
				}
			} catch (IllegalArgumentException e) {//IllegalArgumentExceptionが起きてもプログラムが停止しないように
				System.out.println("IllegalArgumentException: " + line);
				e.printStackTrace();
			}
			line = reader.readLine();
		}
	} catch (Exception e) {
		e.printStackTrace();
		System.out.println(e);
	}
} catch (ApiException e) {
	System.err.println("Exception when calling TweetsApi#sampleStream");
	System.err.println("Status code: " + e.getCode());
	System.err.println("Reason: " + e.getResponseBody());
	System.err.println("Response headers: " + e.getResponseHeaders());
	e.printStackTrace();
}

tweet.getText()で取得できるテキストの問題

RT回数などは取得できますがRTかどうかは判定できず、textが"RT @username:"で始まるかどうかでしか判断できません。その場合以下の様に正規表現でその部分を削除する、あるいはRTかどうかを判定するしかありません。

text = text.replaceAll("^RT @.+: ", "");

また、なぜか"https:…"という文字列が含まれる、また&が"&amp;"に変換されるのでそれは削除する必要があります。

Matcher urlMatcher = Pattern.compile("(https?|ftp)([-_.!~*\\'()a-zA-Z0-9;/?:@&=+$,%#]+)").matcher(text);
text = urlMatcher.replaceAll(" ");
text = text.replaceAll("&amp;", "&");

Discussion