C#でTwitterのStreaming APIを使ってリスト自動追加

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、と思っても我慢してください。どうしても嫌な場合は私の方にメッセージをくれれば、リストからの撤去と、プログラムから以後の追加をしないようなコードを入れたいと思っています。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive