まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

タグ:twitter4j

先日、マンホールマップのログイン機能に障害が発生しました:

通常、マンホールマップ(PC版)のトップページにアクセスすると、画面右上には "Login with Twitter" マークが表示されます(未ログインの場合):
2015122801


で、ここをクリックして、Twitter の OAuth 認可によってマンホールマップにログインして・・・という流れでログインするのですが、ここに問題が発生していると「現在、何らかの障害によってログインできません。」というメッセージが表示されます:
2015122802


一般的には Twitter API の障害が発生していることを示すメッセージなのですが、先日マンホールマップで発生していたものは Twitter API には障害が発生していないにも関わらず、上記のようなメッセージが表示されていたのでした。 ちなみに Twitter API の稼動状態はこちらで確認できます:
API Status | Twitter Developers


さて、上記の原因はなんだったのでしょうか? ちなみにマンホールマップは Java で開発された Web アプリケーションで、この Twitter の OAuth 認可の部分は Twitter4J を使って実装しています(最新版の 4.0.4 でも、スナップショット版の 4.0.5 でも再現)。で、問題部分のコードは以下のようになっていました:
String authorizationURL = null;
try{
  Twitter twitter = new TwitterFactory().getInstance();
  twitter.setOAuthConsumer( TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET );
  RequestToken requestToken = twitter.getOAuthRequestToken();  ←ここで Exception 発生

  String token = requestToken.getToken();
  String tokenSecret = requestToken.getTokenSecret();
    :
    :
}catch( Exception e ){
  e.printStackTrace();
}

リクエストトークンを取得するための Twitter.getOAuthRequestToken() メソッド実行時に Exception が発生していました。それまでに設定しているのは Twitter API の Consumer Key と Consumer Secret だけで、これは正しい値が設定できていました(というか、正しく動いていた時から何も変えてません)。 で、その下の catch 内で以下のようなスタックトレースが出力されていました:
Invalid signature on ECDH server key exchange message
Relevant discussions can be found on the Internet at:
        http://www.google.co.jp/search?q=3cc69290 or
        http://www.google.co.jp/search?q=45a986b4
TwitterException{exceptionCode=[3cc69290-45a986b4 3cc69290-45a9868a], statusCode=-1, message=null, code=-1, retryAfter=-1, rateLimitStatus=null, version=4.0.5-SNAPSHOT(build: 9b7efa6faa540a9defb3e6ba9122a356155986b1)}
        at twitter4j.HttpClientImpl.handleRequest(HttpClientImpl.java:179)
        at twitter4j.HttpClientBase.request(HttpClientBase.java:57)
        at twitter4j.HttpClientBase.post(HttpClientBase.java:86)
        at twitter4j.auth.OAuthAuthorization.getOAuthRequestToken(OAuthAuthorization.java:115)
        at twitter4j.auth.OAuthAuthorization.getOAuthRequestToken(OAuthAuthorization.java:92)
        at twitter4j.TwitterBaseImpl.getOAuthRequestToken(TwitterBaseImpl.java:292)
        at twitter4j.TwitterBaseImpl.getOAuthRequestToken(TwitterBaseImpl.java:287)
        at me.juge.geoimgweb.AppInfo.GetAuthorizationURLTwitter(AppInfo.java:361)
        at me.juge.geoimgweb.AppInfo.GetAuthorizationURL(AppInfo.java:350)
        at org.apache.jsp.stats_jsp._jspService(stats_jsp.java:928)
        at org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:70)
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:717)
        at org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:377)
        at org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:313)
        at org.apache.jasper.servlet.JspServlet.service(JspServlet.java:260)
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:717)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:290)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
        at filters.SetCharacterEncodingFilter.doFilter(SetCharacterEncodingFilter.java:122)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
        at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:233)
        at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:191)
        at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:127)
        at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:102)
        at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109)
        at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:298)
        at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:857)
        at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.process(Http11Protocol.java:588)
        at org.apache.tomcat.util.net.JIoEndpoint$Worker.run(JIoEndpoint.java:489)
        at java.lang.Thread.run(Thread.java:745)
