Twitterのタイムラインであけおめtweetを収集する

"happy new year"と発言した人でplaceが設定されている人はその緯度経度の平均(ポリゴンの頂点なので平均はおかしいけど)を取得して表示するよ。

ソースコード

ソースコードが酷いのは30分ぐらい前に思いついて作ったからだよ。
追記: ログ残したいのでちょっと修正
追記: 緯度と経度が逆だったので修正

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;U
import java.io.InputStreamReader;
import java.util.Date;

import net.arnx.jsonic.JSON;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.methods.GetMethod;


public class NewYearCrawler {

    // とりあえず今回使うところだけフィールド定義する
    public static class PlaceBoundingBox {
        public String type;
        public float coordinates[][][];
    }
    public static class Place {
        public String country;
        public String country_code;
        public String place_type;
        public String url;
        public PlaceBoundingBox bounding_box;
    }
    public static class Tweet {
        public Place place;
        public String text;
        public Date created_at;
    }

    public void run() throws IOException {
        String url = "http://stream.twitter.com/1/statuses/filter.json?track=happy+new+year";

        HttpClient client = new HttpClient();
        String username = 【あなたのID】;
        String password = 【あなたのパスワード】;
        client.getState().setCredentials(AuthScope.ANY,
            new UsernamePasswordCredentials(username,password));
        GetMethod method = new GetMethod(url);
        client.executeMethod(method);
        
        InputStream response = method.getResponseBodyAsStream();
        BufferedReader reader = new BufferedReader(new InputStreamReader(response));
        for (String line = reader.readLine(); line != null; line = reader.readLine()) {
            try {
                Tweet tweet = JSON.decode(line, Tweet.class);
                if (tweet.place == null) {
                    continue;
                }
                System.err.println(""+ line);
                float[][] placeCoordinates =  tweet.place.bounding_box.coordinates[0];
                float average_latitude = 0; // 緯度
                float average_longitude = 0; // 経度
                for (float[] coordinate : placeCoordinates) {
                    average_longitude +=  coordinate[0] / placeCoordinates.length;
                    average_latitude +=  coordinate[1] / placeCoordinates.length;
                }
                System.out.println(tweet.created_at.toString() +
                        "," + tweet.place.country +
                        "," + average_latitude +
                        "," + average_longitude +
                        "," + tweet.text);
            } catch (Exception e) {
                System.err.println(e.getMessage());
            }
        }

    }
    public static void main(String[] args) {
        NewYearCrawler crawler = new NewYearCrawler();
        try {
            crawler.run();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

環境

  • Java5以上
  • commons-codec-1.3.jar
  • commons-httpclient-3.0.1.jar
  • commons-logging-1.03.jar
  • jsonic-1.2.5.jar

JSONIC初めて使ってけどいいですねこれ。

結果(2010/1/3追記)

ということで結果を集計してみた。
自宅回線の状況があまり良くなかったので、残念ながらあまり正確なデータを取ることができなかったけど、取れている範囲で見ていく。ちなみに全件数は54020件*1でした。


とりあえず国別にしてみた。Twitter Places対応国だけなので日本は入ってなかった……


まずは全世界(日時はJSTでの表記。JSTUTC+9なので、一旦9時間引いてから各国のUTCからの時差を足すと、その地域の時間になる)

USとUKとインドネシアからの発言が多い。それぞれのピークは14時(GMTの5時)、9時(GMTの0時)、2時(GMTの17時)で、どうやら各国の標準時における深夜0時がピークになっているようだ。(USとインドネシアは複数の標準時があるので、それは後で見ていく。) また、それぞれ最初のピークの12時間後あたりにも低目のピークがあり、元旦の正午あたりに挨拶している様子が分かる。


上位3国について、もう少し細かく(10分単位)でカウントを見てみる。(以下のグラフは全て10分単位)

UKは0時きっかりに挨拶している人が多いようだ。グリニッジ天文台を擁する世界標準時国の自負があるのだろうか(知らんけど。) USとインドネシアは幅が広いんだけど、これは複数の標準時にばらけているせいかもしれない。

タイムラインの情報に"utc_offset"という項目があり、各利用者が設定のTime Zoneに設定した値が表示されるので、これを元にもう少し細かく見て行く。
まずUS。

EST(UTC-5)、CST(UTC-6)、MST(UTC-7)、PST(UTC-8)のそれぞれで、JSTの14〜17時(GMTの5〜8時)、つまり各地域での0時に挨拶している人が多いようだ。


つぎインドネシア

WikipediaによるとインドネシアにはWIB(UTC+7)、WITA(UTC+8)、WIT(UTC+9)という3つの標準時があるらしい。WIBが多いようだ。あとPSTのまま使っている利用者が結構いる。あんまり気にしないのかね。

折角経度情報があるので経度10度単位でみてみる。

微妙だけど、東経100°〜110°のピークがJSTの2時ごろ、東経110°〜120°のピークがJSTの1時〜2時ごろで、それぞれWIBとWITAに当てはまると辻褄があう。実際はどうか分からないけど。


その他の国では、オランダ(UTC+1)とマレーシア(UTC+8)が比較的多かった。


全世界分をUTCオフセット単位で集計してみると、31種類のUTCオフセットが使われていた。上位7位までをグラフにしてみる。


4:00〜5:00が大きく抜けているが、これは部屋の掃除していたらマシンがスリープしていたため……

UTCオフセット別の国の内訳は、

  • UTC-3はほぼ全部Brazilだった。
  • UTC-5はUnited StatesとCanadaが多い
  • UTC-6はUnited StatesとMexicoとCanada
  • UTC-8はUnited StatesとCanada。デフォルト設定のせいかIndonesiaとRussiaも多い。
  • UTC(+0)はUnited Kingdom、Ireland、Spain、Portugal他各国
  • UTC+1はThe Netherlands、France、Germany、Spain、Italy、Sweden、Belgium、Norway、Denmark、Poland、Switzerlandあたり

南半球は夏時間のところもあるかもしれないけど、今回みたところでは殆ど無い模様。(と思ったけどBrazilが夏時間の模様。グラフ見ても大晦日の夕方がピークなのでよく分からないけど。)


ざっくりとだが、やはり各国の標準時で0時前後に"happy new year"とtweetしている人が多いようだ。当然followしている人が別の地域だったりすると違う時間にも挨拶返ししたりしているかもしれない。

反省

utc_offsetはTwitter Places対応国じゃなくても取れるので、最初からutc_offsetに絞って集計する気なら日本のデータも取れた。
あとgeoという項目(ずばり緯度経度の情報)も見落としていた。place付いてなくてgeo付いてるのもあったかもしれない。


プログラムはplace.bounding_boxがnullの場合の対処が入っていないので直したほうがいいな。
あと日付の書式を"yyyy/MM/dd HH:mm:ss"にして、utc_offsetとgeoの対応も入れてみた。place.bounding_box.coordinatesとgeo.coordinatesは緯度経度の順序が逆なので注意。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;

import net.arnx.jsonic.JSON;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.methods.GetMethod;


public class NewYearCrawler {

    // とりあえず今回使うところだけフィールド定義する
    public static class PlaceBoundingBox {
        public String type;
        public float coordinates[][][];
    }
    public static class Place {
        public String country;
        public String country_code;
        public String place_type;
        public String url;
        public PlaceBoundingBox bounding_box;
    }
    public static class User {
        public int utc_offset;
    }
    public static class Geo {
        public String type;
        public float[] coordinates;
    }
    public static class Tweet {
        public Place place;
        public String text;
        public Date created_at;
        public User user;
        public Geo geo;
    }

    public void run() throws IOException {
        String url = "http://stream.twitter.com/1/statuses/filter.json?track=happy+new+year";

        HttpClient client = new HttpClient();
        String username = 【あなたのID】;
        String password = 【あなたのパスワード】;
        client.getState().setCredentials(AuthScope.ANY,
            new UsernamePasswordCredentials(username,password));
        GetMethod method = new GetMethod(url);
        client.executeMethod(method);
        
        SimpleDateFormat format = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
//        format.setTimeZone(TimeZone.getTimeZone("UTC"));

        InputStream response = method.getResponseBodyAsStream();
        BufferedReader reader = new BufferedReader(new InputStreamReader(response));
        for (String line = reader.readLine(); line != null; line = reader.readLine()) {
            try {
                Tweet tweet = JSON.decode(line, Tweet.class);
                if (tweet.place == null && tweet.geo == null) {
                    continue;
                }
                System.err.println(""+ line);
                String place_longitude = "";
                String place_latitude = "";
                if (tweet.place != null  && tweet.place.bounding_box != null) {
                    float[][] placeCoordinates =  tweet.place.bounding_box.coordinates[0];
                    float latitude_sum = 0;
                    float longitude_sum = 0;
                    for (float[] coordinate : placeCoordinates) {
                        latitude_sum +=  coordinate[0];
                        longitude_sum +=  coordinate[1];
                    }
                    place_longitude = Float.toString(longitude_sum/placeCoordinates.length);
                    place_latitude = Float.toString(latitude_sum/placeCoordinates.length);
                }
                String geo_longitude = "";
                String geo_latitude = "";
                if (tweet.geo != null) {
                    geo_longitude = Float.toString(tweet.geo.coordinates[0]);
                    geo_latitude = Float.toString(tweet.geo.coordinates[1]);
                }
                System.out.println(
                        format.format(tweet.created_at) +
                        "," + (tweet.place != null ? tweet.place.country : "")+
                        "," + tweet.user.utc_offset +
                        "," + geo_longitude + "," + geo_latitude +
                        "," + place_longitude + "," + place_latitude +
                        "," + tweet.text);
             } catch (Exception e) {
                System.err.println(e.getMessage());
            }
        }

    }
    public static void main(String[] args) {
        NewYearCrawler crawler = new NewYearCrawler();
        try {
            crawler.run();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

おまけ: 回線不安定の対処

某モバイル回線のトライアル機を借りてきて置いてるんだけど、マシン設置場所周辺だと電波が不安定なせいか結構通信が切れる。
最初は回線のせいだと分からなくてプログラムで監視&手動で再起動していたけど、徐々に対処が分かったのでbashでループして処理させるようにした。
回線が切れた場合にストリームが切断されれば良いんだけど、無限に待ち続けてしまうようなので、出力ファイルの更新日時を監視して、一定時間(30秒)更新されない場合はプログラムを再起動する処置を取った。
切れた際に回線が再接続できるまで20〜80秒ぐらい掛かるので、60秒待つ処理を入れて、接続が終わったタイミングで起動するようにした。

export CLASSPATH=lib/commons-codec-1.3.jar:lib/commons-httpclient-3.0.1.jar:lib/commons-logging-1.03.jar:lib/jsonic-1.2.5.jar:bin
while [ 1==1 ] ; do
   java NewYearCrawler 1>>new_year.txt 2>>new_year.log&
   pid=$!
   sleep 10
   while [ $(($(date +'%s')-$(stat -f '%m' -t '%s' new_year.txt))) -lt 30 ]; do
     sleep 10
   done
   kill $pid
   afplay wave08.wav
   sleep 60
done

あ、環境はOS X 10.6(Snow Leopard)です。
"kill %"でも行けそうな気がしたけど、念のためプロセス番号を憶えておくようにした。
あと、afplayというのはコマンドラインから音を鳴らすコマンドで、wave08.wavは「システムエラーです」という声が入っています。美しい声です。

*1:sampleだし実際の件数はもっと多いと思うけど