C#でTwitterのStreaming APIを使ってリスト自動追加
- C# XboxInfoTwit - 09.11/05
XboxInfoTwitの認証数は現在450を越えて、近いうちに500には届きそうです。現在の実装はIEを裏で動かすという、しょーもないものになっていて、それに起因する不具合や、どうしょうもない点が幾つかあるため、クローラー部分は全面的に書き変えようと思っています。あと、エラーメッセージがド不親切とか至らない点だらけでした、すみません。そんな次期バージョンの作業は全然捗ってないのですが、せめて年内ぐらいには何とかしたいです。
@のお話
ゲーム名に@が含まれるものをポストする(例えばTHE IDOLM@STER)と、STERさんに@が飛んで迷惑。というお話を見たので検証してみました。@は行頭かスペース + @ + 数字/アルファベットのものがあると飛びます。つまり、@の前にアルファベットがあれば@は飛びません。なので、別にIDOLM@STERだからってSTERさんに@が飛びまくる、なんてことはありません。正規表現で表すと「(?<=^| )@[a-zA-Z0-9_]+」になります。ついでに、ハッシュタグのほうも軽く検証してみました。基本的には@と同じですが、英単語以外にもリンクが張られるようなので、正規表現は「(?<=^| )#[^ ]+」になるようです。
List
Twitterにリストが実装されました。そこで、XboxInfoTwitユーザーのリストを作ってみることにしました。手動で探して登録も大変なので、プログラムでクロールして追加していきましょう。パブリックイタイムラインからXboxInfoTwit利用者(Source=XboxInfoTwit)の人を片っ端からリスト登録するという方針で行きます。以下、C#でのTwitterストリーミングAPIの使用法と実際のコードになります。同じようなことをやりたい人は、適当に書き替えてどうぞ使ってください。突っ込みどころ多数なのでむしろ突っ込んで欲しい……。
2010/4/29 追記:このコードはストリームAPIの利用法にしては冗長すぎるので、書き直しました → neue cc - C#とLinq to JsonとTwitterのChirpUserStreamsとReactive Extensions ストリームAPI取得コードを参考にする場合は、新しいほうを見てください。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Net; using System.IO; using System.Xml.Linq; using System.Collections.Specialized; using System.Threading; using System.Xml; static class Program { // ザ・決めうち文字列s const string UserName = "neuecc"; // 自分のアカウントのユーザー名を const string Password = "password"; // 同じくパスワードを const string ListName = "xboxinfotwitusers"; // リストを、入力ですです const string StreamApi = "http://stream.twitter.com/1/statuses/sample.xml"; const string ListMembersApiFormat = "http://twitter.com/{0}/{1}/members.xml"; /// <summary>指定リストにメンバーを追加する</summary> static void AddMemberToList(string userName, string listName, int id) { var url = string.Format(ListMembersApiFormat, userName, listName); var wc = new WebClient { Credentials = new NetworkCredential(UserName, Password) }; wc.UploadValues(url, new NameValueCollection() { { "id", id.ToString() } }); } /// <summary>指定リストのメンバーIDを全て取得する</summary> static IEnumerable<int> EnumerateListMemberID(string userName, string listName) { var format = string.Format(ListMembersApiFormat, userName, listName) + "?cursor={0}"; var cursor = -1L; var xmlReaderSettings = new XmlReaderSettings { XmlResolver = new XmlUrlResolver { Credentials = new NetworkCredential(UserName, Password) } }; while (true) { using (var xr = XmlReader.Create(string.Format(format, cursor), xmlReaderSettings)) { var xEle = XElement.Load(xr); foreach (var item in xEle.Descendants("user").Select(x => (int)x.Element("id"))) { yield return item; } cursor = long.Parse(xEle.Element("next_cursor").Value); if (cursor == 0) yield break; } } } /// <summary>ストリームAPIのパブリックタイムラインから無限に取得</summary> static IEnumerable<XElement> EnumeratePublicTimeline(StreamReader reader) { while (true) { var xmlString = reader.EnumerateLines() .TakeWhile(s => s != "<?xml version=\"1.0\" encoding=\"UTF-8\"?>") .Join(); if (xmlString == "") continue; yield return XElement.Parse(xmlString); } } /// <summary>サーバーが死んでないか確認</summary> static bool IsServerStatusOK() { var req = WebRequest.Create("http://twitter.com/help/test.xml"); HttpWebResponse res = null; try { res = (HttpWebResponse)req.GetResponse(); if (res.StatusCode == HttpStatusCode.OK) return true; } catch (WebException e) { Console.WriteLine(e); } finally { if (res != null) res.Close(); } // どうでもいいと思っていたり return false; } static void Main(string[] args) { ServicePointManager.Expect100Continue = false; // おまじない(笑) var count = 0; // モニタリング用のカウント変数(動作的には別に使わない) var following = new HashSet<int>(EnumerateListMemberID(UserName, ListName)); var webRequest = (HttpWebRequest)HttpWebRequest.Create(StreamApi); webRequest.KeepAlive = true; webRequest.Credentials = new NetworkCredential(UserName, Password); LOOP: using (var res = webRequest.GetResponse()) using (var stream = res.GetResponseStream()) using (var reader = new StreamReader(stream)) { try { // 例外が発生しなければ、無限リピートになっているのでこの部分を永久に続けます EnumeratePublicTimeline(reader) .Do(_ => { if (++count % 100 == 0) Console.WriteLine("{0} : {1}", DateTime.Now, count); }) // 確認表示用 .Where(x => x.Name == "status") .Select(x => new { Source = x.Element("source").Value, ID = (int)x.Element("user").Element("id"), Name = x.Element("user").Element("screen_name").Value }) .Where(a => a.Source.Contains("XboxInfoTwit")) .Do(a => Console.WriteLine("Found:{0}", a.Name)) // ここでも確認表示用 .Where(a => following.Add(a.ID)) .ForEach(a => { AddMemberToList(UserName, ListName, a.ID); Console.WriteLine("{0} : {1} : {2}", a.Name, DateTime.Now, count); // 確認表示用 }); } catch (IOException e) { Console.WriteLine(e); // 接続が閉じられてたりするのでー。 while (!IsServerStatusOK()) { Thread.Sleep(TimeSpan.FromMinutes(5)); // サーバー死んでたら5分間お休み } } finally { webRequest.Abort(); // これ呼ぶ前にCloseするとハング } } goto LOOP; // goto! goto! } // Extension Methods public static IEnumerable<string> EnumerateLines(this StreamReader streamReader) { while (!streamReader.EndOfStream) { yield return streamReader.ReadLine(); } } public static string Join<T>(this IEnumerable<T> source) { return source.Aggregate(new StringBuilder(), (sb, s) => sb.Append(s)).ToString(); } public static void ForEach<T>(this IEnumerable<T> source, Action<T> action) { foreach (var item in source) { action(item); } } public static IEnumerable<T> Do<T>(this IEnumerable<T> source, Action<T> action) { foreach (var item in source) { action(item); yield return item; } } }
ストリーミングAPIとは無関係のリスト関連の処理や、投げやりなtry-catch-gotoがあって、ちょっとゴチャゴチャしてますが、基本的にはusing三段重ねの部分だけです。無限にXMLが継ぎ足されてくるので、接続を切らさずひたすらReadLine。XMLの切れ目は、XML宣言部を使うことにしましたが、今一つスマートではないです。文字列にしてParseってのもあまり良い感じじゃなく。あ、あと例外処理は全然出来てませんので何かあると平然と死にます。
コードは、書き捨て感全開。例によって何でもLinq、何でもIEnumerable。コレクションになりそうな気配があると、すぐにじゃあyieldね、と考える癖がついてしまっていて。細かいことは後段に任せればいーんだよ、というのが楽ちんでして。リストメンバー全件取得の部分なんかは、わりとスマートに書けてるかと思うのですがどうでしょう。
なお、このストリーミングAPIは全件を漏れなく取得出来るわけではないので、それなり、というかかなり漏れが出ます。なのでXboxInfoTwit使ってるのに登録されねーぞ、という場合は、しょーがない。です。そのうち登録されると思います。あと、このプログラムはサーバー上で24時間動かしているわけじゃなく、私のローカルPC上で動かしているだけなので、私の気まぐれで動かしてたり動かしてなかったりします。私が寝てる間はPCがウルサイので動いてませんし、私が家に居ない時は省エネのために動いてません。なので、むしろ登録されるほうが珍しいです。レアです。効率的には20000件に1人登録出来るか出来ないか、って感じでした。一時間に一人見つかるかどうかも怪しいぐらいの頻度。とてもレア。ぶっちゃけgoogle経由で引っ張ってくるとかしたほうが遙かに効率良さそうですが、まあ、Streaming API使ってみたかったというだけなので。
そういえばですが、逆にリストに登録されてUZEEEE、という場合は、現状はリスト機能がベータのせいなのか拒否は出来ないようです。すみません。UZEEEE、と思っても我慢してください。どうしても嫌な場合は私の方にメッセージをくれれば、リストからの撤去と、プログラムから以後の追加をしないようなコードを入れたいと思っています。