Caused by: javax.net.ssl.SSLKeyException: Invalid signature on ECDH server key exchange message
        at sun.security.ssl.HandshakeMessage$ECDH_ServerKeyExchange.(HandshakeMessage.java:1098)
        at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:278)
        at sun.security.ssl.Handshaker.processLoop(Handshaker.java:913)
        at sun.security.ssl.Handshaker.process_record(Handshaker.java:849)
        at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1035)
        at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1344)
        at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1371)
        at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1355)
        at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:559)
        at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185)
        at sun.net.www.protocol.http.HttpURLConnection.getOutputStream(HttpURLConnection.java:1093)
        at sun.net.www.protocol.https.HttpsURLConnectionImpl.getOutputStream(HttpsURLConnectionImpl.java:250)
        at twitter4j.HttpClientImpl.handleRequest(HttpClientImpl.java:137)
        ... 30 more


問題の getRequestOAuthToken() を実行した時に "Invalid signature on ECDH server key exchange message" というエラーメッセージが出力されています。何コレ?"Invalid signature" って言われても、この時点ではこちらは Key と Secret 以外何も指定してないので間違えているとは思えないけど・・・??

しつこいようですが、今までは何の問題もなく動いていたコードが、ある時を境に急にこのような挙動になってしまいました。となると、Java ソースコードに直接の原因があるとは考えにくい・・・


しかもこの問題、何が厄介かというと、デバッグしようとしてローカルの Eclipse 内で動かすと、このエラーも Exception も発生しないのです。ちゃんと動いてしまいます。更に言うと、別のサーバー内に同様の Java + Tomcat 環境を作って、同じ war をデプロイして実行しても発生しない。要はこの本番サーバーだけで発生するエラーだったのです。。 となると環境依存か、原因特定は更に厄介だぞ・・・

とりあえず緊急でサーバーを1つ作り、そちらに Java と Tomcat を導入し、アプリの war をデプロイ、したら動きました。なので、応急処置としてはこれでなんとかなります。この応急処置サーバーで運用を続けながら原因追求と解決を続けます。 ああ、クラウドってこういう時に便利なのね。。。


さて、このエラーメッセージをググってみると分かるのですが、SSL 関連の Exception のようなのです。はて、マンホールマップに SSL 関連モジュールなんか使ってたっけ?? 何よりも仮に使っていたとしても、実行コードは Twitter4J の中だし、指定しているのは Key と Secret だけで変えようがないし、他のサーバー環境だと動いちゃうし・・・ 念のため API Key と Secret を新たに取得し直して見ましたが、状況は変わりませんでした。


次に行ったのは JDK と Tomcat のアンインストール、および再インストールです。環境は CentOS だったので、 JDK のアンインストールは以下の手順で行いました:
# yum list installed | grep jdk  (インストール済み JDK を確認)
java-1.7.0-openjdk.x86_64
java-1.7.0-openjdk-devel.x86_64
jdk.x86_64              2000:1.7.0_79-fcs (候補は3つ、全部削除してもよいと判断)

# yum remove java-1.7.0* (上2つをアンインストール)
# yum remove jdk* (一番下をアンインストール)

同様にして、Tomcat も以下の手順でアンインストールしました:
# yum list installed | grep tomcat (インストール済み tomcat を確認)
tomcat6.x86_64          6.0.24-90.el6   @base
tomcat6-admin-webapps.x86_64
tomcat6-el-2.1-api.x86_64
tomcat6-jsp-2.1-api.x86_64
tomcat6-lib.x86_64      6.0.24-90.el6   @base
tomcat6-servlet-2.5-api.x86_64 (候補は6つ、全部削除してよいと判断)

# yum remove tomcat6* (まとめてアンインストール)

でもってそれぞれを再インストールします:
# yum install java-1.7.0-openjdk-devel
# yum install tomcat6 tomcat6-admin-webapps

これで Java と Java アプリケーションサーバーはまっさらな状態に戻りました。この状態でマンホールマップの JAR をインストールしてアクセスしたところ・・・ 何も変わらない・・・ orz


全く原因が思いつきません。いよいよヤバい。 改めてこの ECDH なるものを調べてみると暗号化のアルゴリズムっぽいようでした:
ECDH アルゴリズムの概要

暗号化アルゴリズムの、鍵の交換の所で(しかも今まで動いていたものが、ある時から)動かなくなった、ということになります。いよいよ OS レベルの中身がぶっ壊れてしまったか?? と思ったのですが、最後の希望を託してインストールした全ライブラリを更新してみることにしました:
# yum update -y
  :
  :
  :

# /etc/init.d/tomcat6 restart

