前回作成したTwitter Search APIによるつぶやき検索 (C#)では、TweetDeckとの検索結果と比較して、情報鮮度がいまいちでした。。。ショック。

私が勝手にライバル視しているTweetDeckはTwitter Streaming APIなるものを使用しているらしく、Twitter側から検索結果がプッシュされるので、Twitter APIより新鮮なデータが取得できるようです。
そこで、今回は、Twitter Streaming APIを利用した検索クライアントを作成してみました。
TweetDeckと同じタイミングでつぶやきデータが受信できます。
2011/01/20 22:00 本文全体を加筆
Twitter Streaming API
Twitter Search APIによるつぶやき検索 (C#)では、クライアント側からデータを要求した分だけデータが返ってきますが、こちらはつぶやきが発生次第Twitterサーバからデータが届きます。
Streaming API Documentationに本家のドキュメントがありますが、私はTwitter API 仕様書 (勝手に日本語訳シリーズ)を活用させていただきましたw
・αテスト中につき、仕様変更される可能性あり
・BASIC認証、HTTP POST/GET使用
・APIによって取得件数等の制限のある/なしが決まっている
・ツイートのデータは、XML or JSON形式
filter API 概要
今回は任意のワードで検索できるfilter APIを使用しました。
URL:http://stream.twitter.com/1/statuses/filter.json
検索ワードは、track=検索ワード1,検索ワード2,,,,のように指定します。
Twitter Serch APIとは異なり、日本語(マルチバイト文字)の指定ができません(残念)。
返ってくるデータ形式はJSONのみとのこと。
json形式つぶやきデータ
受信したつぶやきデータの例です。
key:valueの形になっています。userとentitiesのデータは入れ子になっています。
つぶやき時刻は”created_at”、ユーザ名は”user”の中の”name”、つぶやきデータは”text”です。
{
"place":null,
"user":
{
"profile_background_tile":true,
"time_zone":"Hawaii",
"location":"",
"contributors_enabled":false,
"verified":false,
"listed_count":71,
"profile_link_color":"0084B4",
"show_all_inline_media":false,
"geo_enabled":false,
"profile_sidebar_border_color":"C0DEED",
"id_str":"0123456789",
"url":"http://hogehoge.com/",
"notifications":null,
"profile_use_background_image":true,
"created_at":"Sat Jan 09 15:11:04 +0000 2010",
"description":"hogehoge",
"profile_background_color":"C0DEED",
"profile_background_image_url":"http;//hogehoge.com/hoge.jpg",
"favourites_count":64,
"friends_count":919,
"followers_count":840,
"protected":false,
"lang":"ja",
"statuses_count":18866,
"profile_image_url":"http://twi.com/hoge.jpg",
"name":"hogename",
"follow_request_sent":null,
"following":null,
"profile_text_color":"333333",
"id":0123456789,
"is_translator":false,
"utc_offset":-36000,
"profile_sidebar_fill_color":"DDEEF6",
"screen_name":"karimas_P"
},
"in_reply_to_user_id":null,
"favorited":false,
"in_reply_to_status_id_str":null,
"contributors":null,
"in_reply_to_screen_name":null,
"text":"hogehoge",
"in_reply_to_user_id_str":null,
"id_str":"hogehoge",
"geo":null,
"retweeted":false,
"retweet_count":0,
"source":"u003Ca href="http://hogehoge.com" rel="hogehoge",
"created_at":"Thu Jan 20 12:18:01 +0000 2011",
"coordinates":null,
"truncated":false,
"id":01234567890,
"entities":{
"urls":[{
"indices":[37,62],
"expanded_url":null,
"url":"http://hogehoge.com"
}],
"hashtags":[{
"indices":[63,74],
"text":"sm13357394"}],
"user_mentions":[]
},
"in_reply_to_status_id":null
}
C#でJSONデータの解析
JavaScriptSerializer .Deserialize()メソッドを使って解析しています。
時刻データは実際にはクライアント側のローカル時刻に変換してやる必要があります(最終コードを参考にして下さい)。
string jsonData;//受信文字列を格納 //解析 JavaScriptSerializer serializer = new JavaScriptSerializer(); Dictionary<String, Object> dic = serializer.Deserialize<Dictionary<String, Object>>(jsonData); Dictionary<String, Object> usr = (Dictionary<string, Object>)dic["user"]; //データ取り出し string[] data = new string[3]; data[0] = (string)dic["created_at"];//時刻 data[1] = (string)usr["name"];//ユーザ名 data[2] = (string)dic["text"];//つぶやき本文
ソースコード
全体のコードです。
つぶやきデータのReadはスレッドtrd1で行なっています。
また、特にマイナーな検索ワードだと、ReadLine()メソッドがずっと
ブロッキングされるため、タイムアウト時間を指定しています。
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.Text.RegularExpressions;
using System.Threading;
using System.Net;
using System.IO;
using System.Web.Script.Serialization;
using System.Globalization;
namespace StreamingAPITest
{
public partial class Form1 : Form
{
string _SearchWord = ""; //検索ワード
bool _StateStarted = false;//アプリの状態(停止中/動作中)
bool _ReadTweetFlag;//true:つぶやきデータをReadし続ける
//通信用
Thread trd1 = null;
HttpWebRequest _Request;
WebResponse _Response;
StreamReader _StreamReader;
Stream _ReqStream;
Stream _ResStream;
//スレッド側からGUIコントロール更新用のdelegate
delegate void UpdateString(string text);
delegate void UpdateBool(bool value);
delegate void SetListView(string[] data);
public Form1()
{
InitializeComponent();
//dataGridViewの行ヘッダーの表示は不要
dataGridView1.RowHeadersVisible = false;
}
private void button1_Click(object sender, EventArgs e)
{
if (_StateStarted == false)
{
//停止中なので開始処理を行う
//検索ワードチェック
if (CheckWord(textBox1.Text) == false)
{
return;
}
//スレッド起動
_ReadTweetFlag = true;
trd1 = new Thread(new ThreadStart(ReadTweet));
trd1.IsBackground = true;
trd1.Start();
label1.Text = "接続しています...";
textBox1.Enabled = false;
button1.Enabled = false;
}
else
{
//動作中なので停止処理を行う
//スレッド停止
//ReadLine()処理後にスレッドを停止
_ReadTweetFlag = false;
//停止移行にする
label1.Text = "切断しています...";
textBox1.Enabled = true;
button1.Enabled = false;
}
}
//検索ワードチェック
bool CheckWord(string str)
{
try
{
if (str == "")
{
return false;//検索ワードなし
}
//半角英数か
Match result = Regex.Match(str, "^[a-zA-Z0-9!-~ ]+$");
if (result.Success == false)
{
MessageBox.Show("半角英数字で入力してください");
return false;
}
//前回から変更されたか
if (_SearchWord != str)
{
dataGridView1.Rows.Clear();//ビューをクリア
}
_SearchWord = str;
}
catch (Exception)
{
return false;
}
return true;
}
//つぶやきの取得、表示
void ReadTweet()
{
//ラベル更新関数
UpdateString updateLabel = (text) =>
{
label1.Text = text;
};
//ボタン更新関数
UpdateString updateButton = (text) =>
{
button1.Text = text;
button1.Enabled = true;
};
//テキストボックス更新関数
UpdateBool UpdateTextBox = (state) =>
{
textBox1.Enabled = (bool)state;
};
//POST
this.Invoke(updateLabel, "POSTしています...");
if (Post() == false)
{
this.Invoke(updateLabel, "POSTに失敗しました");
return;
}
//GET
this.Invoke(updateLabel, "GETしています...");
if (Get() == false)
{
this.Invoke(updateLabel, "GETに失敗しました");
return;
}
_StateStarted = true;
//GUI更新
this.Invoke(updateLabel, "接続しました");
this.Invoke(updateButton, "STOP");
//READ
ReadLoop();
//切断
this.Invoke(updateLabel, "切断しています...");
CloseConnect();
_StateStarted = false;
//GUI更新
this.Invoke(updateLabel, "切断しました");
this.Invoke(updateButton, "START");
this.Invoke(UpdateTextBox, true);
}
//POST
bool Post()
{
try
{
string id = "Your_Twitter_ID";
string password = "Your_Twitter_PASS";
string url = "http://stream.twitter.com/1/statuses/filter.json";
//HTTP POSTリクエストの作成
//trackによる検索(半角英数のみ対応。マルチバイト文字、記号は不可。コンマ区切り。)
string param = String.Format("{0}={1}", "track", textBox1.Text.Replace(' ', ','));
byte[] data = Encoding.UTF8.GetBytes(param);
_Request = (HttpWebRequest)WebRequest.Create(url);
_Request.Method = "POST";
_Request.ContentType = "application/x-www-form-urlencoded";
_Request.ContentLength = data.Length;
_Request.Credentials = new NetworkCredential(id, password);
//POSTを実行
_ReqStream = _Request.GetRequestStream();
_ReqStream.Write(data, 0, data.Length);
_ReqStream.Close();
}
catch (WebException e)
{
MessageBox.Show("Status Code : " + ((HttpWebResponse)e.Response).StatusCode);
MessageBox.Show("Status Description : " + ((HttpWebResponse)e.Response).StatusDescription);
return false;
}
catch (Exception e)
{
MessageBox.Show("Message :n" + e.Message);
MessageBox.Show("Type :n" + e.GetType().FullName);
MessageBox.Show("StackTrace :n" + e.StackTrace.ToString());
return false;
}
return true;
}
//GET
bool Get()
{
try
{
//GET実行
_Response = _Request.GetResponse();
_ResStream = _Response.GetResponseStream();
bool timeout = _ResStream.CanTimeout;
//注)Timeout値を設定しとかないと、
// つぶやきを受信するまでReadLineはブロッキングされる
_ResStream.ReadTimeout = 3 * 1000;
_StreamReader = new StreamReader(_ResStream, Encoding.UTF8);
}
catch (WebException e)
{
MessageBox.Show("Status Code : " + ((HttpWebResponse)e.Response).StatusCode);
MessageBox.Show("Status Description : " + ((HttpWebResponse)e.Response).StatusDescription);
return false;
}
catch (Exception e)
{
MessageBox.Show("Message :n" + e.Message);
MessageBox.Show("Type :n" + e.GetType().FullName);
MessageBox.Show("StackTrace :n" + e.StackTrace.ToString());
return false;
}
return true;
}
//READ
void ReadLoop()
{
//ビューへ追加用関数
SetListView setListView = (string[] data) =>
{
DataGridViewRow dgvr = new DataGridViewRow();
dgvr.CreateCells(dataGridView1);
dgvr.Cells[0].Value = data[0];
dgvr.Cells[1].Value = data[1];
dgvr.Cells[2].Value = data[2];
dataGridView1.Rows.Add(dgvr);
//最後の行までスクロール
dataGridView1.FirstDisplayedScrollingRowIndex = dataGridView1.RowCount - 1;
};
//受信ループ(json形式データ)
while (_ReadTweetFlag)
{
try
{
string result = _StreamReader.ReadLine();
if (result.Length <= 0)
{
continue;
}
//jsonデータをデコード
JavaScriptSerializer serializer = new JavaScriptSerializer();
Dictionary<String, Object> dic = serializer.Deserialize<Dictionary<String, Object>>(result);
//データチェック
if ((dic["created_at"] == null) || (dic["user"] == null) || (dic["text"] == null))
{
//情報が欠けている場合はスキップ
continue;
}
Dictionary<String, Object> usr = (Dictionary<string, Object>)dic["user"];
//データ取り出し
string[] data = new string[3];
//日付:"Thu Jan 20 10:45:54 +0000 2011"という形の文字列。
//DateTime.ParseExactでstring型からDateTime型に変換し、
//ToLocalTime()で自分の地域の日付とする。
//dddやMMMの変換のためCultureInfoも必要。
data[0] = DateTime.ParseExact((string)dic["created_at"], "ddd MMM dd HH:mm:ss +0000 yyyy", new CultureInfo("en-US")).ToLocalTime().ToString();
data[1] = (string)usr["name"];//ユーザ名
data[2] = (string)dic["text"];//つぶやき本文
//ビューに追加
this.Invoke(setListView, new object[] { data });
}
catch (Exception e)
{
if ((e.Message.Contains("タイムアウト") || e.Message.Contains("応答"))//タイムアウトっぽい例外
|| (e.Message.Contains("キー")))//情報欠けっぽい例外
{
//再びReadLine()する
}
else
{
//ループを抜ける
MessageBox.Show("Message :n" + e.Message);
MessageBox.Show("Type :n" + e.GetType().FullName);
MessageBox.Show("StackTrace :n" + e.StackTrace.ToString());
break;
}
}
}
}
//Close
void CloseConnect()
{
try
{
//通信を止める
if (_Request != null)
{
_Request.Abort();
}
if (_Response != null)
{
_Response.Close();
}
//ストリームを閉じる
if ((_ReqStream != null) && (_ReqStream.CanRead == true))
{
_ReqStream.Close();
}
if ((_ResStream != null) && (_ResStream.CanRead == true))
{
_ResStream.Close();
}
if (_StreamReader != null)
{
_StreamReader.Close();
}
}
catch (Exception e)
{
MessageBox.Show("Message :n" + e.Message);
MessageBox.Show("Type :n" + e.GetType().FullName);
MessageBox.Show("StackTrace :n" + e.StackTrace.ToString());
return;
}
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
try
{
if (_ReadTweetFlag == true)
{
//スレッド停止
//ReadLine()処理後にスレッドを停止
_ReadTweetFlag = false;
label1.Text = "切断しています...";
}
}
catch (Exception exp)
{
MessageBox.Show("Message :n" + exp.Message);
MessageBox.Show("Type :n" + exp.GetType().FullName);
MessageBox.Show("StackTrace :n" + exp.StackTrace.ToString());
}
}
}
}
ピンバック:Twitter Search APIによるつぶやき検索 (C#) « 夏研ブログ