で、Tomcat をリスタートしてアクセスしてみたら・・・ 直りました! 助かった!!
2015122901
 ↑直ったのはついさっき


後は緊急運用サーバーからこちらに切り替えれば対処完了(予定)。 結局「これ」という直接の原因は分からなかったけど、古いライブラリでは対応しない機能を使っていた、ということなんでしょうかね~。

自分が作って運用しているウェブサービスの多くはクラウド上で動いてますが、このマンホールマップに関しては自宅サーバーで(更に言えば ThinkPad で)運用することにまだこだわっています。今回のエラーはその信念を揺るがすものでしたが、とりあえず解決できました。もうしばらく自宅サーバー運用を続けることができそうです。 v(^o^)



こちらで書いた記事の続きです:
Twitter4J で Twetter の Stream API を使う

上記記事では Twitter 上のツイートをリアルタイムに取得する、という Stream API が一般公開され、Java から利用する場合のサンプルを紹介しました。


折角リアルタイムに取得した情報も、単に表示して終わり、ではつまらないです。後で再利用できるよう、データベースに格納してみます。ただどういったフィルタリングをかけるか(かけないのか)にもよりますが、リアルタイムに取得するツイート情報は、かなり膨大になる可能性があります。つまりそれなりのパフォーマンスを持ったデータストアを用意する必要がある、ということです。

という背景がある中で、今回は Couchbase Server をデータストアとして使ってみます。プログラムとしては先日紹介したこの記事のものをベースとして、取得したツイートを次々に Couchbase Server に格納していく、というロジックに変更します。なお CentOS に Couchbase サーバーを導入する手順についてはこちらの記事を参照してください:
CentOS に Couchbase サーバーを導入する

次にロジック変更のための準備として、こちらの記事を参考に Couchbase の Java SDK をダウンロードして、プロジェクトに追加します:
Java から Couchbase Server にアクセスする

次に、Couchbase Server には JSON データを格納することになるので、Java のオブジェクトを JSON 化するために Gson ライブラリをプロジェクトに追加します。以下のサイトから Gson ライブラリをダウンロード&展開し、gson-2.2.jar をプロジェクトに追加します:
https://google-gson.googlecode.com/files/google-gson-2.2.4-release.zip

更に、取得したツイートを Java オブジェクトとして格納するためのクラスを定義します。ここで定義したクラスのインスタンスとしてツイートを生成し、これを GSON で JSON に変換して Couchbase Server に格納する、という流れになります:
import java.util.Date;

public class Tweet {
	private long id;
	private long userid;
	private String username;
	private String text;
	private Date created;
	private Double lat;
	private Double lng;
	private String[] urls;
	private String[] medias;

	public Tweet( long id, long userid, String username, String text, Date created, Double lat, Double lng, String[] urls, String[] medias ){
		this.id = id;
		this.userid = userid;
		this.username = username;
		this.text = text;
		this.created = created;
		this.lat = lat;
		this.lng = lng;
		this.urls = urls;
		this.medias = medias;
	}

	public long getId() {
		return id;
	}

	public void setId(long id) {
		this.id = id;
	}

	public long getUserid() {
		return userid;
	}

	public void setUserid(long userid) {
		this.userid = userid;
	}

	public String getUsername() {
		return username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

	public String getText() {
		return text;
	}

	public void setText(String text) {
		this.text = text;
	}

	public Date getCreated() {
		return created;
	}

	public void setCreated(Date created) {
		this.created = created;
	}

	public Double getLat() {
		return lat;
	}

	public void setLat(Double lat) {
		this.lat = lat;
	}

	public Double getLng() {
		return lng;
	}

	public void setLng(Double lng) {
		this.lng = lng;
	}

	public String[] getUrls() {
		return urls;
	}

	public void setUrls(String[] urls) {
		this.urls = urls;
	}

	public String[] getMedias() {
		return medias;
	}

	public void setMedias(String[] medias) {
		this.medias = medias;
	}	
}

そして、先日の記事で作成した Stream クラスを以下のように書き換えます(変更箇所を青くしています):
import twitter4j.MediaEntity;
import twitter4j.StallWarning;
import twitter4j.Status;
import twitter4j.StatusDeletionNotice;
import twitter4j.StatusListener;
import twitter4j.TwitterStream;
import twitter4j.TwitterStreamFactory;
import twitter4j.URLEntity;
import twitter4j.conf.Configuration;
import twitter4j.conf.ConfigurationBuilder;

public class Streams {

    private static final String CONSUMER_KEY = "(CONSUMER KEY)";
    private static final String CONSUMER_SECRET = "(CONSUMER SECRET)";
    private static final String ACCESS_TOKEN = "(ACCESS TOKEN)";
    private static final String ACCESS_TOKEN_SECRET = "(ACCESS TOKEN SECRET)";

    private static final String COUCHBASE_SERVER = "192.168.X.X"; //. CouchBase Server ホスト

    public static CouchbaseClient conn = null;
    public static Gson gson = null;

    static class MyStatusListener implements StatusListener {

        public void onStatus(Status status) {
            Double lat = null;
            Double lng = null;
            String[] urls = null;
            String[] medias = null;

            //. 位置情報が含まれていれば取得する        	
            GeoLocation location = status.getGeoLocation();
            if( location != null ){
                double dlat = location.getLatitude();
                double dlng = location.getLongitude();
                lat = dlat;
                lng = dlng;
            }
            long id = status.getId(); //. ツイートID
            String text = status.getText(); //. ツイート本文
            long userid = status.getUser().getId(); //. ユーザーID
            String username = status.getUser().getScreenName(); //. ユーザー表示名
            Date created = status.getCreatedAt(); //. ツイート日時
            
            //. ツイート本文にリンクURLが含まれていれば取り出す
            URLEntity[] uentitys = status.getURLEntities();
            if( uentitys != null && uentitys.length > 0 ){
            	List list = new ArrayList();
                for( int i = 0; i < uentitys.length; i ++ ){
                    URLEntity uentity = uentitys[i];
                    String expandedURL = uentity.getExpandedURL();
                    list.add( expandedURL );
                }
	        urls = ( String[] )list.toArray( new String[0] );
            }
            
            //. ツイート本文に画像/動画URLが含まれていれば取り出す
            MediaEntity[] mentitys = status.getMediaEntities();
            if( mentitys != null && mentitys.length > 0 ){
            	List list = new ArrayList();
                for( int i = 0; i < mentitys.length; i ++ ){
                    MediaEntity mentity = mentitys[i];
                    String expandedURL = mentity.getExpandedURL();
                    list.add( expandedURL );
                }
                medias = ( String[] )list.toArray( new String[0] );
            }

            //. 取り出した情報を JSON にして Couchbase に格納する
            Tweet tweet = new Tweet( id, userid, username, text, created, lat, lng, urls, medias );
            conn.set( "" + id, gson.toJson( tweet ) );            
        }

        public void onDeletionNotice(StatusDeletionNotice sdn) {
            //System.out.println("onDeletionNotice.");
        }

        public void onTrackLimitationNotice(int i) {
            //System.out.println("onTrackLimitationNotice.(" + i + ")");
        }

        public void onScrubGeo(long lat, long lng) {
            //System.out.println("onScrubGeo.(" + lat + ", " + lng + ")");
        }

        public void onException(Exception excptn) {
            //System.out.println("onException.");
        }

        public void onStallWarning(StallWarning arg0) {
            // TODO Auto-generated method stub
        }
    }

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Configuration configuration = new ConfigurationBuilder().setOAuthConsumerKey(CONSUMER_KEY)
                .setOAuthConsumerSecret(CONSUMER_SECRET)
                .setOAuthAccessToken(ACCESS_TOKEN)
                .setOAuthAccessTokenSecret(ACCESS_TOKEN_SECRET)
                .build();

        //. Couchbase Server に接続用
        try{
            List hosts = Arrays.asList( new URI( "http://" + COUCHBASE_SERVER + ":8091/pools" ) );
            conn = new CouchbaseClient( hosts, "default", "" ); //. "Too many open files"
            gson = new Gson();
        }catch( Exception e ){
            e.printStackTrace();
        }

        TwitterStream twStream = new TwitterStreamFactory(configuration).getInstance();
        twStream.addListener(new MyStatusListener());
        
        twStream.sample();
    }
}

前回のコードと比べて、あまり違いはありません。Couchbase SDK を使って Couchbase サーバーに接続するための準備を main メソッド内で行い、onStatus メソッド内では実際に取得したツイートの内容を Java オブジェクト化したものを GSON を使って JSON 化して、後はひたすら ID をキー値として格納する、ということを実行しています。 なお上記コードではフィルタリングを定義していませんが、必要であれば適当なフィルタリング処理を加えていただくのもいいと思います。

このコードを実行すると(フィルタリングの有無や種類にもよりますが)条件に該当するツイートを次々に取得して、Couchbase サーバー内に格納していきます。その様子は Couchbase の管理コンソールからも確認できます:
2014102101

必要に応じて View を定義するなどして、この取得結果を見やすくしたり、別のプログラムから利用できるようにしていく予定です。そちらはまた別途。







 

特別目新しい情報ではなく今更感のある内容ですが、Twitter の Stream 検索 API を使ってみました。

Steam API はいわば「Twitter そのもの」です。自分のタイムラインに関連した内容だけでなく、世界中の Twitter ユーザーがつぶやいた内容をリアルタイムに(Stream として)取得することを目的として提供されています。さすがに全てのツイートを取得するのはごく一部の限られたユーザーにだけ公開されているようですが、一部の(全ツイートの1%程度と言われています)ツイートを取得する API は誰でも使うことができます。一部といっても全て取得していたら、それこそとんでもない量になりますけど・・・

なおずっと以前から公開されていた Twitter の API と区別する場合、Steam API と(従来の)REST API という名称で区別します。


で、その Twitter Stream API を Java から、Twitter4J を使って取得するコードを作って試しに動かしてみました。以下にその様子を紹介します。

まず、何はともあれ Twitter Application Managementへの Developer 登録と、アプリケーションの登録、そして CONSUMER KEY, CONSUMER SECRET, ACCESS TOKEN, ACCESS TOKEN SECRET といった各種キーの取得が必要です。この辺りは REST API の時とも変わらない作業なので、適当にググるなどして調べてください。

次に最新の Twitter4J をダウンロードして展開します。zip アーカイブのファイル名は twitter4j-[バージョン番号].zip です。展開後の lib ディレクトリに含まれる必要なファイル(最低限動かすためには twitter4j-core-[バージョン番号].jar と twitter4j-stream-[バージョン番号].jar の2つのファイル)を開発プロジェクトに取り込むか、CLASSPATH を通して参照可能にします。
2014102001


事前準備はこれでできました。後は以下の様なコードを記述します。冒頭部分で取得した CONSUMER KEY 等のキーに書き換える必要があります。この例では取得する条件を付けず、世の中の全てのツイート(の約1%)をそのまま取得しています。取得イベントが発生する度に onStatus メソッドが呼ばれ、そこで ID やユーザー名、本文といった各種情報を取り出して、最終的には System.out で標準出力しています(実際のアプリではここで DB に格納するなどという流れになると思います):
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;

import twitter4j.FilterQuery;
import twitter4j.GeoLocation;
import twitter4j.MediaEntity;
import twitter4j.StallWarning;
import twitter4j.Status;
import twitter4j.StatusDeletionNotice;
import twitter4j.StatusListener;
import twitter4j.TwitterStream;
import twitter4j.TwitterStreamFactory;
import twitter4j.URLEntity;
import twitter4j.conf.Configuration;
import twitter4j.conf.ConfigurationBuilder;

public class Streams {

    private static final String CONSUMER_KEY = "(CONSUMER KEY)";
    private static final String CONSUMER_SECRET = "(CONSUMER SECRET)";
    private static final String ACCESS_TOKEN = "(ACCESS TOKEN)";
    private static final String ACCESS_TOKEN_SECRET = "(ACCESS TOKEN SECRET)";

    static class MyStatusListener implements StatusListener {

        public void onStatus(Status status) {
            Double lat = null;
            Double lng = null;
            String[] urls = null;
            String[] medias = null;

            //. 位置情報が含まれていれば取得する        	
            GeoLocation location = status.getGeoLocation();
            if( location != null ){
                double dlat = location.getLatitude();
                double dlng = location.getLongitude();
                lat = dlat;
                lng = dlng;
            }
            long id = status.getId(); //. ツイートID
            String text = status.getText(); //. ツイート本文
            long userid = status.getUser().getId(); //. ユーザーID
            String username = status.getUser().getScreenName(); //. ユーザー表示名
            Date created = status.getCreatedAt(); //. ツイート日時
            
            //. ツイート本文にリンクURLが含まれていれば取り出す
            URLEntity[] uentitys = status.getURLEntities();
            if( uentitys != null && uentitys.length > 0 ){
            	List list = new ArrayList();
                for( int i = 0; i < uentitys.length; i ++ ){
                    URLEntity uentity = uentitys[i];
                    String expandedURL = uentity.getExpandedURL();
                    list.add( expandedURL );
                }
	        urls = ( String[] )list.toArray( new String[0] );
            }
            
            //. ツイート本文に画像/動画URLが含まれていれば取り出す
            MediaEntity[] mentitys = status.getMediaEntities();
            if( mentitys != null && mentitys.length > 0 ){
            	List list = new ArrayList();
                for( int i = 0; i < mentitys.length; i ++ ){
                    MediaEntity mentity = mentitys[i];
                    String expandedURL = mentity.getExpandedURL();
                    list.add( expandedURL );
                }
                medias = ( String[] )list.toArray( new String[0] );
            }

            //. 取り出した情報を表示する(以下では id, username, text のみ)
            System.out.println( "id = " + id + ", username = " + username + ", text = " + text );
        }

        public void onDeletionNotice(StatusDeletionNotice sdn) {
            //System.out.println("onDeletionNotice.");
        }

        public void onTrackLimitationNotice(int i) {
            //System.out.println("onTrackLimitationNotice.(" + i + ")");
        }

        public void onScrubGeo(long lat, long lng) {
            //System.out.println("onScrubGeo.(" + lat + ", " + lng + ")");
        }

        public void onException(Exception excptn) {
            //System.out.println("onException.");
        }

        public void onStallWarning(StallWarning arg0) {
            // TODO Auto-generated method stub
        }
    }

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Configuration configuration = new ConfigurationBuilder().setOAuthConsumerKey(CONSUMER_KEY)
                .setOAuthConsumerSecret(CONSUMER_SECRET)
                .setOAuthAccessToken(ACCESS_TOKEN)
                .setOAuthAccessTokenSecret(ACCESS_TOKEN_SECRET)
                .build();

        TwitterStream twStream = new TwitterStreamFactory(configuration).getInstance();
        twStream.addListener(new MyStatusListener());
        
        twStream.sample();
    }
}
このサンプルを実行するとツイッター本体が管理するツイートをリアルタイムに(ここ重要!)取得して、目で追えないほど次々に画面出力されていきます。1つ1つの内容を読むことが無理だと思いますが、この様子を眺めているだけで、いかに日本語ツイートの占める割合が高いかもわかると思います:
2014102002


このコードのままだと確かに全ツイート(のランダムな一部)は取得できますが、特定のキーワードやハッシュタグなど、ある特定の目的で絞り込んだツイートだけを集めたいこともあると思います。その場合は main 関数を以下のように書き換えて、フィルタリングを指定します:
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Configuration configuration = new ConfigurationBuilder().setOAuthConsumerKey(CONSUMER_KEY)
                .setOAuthConsumerSecret(CONSUMER_SECRET)
                .setOAuthAccessToken(ACCESS_TOKEN)
                .setOAuthAccessTokenSecret(ACCESS_TOKEN_SECRET)
                .build();

        TwitterStream twStream = new TwitterStreamFactory(configuration).getInstance();
        twStream.addListener(new MyStatusListener());
        
        //. フィルター
        String[] track = { "wordpress", "#php", "ワードプレス" };
        FilterQuery filter = new FilterQuery();
        filter.track( track );
        twStream.filter( filter );
    }

この例では { "wordpress", "#php", "ワードプレス" } という3つのフィルタを指定しています。これらのキーワードのいずれかが本文に含まれるツイートだけを集めてストリーミングする、という機能に切り替えています。

こちらを実行すると、先ほどのフィルタリングのないケースと比べてかなりゆっくり、少しずつツイートが記録されていく様子がわかると思います:
2014102003


ただこのフィルタリングですが、特に日本語キーワードを正しく取得できる率があまり高くないようです。要は日本語文章の単語分割ロジックが英語の場合と異なることに起因しているのだと思いますが、注意が必要です。ただ日本語のハッシュタグの場合はかなり正しく取得できる、らしいです。


改めて、全レコードではないとはいえ、これだけのリアルタイムビッグデータを誰でも取得できる時代になった、ということだと思います。ビッグデータをリアルタイムに解析する生きたサンプルデータとして、Twitter は最適だと思います。ビッグデータ関連製品もいよいよこれらをどれだけ速く処理して、どれだけ遅れなく格納して、どれだけ最適化して、どれだけ簡易的に扱えるか、という段階になっていくんでしょうかね。

 

このページのトップヘ