Archive - C#
はてなダイアリー to HTML
- C# - 10.03/09
はてなダイアリーの記事を根こそぎ取得してローカルHTMLに保存するアプリケーションです。過去ログを全部取得して昇順に並び替えます。カテゴリ指定も可。 本文抽出アルゴリズムだなんて高尚なことはせず、HTMLをそのまま切り出しているだけのはてなダイアリー完全特化なぶんだけ、デザインやsyntax-highlightなどもそのままで見ることができます。上の画像はNyaRuRuの日記(勝手に貼ってすみません)の.NETカテゴリーを抽出しているところ。私がC#やLinqを覚えられたのはNyaRuRuさんの日記のお陰といっても過言ではなく、しかも読み返す度に新しい発見があって本当に素晴らしい。ので、度々読み返しているのですが、はてな重い。重い。なら全部ぶっこぬけばいいぢゃない。というのが作った理由でして……。
あと、最近こそこそごそごそとC++も勉強中なので、 [C++] - Cry’s Diaryや [C++] - Faith and Brave - C++で遊ぼうを読むと、(大体は全く分からないのですが)勉強になります。なお、Permalinkは相対パスになってしまい使えないのですが、日付の部分は絶対パスなので、コメント見たくなったりPermalinkを取りたくなったら日付から辿れます。
こうしてHTMLを自炊(?)すると、電子ブックリーダー欲しくなりますね。それと、リーダーはやっぱブラウザが載ってないとダメよねー。PDF(と独自形式?)だけ見れても嬉しくぁない。そんなに本には興味ない。HTMLが見たいのです。Twitterのログが見たいのです。2chまとめサイトが見たいのです。海外の技術書は結構PDFで買える感じなのでそれはそれで気になるところですが――。
以下ソースコード。↑のzipにも同梱してありますが。コンパイルにはSGMLReaderが必要です。
static class Program { static IEnumerable<T> Unfold<T>(T seed, Func<T, T> func) { for (var value = seed; ; value = func(value)) { yield return value; } } const string HatenaUrl = "http://d.hatena.ne.jp"; static void Main() { Thread.GetDomain().UnhandledException += (sender, e) => { Console.WriteLine(e.ExceptionObject); Console.ReadLine(); }; Console.WriteLine("抽出対象のはてなIDを入力してください"); var id = Console.ReadLine(); Console.WriteLine("カテゴリを入力してください(全ての場合は空白)"); var word = Console.ReadLine(); Console.WriteLine("出力ファイル名を入力してください"); var fileName = Console.ReadLine(); // 抽出クエリ! var root = XElement.Load(new SgmlReader { Href = HatenaUrl + "/" + id + ((word == "") ? "" : "/searchdiary?word=*[" + Uri.EscapeDataString(word) + "]") }); var contents = Unfold(root, x => { var prev = x.Element("head").Elements("link") .FirstOrDefault(e => e.Attribute("rel") != null && e.Attribute("rel").Value == "prev"); if (prev == null) return null; retry: try { var url = HatenaUrl + prev.Attribute("href").Value; Console.WriteLine(url); // こういうの挟むのビミョーではある return XElement.Load(new SgmlReader { Href = url }); } catch (WebException) // タイムアウトするので { Console.WriteLine("Timeout at " + DateTime.Now.ToString() + " wait 15 seconds..."); Thread.Sleep(TimeSpan.FromSeconds(15)); // とりあえず15秒待つ goto retry; // 何となくGOTO使いたい人 } }) .TakeWhile(x => x != null) .SelectMany(x => x .Descendants("div") .Where(e => e.Attribute("class") != null && e.Attribute("class").Value == "day")) .TakeWhile(e => !Regex.IsMatch(e.Value, @"^「\*\[.+\]」に一致する記事はありませんでした。検索語を変えて再度検索してみてください。$")) // 間違ったカテゴリ入力した時対策 .Reverse(); // 古いのから順に見たいので // style抽出 var styles = root.Element("head").Elements("link") .Where(e => e.Attribute("rel").Value == "stylesheet") .Select(e => { e.SetAttributeValue("href", HatenaUrl + e.Attribute("href").Value); return e; }) // 副作用ダサい .Concat(root.Element("head").Elements("style")); // HTML組み立て! var html = new XStreamingElement("html", // まあ、Reverseでバッファに貯めるので焼け石に水ですけどね、XStreamingElement new XStreamingElement("head", styles), new XStreamingElement("body", // new XElement("div", new XAttribute("class", "hatena-body"), サイドバーとか邪魔なので無視 // new XElement("div", new XAttribute("class", "main"), new XStreamingElement("div", new XAttribute("id", "days"), contents))); // 保存 var path = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().FullName), fileName + ".html"); var xws = new XmlWriterSettings { Indent = true, CheckCharacters = false }; // 不正な文字のあるサイトを書き出すと落ちるので防止 using (var xw = XmlWriter.Create(path, xws)) { html.Save(xw); } } }
try-catchが出るとゴチャついて嫌。なのだけど、しょうがないか。それと Unfoldはどうしたものかねえ。極力、標準演算子のみで済ませたいんですが、今回はちょっと使わざるを得なかったと思っています。次のページのURLを得るには、取得したHTMLから解析しなければならない。下流で解析し取得した次のURLは、上流に渡さなきゃいけない。のですが、通常は下から上に渡せないのがLinqなのよね。そんな場合、外部変数を介して渡すか、Unfoldか、場合によってはScanなんかを使うかになるわけで、とにかく外部変数は避けたかったのでUnfoldを使いました。
HTMLへの書き出し部分では物珍しい XStreamingElementを使ってみました。今回はただ単に書き出すだけなので、通常のXElementのようにメモリ内にツリーを保持する必要はないし、相当大きいXmlを扱うため効率も気になってくるところ。そこで遅延ストリーム書き込みを可能にするXStreamingElementの出番です。詳しくは 方法: 大きな XML ドキュメントのストリーミング変換を実行する をどうぞ。とはいっても、このプログラムでは反転させるためReverseでバッファに全て溜め込んでいるので、まあ……。XStreamingElementって言いたいだけちゃうんか、みたいな。
デザインはdiv class=hatena-bodyとdiv class=mainを抜いているので(不必要なサイドバーの描画を除去するため)、この二つに依存するCSSが書かれているサイトの場合はデザインが崩れることがあります。ちなみにneuecc clipはこの二つどころか、その他にもwrapperを置いているというデタラメなCSS構造をしているため、デザインは保存出来ません。全くもって酷い。もっとスクレイピングに優しいHTMLを書かないとダメですな。
HTMLへの書き出し部分ははまりどころでした。最初Save(fileName)で保存していたんですが、特定のサイトの特定の部分で落ちてしまって困りました。具体的には 2008-07-23 - Faith and Brave - C++で遊ぼう で(例に出してすみません)、Protocol Bufferによる出力結果がInvalidXmlCharに引っかかってアウト、のようです。回避する方法は、XmlWriterSettingsのCheckCharactersをfalseに設定したXmlWriterを生成して書き出せばOK。
C#でスクレイピング:HTMLパース(Linq to Html)のためのSGMLReader利用法
- C# - 10.03/02
Linq to XmlがあるならLinq to Htmlもあればいいのに!と思った皆様こんばんは。まあ、DOMでしょ?ツリーでしょ?XHTMLならそのままXDocument.Loadで行けるよね?XDocument.Parseで行けるよね? ええ、ええ、行けますとも。XHTMLなら、ね、ValidなXHTMLならね。世の中のXHTML詐称の99.99%がそのまま解析出来るわけがなく普通に落ちてくれるので、XDocumentにそのまま流しこむことは出来ないわけです(もちろん、うちのサイトも詐称ですよ!ていうかこのサイトのHTMLは酷すぎるのでそのうち何とかしたい……)。
そこでHtmlを整形してXmlに変換するツールの出番なわけですが、まず名前が上がるのがTidy、の.NET移植であるTidy.NETで、これは論外。とにかく面倒くさい上に、パースしきれてなくてXDocumentに流すと平然と落ちたりする。おまけにXDocumentに入れるには文字列にしてから入れる必要があって二度手間感がある、などなど全くお薦めできません。XboxInfoTwitはTidy使ってますが、後悔してますよ……。
次にHtml Agility Packで、これは中々良いです。Linq to Xml風で大変使いやすい。のですが、あくまで風味であって、なんで本物のLinq to Xmlが目の前にあるのに、それっぽく模したものを覚えなきゃいけないの?二度手間で面倒くさいよ。
そこで、第三の選択としてSGMLReaderを使うという方法を提案します。SGML Reader自体は古くからあるのですが、日本語での情報はあまりないみたいだしLinq to Xmlと組み合わせたスクレイピング用途、に至っては皆無のようなので、ここで紹介しましょう。とりあえず例を。
// たったこれだけのメソッドを用意しておけば static XDocument ParseHtml(TextReader reader) { using (var sgmlReader = new SgmlReader { DocType = "HTML", CaseFolding = CaseFolding.ToLower }) { sgmlReader.InputStream = reader; // ↑の初期化子にくっつけても構いません return XDocument.Load(sgmlReader); } } static void Main(string[] args) { using (var stream = new WebClient().OpenRead("http://www.bing.com/search?cc=jp&q=linq")) using (var sr = new StreamReader(stream, Encoding.UTF8)) { var xml = ParseHtml(sr); // これだけでHtml to Xml完了。あとはLinq to Xmlで操作。 XNamespace ns = "http://www.w3.org/1999/xhtml"; foreach (var item in xml.Descendants(ns + "h3")) { Console.WriteLine(item.Value); // bingでlinqを検索した結果のタイトルを列挙 } } }
見たとおり、信じられないほど簡単です。SgmlReaderはXmlReaderを継承しているため、XDocument.Load(xmlReader)にそのまま流し込めます。また、SgmlReader自体もDocTypeとInputStreamを設定するだけという超簡単設計になっているため、楽にHtml to Xmlが実現。HtmlはXDocumentになってしまえさえすれば、あとは慣れ親しんだLinq to Xmlの操作で抽出していけます。
例では、BingでLinqを検索した結果の検索結果見出し部分を抽出しています。見出しはh3で囲まれているので、Descendants(ns + “h3″)。以上。超簡単。C#のスクレイピングの簡単さはRubyも超えたね!
残る問題は、日本語を扱う際はエンコーディング周りの設定が面倒くさい(間違ったエンコーディングだと文字化けする)、ということなのですが、そのWebClientのエンコーディング問題は、.NET Framework 4.0から修正された System.Net.WebClient は、HTTP ヘッダーから Encoding を自動的に認識してほしい | Microsoft Connect らしいです。素晴らしい!提案して頂いたbiacさんに感謝。
追記(より簡単に)
上の記事を書いてから気づいたのですが、HrefプロパティにURLを指定するだけで、中でStream類を作って自動的にHtmlだと判別してくれるようです。更には、エンコーディングもContentTypeを見て自動調整してくれます(詳しくはSgmlParser.csのOpenメソッドを参照)。よって、もっとずっと簡単に書けます。
static void Main(string[] args) { XDocument xml; using (var sgml = new SgmlReader() { Href = "http://www.xbox.com/ja-JP/games/calendar.aspx" }) { xml = XDocument.Load(sgml); // たった3行でHtml to Xml } // Xboxの発売スケジュールからタイトルと発売日を抜き出してみる var ns = xml.Root.Name.Namespace; var query = xml.Descendants(ns + "table") .Last() .Descendants(ns + "tr") .Skip(1) // テーブル一行目は項目説明なので飛ばす .Select(e => e.Elements(ns + "td").ToList()) .Select(es => new { Title = es.First().Value, ReleaseDate = es.Last().Value }); // 書き出し foreach (var item in query) { Console.WriteLine(item.Title + " - " + item.ReleaseDate); } }
usingの辺りが若干鬱陶しいので、最初の例のようにメソッドに切り出してもいいかもしれません(CaseFolding.ToLowerも付けたいし)。Loadし終わったらストリームはもう不要です。XDocumentはメモリ内に全部構築するタイプのもので、実質XmlDocument(DOMツリー)の代替となっています。抽出時のXml名前空間ですが、サイトによってついていたりついていなかったりするので、var ns = xml.Root.Name.Namespaceとしておくと、全てのサイトに対応出来ます。
抽出のテクニック
上の例は Xbox.com | Xbox ゲームソフト 発売スケジュールからタイトルと発売日を抽出するというものです。定期的にページを監視して、更新されたらTwitterに投稿するXbox発売予定BOTとか作れますね!Xmlで取れるAPIさえあれば……なんてことはなくなりました!これからは 全てのサイトが易々とスクレイピング可能な代物として浮き上がってきますな。
ただし、元がHTMLのものはAPIとして用意されているXMLと違って、抽出に優しくない構造をしています。 Descendants一発でOk、というわけにもいかないので若干の慣れは必要かもしれません。今回の例の抽出コードが何やってるかよくわからない、という人はHTMLソースと見比べてみてください。目的のTableに辿り着くにも、いくつかの方法があります。決め打ち成分が入ってしまうのはどうにもならないのですが、何を決め打ちにするのがスッキリ書けるのか、となると色々です。今回はTableがLastである、という点を使いましたが、他の方法を考えてみると
var case2 = xml.Descendants(ns + "div") .Where(e => e.Attribute("class") != null && e.Attribute("class").Value == "XbcWpFreeForm1") .SelectMany(e => e.Descendants(ns + "tr")); var case3 = xml.Descendants(ns + "table") .First(x => x.Ancestors().Any(e => e.Attribute("class") != null && e.Attribute("class").Value == "XbcWpFreeForm1")) .Descendants(ns + "tr");
目的のTableを囲むdivのclassが XbcWpFreeForm1であり、XbcWpFreeForm1が適用されているdivは一つしかない、ということに着目するとこうなります。case2は、divを全て列挙して探し出す方法。First(predicate)ではなくWhere.SelectManyにすることで、目的のTableが複数個ある場合でも対応出来ます。case3はTableに絞った上で、そのTableの上位階層(Ancestors)にXbcWpFreeForm1が含まれるかを探し当てる方法。Last、という決め打ちが難しい(場合により変動するケース)場合には有効でしょう。この、上位階層(Ancestors)や下位階層(Descendants)の要素に特異な要素はないか(Any)、と探す手法は、ターゲット自体に特徴がなく抽出し難い場合に活用出来ます。
そもそもDescendantsなんて富豪すぎて許せん、実行効率命!という人はElement(”body”).Element(”div”)….とトップから掘っていってもいいわけですが、さすがにElementの連続はダルいのでXPathを使うのもよいでしょう。Linq to XmlでのXPathの利用法は、以前neue cc - そしてXPathに戻るに書きました。XPathは、複雑なことを書こうとすると暗号めいた感じになるから余りすきじゃないですね。私は多少冗長なぐらいでもLinqで書くのが好きだなあ。
何故C#には(Javaの)Collections.sortに相当するものがないのか
Java は Collections.sort があるのに .NET は List 自身が Sort メソッドを持っているのはなぜ?
だそうです。あまり疑問でもないです。だってIListはSortないし。比較するならIList - ListでありList
そして何故かスレッドの話は迷走していて不思議。パフォーマンス? んー……。勝手な印象論ですみませんが、1さん(名前を出すのもアレなので1さん、ということにさせてください)はJavaのCollections.sortの性能を勘違いしているんじゃないかな? 配列のコピーを問題にしてるようだけど、Javaの方式はコピー、してますよ。もっとも最初の方の発言(効率は重要じゃない)と後ろの発言(コピーが嫌)が矛盾していますが。ともかく、実際のSunの実装を見てみましょう。
public static <T extends Comparable<? super T>> void sort(List<T> list) { Object[] a = list.toArray(); Arrays.sort(a); ListIterator<T> i = list.listIterator(); for (int j=0; j<a.length; j++) { i.next(); i.set((T)a[j]); } }
toArrayしてコピー。それをsortして、forで詰め替え。C#で同じような外部Sort関数を書くのならば以下のようになります。厳密には違いますが、それは最後に述べます。
static void Sort<T>(IList<T> list) { T[] array = new T[list.Count]; list.CopyTo(array, 0); Array.Sort(array); // List<T>のSortもArray.Sortを利用しています for (int i = 0; i < array.Length; i++) { list[i] = array[i]; } }
forで詰め替えている様は、マジマジと見ると微妙。実際、このコードには問題があります。クイズだと思って、どこが問題なのか考えてみてください、答えは最後に述べます。
List<T>のSortは、内部に配列を持っているため、CopyToとforでの詰め替えが不要です。パフォーマンスで言えば理想的な形になっているわけです。 ゲッタとセッタ経由でソートの入れ替えすればいいぢゃん、というのはそうですね、違います。配列のほうが速いし!というのもそうでしょうが、それ以前にListインターフェイスの実装は自由です。例えばLinkedListのゲッタを考えてみたらどうでしょう。getの度に前(もしくは後ろ)から走査があるため、パフォーマンスが悲惨な事になるのは容易に想像出来ます。ListはArrayListだけじゃないので、ゲッタ/セッタに頼るのは無理がある。汎用的に、Listインターフェイスならば何でも受け入れるという設計にする以上、コピーを作るのは不可避です。
というわけで、パフォーマンス云々を言うならば、内部を知っているListクラスがSortを持つのはベストな選択でしょう。そして利便性を考えてもベストな選択。ならばいったい何処に不満があるのでしょうか?
Javaのほうが優れているのは、「自前でIListを実装したクラス」を破壊的ソートするのに、クラスにSortメソッドを用意したくない。といったところでしょうか。Sortの実装自体は通常はArray.Sortを呼ぶだけなので簡単なので別に手間でもないのですけどね……。というか、この手の基本アルゴリズムを自前実装したのを使うのは悪です(勉強用に、なら当然すべきで悪なのは自前実装の妄信です)。
ようするところ、Sortメソッドを自前で用意したくないけど破壊的ソートが欲しい、ということになる。ふーむ、個人的にはなくてもいいかな。非破壊的なソートがLinqのOrderByを使うことで可能なので、破壊的のほうを欲しいとはあまり思わない。
ListIterator
では本題。何でC#には破壊的ソートをしてくれる外部関数がないの?というと、インターフェイスの都合上、不可能だから。が理由だと私は考えています(そもそも必要性薄いから、が最大の理由だと思いますがそれはそれとして)。Javaのsortと私の書いたC#のSortを見比べてください。大きな違いがあります。それは、forでソートした配列をリストに詰め直している部分。
i.set((T)a[j]); // Java list[i] = array[i]; // C#
C#の場合、listがLinkedListのようなものだった場合は悲惨なことになります。それに比べて、JavaではListIteratorを用いているため、配列と同じ処理効率で値をセット出来ます。C#には、このJavaのListIteratorに相当するものがないので、全てのIListに対して問題なく性能を発揮する破壊的ソート関数を作成することは不可能です。
「LinkedListのようなもの」という歯切れの悪い言い方をしたのは、.NETのLinkedList
ただ、IListのインデクサは取得にコストがかからないことを期待、してもいいとは思います。そんなのを一々気にしてたら何も作れない。それにちゃんと、.NETのLinkedListはIListじゃないしね。ね。というわけで上のほうで出したクイズは、問題があるかないかは何とも言えない微妙ラインです。んーと、つまりはfor(int i;i < hoge.length(); i++)の問題点はどこだー!みたいな話で、基本的にはhoge.length()なんてコストがかからないのを期待して問題ないし、コストがかかるんならそのクソクラスが悪い、みたいな。
ついでに個人的な意見ですがListインターフェイスにListIteratorはそこまで必要ではない。普通のIteratorと機能がかなり被る割には、使う機会はとても少ない。おまけに、ListIteratorのsetってoptionalで、実装されていることが保証されてない。この手の、実装しなくてもいいインターフェイスって撲滅した方がいいと思うんですけどねー。私は怖くて呼べません。
とはいえ、保証されないインターフェイスを完全に撲滅など出来はしません。例えばList(Java)/IList(C#)のAddは実行出来ることが保証されていない。JavaならArrays.asList(1, 2, 3).add(4)を、C#なら(array as IList
まとまってませんが、結論としては疑問に思ったらソース読むのが手っ取り早い、とかそんなところで。Javaの良いところはC#に比べてフレームワークのコードへのアクセスが簡単なところですね。C#は部分的にはコードは公開されていて大変タメになるのですが、色々と面倒くさいし、公開されてない範囲も少なくないし……。.NET Reflectorのお世話になりまくってイリーガルな気分を味わうのはもう嫌ぽ。嘘。リフレクタ大好きですがそれはそれ。
AnonymousComparer - ver.1.2.0.0
- C# - 10.02/04
AnonymousComparerを再度バージョンアップしました。ダウンロードは上記リンク先、CodePlexからどうぞ。バグがなければ、これで最後だと思います。いやもう内容的には出尽くしたかな、と。更新内容はIComparer<T>を作成可能にしました。また、OrderByでIComparer<T>を利用するものへ、拡張メソッドを追加しました。
// こんなシーケンスがあるとして、IComparer<T>を使用してその場で自由に比較を指定したい var seq = new[] { 1, 2, 3 }; // IComparer<T>を作る var comparer = AnonymousComparer.Create<int>((x, y) => y - x); seq.OrderBy(x => x, comparer); // OrderBy/ThenByに拡張メソッドが追加されているので、型推論が効いたまま書けます // List.Sort(Comparison)みたいなイメージですかね seq.OrderBy(x => x, (x, y) => y - x); // 3, 2, 1 seq.OrderByDescending(x => x, (x, y) => y - x); // 1, 2, 3
LinqにはDescendingが用意されているので、あまり使い道はなさそうですね。私もOrderByのICompare<T>オーバーロードを使いたいと思ったシチュエーションが今までにありませんし……。第一引数がkeySelectorなので、それで十分用を足せちゃうのですよね。それにしても、DescendingでIComparerを指定した場合の結果は紛らわしくていかんですな。
さて、更新内容はもう一つあって、むしろこっちのほうが重要なのですが、compareKeySelectorを利用したオーバーロード(Linq演算子への拡張メソッドは全部それです)で、シーケンスにnullが含まれている場合にnullで落ちるのを修正しました。今回からはヌルぽで落ちません。どういうこっちゃ、というと説明しづらいのでコードで。
class MyClass { public int MyProperty { get; set; } public override string ToString() { return "Prop = " + MyProperty; } } static void Main() { var array = new[] { new MyClass{MyProperty=1}, null, new MyClass{MyProperty=2}, null, new MyClass{MyProperty=1} }; var r1 = array.Count(); // 5 var r2 = array.Distinct().Count(); // 4 (nullが重複として消える) foreach (var item in array.Distinct(mc => mc.MyProperty)) { Console.WriteLine((item == null) ? "ヌルぽ" : item.ToString()); } // 出力結果は // Prop = 1 // ヌルぽ // Prop = 2 }
といった感じです。分かったような分からないような?
AnonymousComparer - ver.1.1.0.0
- C# - 10.01/26
AnonymousComparerをバージョンアップしました。ダウンロードは上記リンク先、CodePlexからどうぞ。更新内容はCreateのオーバーロードに追加して、IEqualityComparerの完全模写を可能にしました。今まではキー選択だけだったのですが、今回からはEqualsとGetHashCodeを個別に指定することが可能です。
var myClassComparer = AnonymousComparer.Create<MyClass>( (x, y) => x.MyProperty == y.MyProperty, // Equals obj => obj.MyProperty.GetHashCode()); // GetHashCode
こんな感じに指定します。全くもってそのままです。Equalsとかをラムダ式で指定するというだけです。型推論は効きませんので指定してやってください。なお、使い方自体は全然変わってませんので、普通の使い方は初回リリース時の記事を参照ください。
以下オマケ。
// ランダムでtrueかfalse返すComparer var rand = new Random(); var randomComparer = AnonymousComparer.Create<MyClass>( (_, __) => rand.Next(0, 2) == 0, _ => 0); // 間引くのに使えるぜ! var mabiita = list.Distinct(randomComparer).ToArray(); // とか思ったけど、最初の一つ目は必ず選択されるのよね(Distinctなので当然……) // といった、特殊な間引き方をしたい人はどうぞ(いません) // 普通にやるならWhereでランダムにフィルタリングするのを選びます
色々と用途が考えられるようで、そもそもEqualityComparer自体があまり使う状況ってないので、使い道が考えられない微妙な感じが素敵です。まあ、AnonymousComparer自体はふつーに使う分にはふつーに便利ですので、Linqのお供にどうぞ。もう一つ、今度は役に立つ例でも。
class MyClass { public int Prop1 { get; set; } public string Prop2 { get; set; } public string Prop3 { get; set; } } static void Main() { var array = new[] { new MyClass{Prop1=30, Prop2="hoge", Prop3="huga"}, new MyClass{Prop1=100, Prop2="foo", Prop3="bar"}, new MyClass{Prop1=100, Prop2="hoge", Prop3="mos"}, new MyClass{Prop1=30, Prop2="hoge", Prop3="mos"} }; // Prop1とProp2が一致するのだけ省きたい!といった複数キー指定は匿名型を作る array.Distinct(mc => new { mc.Prop1, mc.Prop2 }); }
複数キーで比較したい時は、匿名型を作るのが手っ取り早いです。こういった用途に匿名型を使うというテクニックは、Linqの他のところでも結構出てくるので覚えておくと便利です。富豪的?気にしない気にしない。
C#(.NET Framework)の文字列連結について
- C# - 10.01/07
一般に文字列を+=で連結するのは遅いと言われています。事実そのとおりで、多量の連結を+=で行うと死ぬほど時間がかかります。例えば、以下のようなコードで示されることが多いでしょう。
// ベンチマーク用関数(10万回実行) Func<Action, TimeSpan> bench = action => { var sw = Stopwatch.StartNew(); for (int i = 0; i < 100000; i++) { action(); } return sw.Elapsed; }; // StringBuilderの計測(最後のToStringを入れてませんが、あまり変わらないのでスルー) var sb = new StringBuilder(); var sbTime = bench(() => sb.Append("hoge")); // stringの+=での計測 var s = ""; var stringTime = bench(() => s += "hoge"); Console.WriteLine(sbTime); // 0.004sec Console.WriteLine(stringTime); // 14.97sec
0.004secと15secでは話になりません(正確には、StringBuilderでは最後に文字列に変換するToStringを入れるべきですが、それでも1secは超えなくて差は歴然なので省略します)。ならば、文字列を連結する場合は、どのような時でもパフォーマンスのためにStringBuilderを使うべきでしょうか? 答えは違います。
// ILではひとつにまとまる // IL_0001: ldstr "abcde" var s = "a" + "b" + "c" + "d" + "e";
定数の連結はコンパイル時にひとまとめにされるので、StringBuilderを使うのは愚かな選択となります。この辺はILDASMで見ればわかるし、Reflectorでもひとまとめになって展開されているのが確認できます。では定数ではなく動的に値を返すものは?
static string Get() { return DateTime.Now.ToString(); } static void Main(string[] args) { // IL_0031: call string [mscorlib]System.String::Concat(string[]) var s = Get() + Get() + Get() + Get() + Get(); }
ILを見ると、s += Get(); s += Get(); みたいな展開のされかたにはならず、String.Concat(string[])が呼ばれることになります。よって、速度を心配してStringBuilderを使う必要は全くありません。測定してみましょう。(ちなみにs+=Get()だとString.Concat(string,string)が大量に呼ばれることになるのが遅い理由)
// benchとGetは上で使ったのと同じものを流用 var sbTime = bench(() => { var sb = new StringBuilder(); sb.Append(Get()) .Append(Get()) .Append(Get()) .Append(Get()) .Append(Get()); var s = sb.ToString(); }); var stringTime = bench(() => { var s = Get() + Get() + Get() + Get() + Get(); }); Console.WriteLine(sbTime); // 0.65sec Console.WriteLine(stringTime); // 0.64sec
速度はほとんど変わりません。StringBuilderとString.Concatでは処理の中身は結構違いますが、速度変わらないのならどっちでもいいよね。なら、記述しやすいほうを選ぶのが良いでしょう。妄信的にパフォーマンスのためにStringBuilder!とか思っている人は、少し考え直してみてください。そんなの当たり前だろ常識的に考えて、と思っていた時期が私にもありました……。世の中は存外StringBuilder神話に溢れているかもですよ? いやほんと。あとC#なら逐語的リテラル文字列もお忘れなく。
AssemblyInfoの取得
- C# - 09.12/31
public sealed class AssemblyInfo { public string FileName { get; private set; } public string Version { get; private set; } public string FileVersion { get; private set; } public string Title { get; private set; } public string Description { get; private set; } public string Configuration { get; private set; } public string Company { get; private set; } public string Product { get; private set; } public string Copyright { get; private set; } public string Trademark { get; private set; } public string Culture { get; private set; } public AssemblyInfo(Assembly assembly) { FileName = assembly.GetName().Name; Version = assembly.GetName().Version.ToString(); FileVersion = GetAttributeName<AssemblyFileVersionAttribute>(assembly, a => a.Version); Title = GetAttributeName<AssemblyTitleAttribute>(assembly, a => a.Title); Description = GetAttributeName<AssemblyDescriptionAttribute>(assembly, a => a.Description); Configuration = GetAttributeName<AssemblyConfigurationAttribute>(assembly, a => a.Configuration); Company = GetAttributeName<AssemblyCompanyAttribute>(assembly, a => a.Company); Product = GetAttributeName<AssemblyProductAttribute>(assembly, a => a.Product); Copyright = GetAttributeName<AssemblyCopyrightAttribute>(assembly, a => a.Copyright); Trademark = GetAttributeName<AssemblyTrademarkAttribute>(assembly, a => a.Trademark); Culture = GetAttributeName<AssemblyCultureAttribute>(assembly, a => a.Culture); } private string GetAttributeName<T>(Assembly assembly, Func<T, string> selector) where T : Attribute { var attr = assembly.GetCustomAttributes(typeof(T), true).Cast<T>().FirstOrDefault(); return (attr == null) ? "" : selector(attr); } } // 利用例 class Program { static void Main(string[] args) { var assembly = Assembly.GetEntryAssembly(); var info = new AssemblyInfo(assembly); // こんな感じに。 Console.WriteLine(info.Title); } }
通常AssemblyInfo.csに記載する、タイトルとか説明とかバージョンの取得って、用いたいシーンも少なくないわりに存外面倒くさい。そんなわけで補助クラスを作ってみました。コンストラクタにAssemblyを投げ込むと、文字列にして返してくれます。
これと同じことをやるコードを一年ぐらい前に書いたのですが、今見たらどうしょうもなく酷かった……。(あまりにも酷いので見せられません!)。なので、今基準で書き直してみました。いかに型を書かないで済ませるか、いかに行数を少なく見た目をすっきりさせられるか。Func<T, string>という発想が一年前は出来なかったんだなあ。CastやFirstOrDefaultも知らなかったやも。
そんなわけで今年もありがとうございました。無事、閉鎖せずに一年を乗り越えられました。ただ、ある意味閉鎖してますけれどね、ゲサイト的な意味では。今年を振り返るとプログラミング、プログラミング、プログラミングでした。一年前とは見比べるまでもなく成長出来たと思います。ブログにコードを晒すこと、小さくてもいいのでソフトウェアを作って公開すること、というのが確実に貢献してくれました。よく言われる、コード晒せば他の人の添削が期待できるよ!ってのはそこまで期待できないと思うのですが(勿論、ありがたい指摘も幾つかありました、感謝です)、それよりも他の人が見る、という意識をもってコードに取り組むのが効いた気がします。カッコつけて書こうと、何度も練り直すのが結果的には良かったかな、と。
もう少し振り返れば、なんといってもC#、というかLinq。Linqの魅力に取りつかれて、そのままフルスロットルで加速した一年でした。C#や.NET Frameworkに詳しいか? と言われるとまだまだモニョるのですが、Linq to Objectsなら詳しい、と言えるだけの自信はつきました。これは、linq.jsとしてJavaScriptに移植したのが大きいです。動作が完全に一致するよう何度もチェックしたり、Monoのソース読んだり、リフレクタでSystem.Core.dll読んだりしたので、内部をきちんとイメージ出来るようになったので。
来年は、んー、とりあえずlinq.jsのWSH拡張の早期リリースを目指したいです。linq.jsは、個人的には非常に便利だと思っているのですが、ウェブ用のライブラリとしてアピールするのはどう考えても「無理」。パフォーマンス無視で、リスト処理がこんなに簡単に書けるんです、どうでしょう?ってんじゃあ請求力もないって話です。そもそもウェブ用JavaScriptで多用するDOM操作関連は未実装部分(linq.js Xml Extensions)多いし。とはいえ、せっかく作ったわけなので、真面目に布教させたいと思っています。今のところJavaScriptライブラリの隙間、WindowsScriptHost用やテキストエディタのマクロ用としての応用例を探っている、というか実装中。特に、WSH用に使うと物凄く便利なことが分かったので、とっとと実装を終えて、使ってみて欲しいところです。
実装中というか、途中で放置しちゃってるのがアレですが。ちょろっと記事書いたのが8月。今まで、それから全く進んでおりません。XboxInfoTwitのリリースに追われたり、Rxで遊んだりで放置ルートに入ってしまったのですねえ。やる気はめっちゃあるので、来年はまず一番に、linq.jsのWSH拡張のリリースを目指します。本気で本当に。
プログラミング以外だと、今年で一番影響が大きかったのがTwitterかなあ→neuecc on Twitter。今までチャットやネトゲなど、コミュニケーション系のウェブサービスを全く受けつけなかったコミュ不全の私が、唯一利用出来たサービスだという。一人で書き飛ばしていればよくて、無理に繋がらなくていいのが非常に楽。……。まあ、私はもう少し@飛ばしてもいいと思います。むしろ飛ばせ。これも来年の目標、ですかね。
C#のWebRequestとWebClientでCookie認証をする方法(と、mixiボイスへの投稿)
- C# - 09.12/17
WebからHTMLをダウンロードするにはWebClientが便利です。が、そのまんまだとCookie認証で躓きます。せっかく便利にダウンロード出来るのに、認証を超えられないんじゃ意味が無いよ!というわけかで幾つかのやり方を紹介したいと思います。海外だと沢山情報が出回っているのですが、日本だとWebClientはクッキーがとれないが検索上位に出てくるので、WebClientの利用を諦めて面倒くさいWebRequestを使う羽目になっている人が多いんじゃないかしらん。WebRequestなら@ITの記事、@IT:.NET TIPS クッキーを使ってWebページを取得するには?が引っかかりますからね。
とりあえず、@ITのmixiへの認証を例題に、まずはWebRequestでのやり方を見てみます。
// WebRequestによるCookie認証 // POSTしてCookieContainerに書き込む var data = Encoding.ASCII.GetBytes(string.Join("&", new[] { "next_url=/home.pl", "email=めるあど", "password=ぱすわど" })); var cookieContainer = new CookieContainer(); var req = (HttpWebRequest)WebRequest.Create("https://mixi.jp/login.pl"); req.CookieContainer = cookieContainer; req.Method = "POST"; req.ContentType = "application/x-www-form-urlencoded"; req.ContentLength = data.Length; using (var stream = req.GetRequestStream()) { stream.Write(data, 0, data.Length); } var res = req.GetResponse(); // ここでCookieContainerに書き込まれる // 以下、そのCookieを使えばアクセスし放題 var reqLog = (HttpWebRequest)WebRequest.Create("https://mixi.jp/show_log.pl"); reqLog.CookieContainer = cookieContainer; // CookieContainerセット var resLog = reqLog.GetResponse(); using (var stream = resLog.GetResponseStream()) using (var sr = new StreamReader(stream, Encoding.GetEncoding("euc-jp"))) { Console.WriteLine(sr.ReadToEnd()); // アクセスできてるのを確認 }
CookieContainerを設定すれば、Cookieのサーバーからの取得も送信も全部自動でやってくれる、というのがポイント。そこは楽です。楽なのですが、WebRequest自体が使いづらい。何をやるにも、いちいちStreamがどうだのこうだのなんてウンザリです。ていうか何だこのvarの多さ、変数乱れ打ち! そうなるとついつい、よーしパパ、ラッパー作っちゃうぞー、とか言ってしまいますが、もう見てらんない。.NET FrameworkにはWebClientというMS謹製のラッパーがあるわけなので、それを使いましょう。認証?Cookie?自前で取ればいいんですよ、ヘッダーから。
// WebClientならポストは超簡単! var wc = new WebClient { Encoding = Encoding.GetEncoding("euc-jp") }; wc.UploadValues("https://mixi.jp/login.pl", new NameValueCollection { {"next_url", "/home.pl"}, {"email", "めるあど"}, {"password", "ぱすわど"} }); // じゃあCookieはどうするの?というと、ResponseHeaderから自前で抽出します var setCookie = wc.ResponseHeaders[HttpResponseHeader.SetCookie]; var cookies = Regex.Split(setCookie, "(?<!expires=.{3}),") .Select(s => s.Split(';').First().Split('=')) .Select(xs => new { Name = xs.First(), Value = string.Join("=", xs.Skip(1).ToArray()) }) .Select(a => a.Name + "=" + a.Value) .ToArray(); var cookie = string.Join(";", cookies); // 以降は取得したCookieをHeaderに設定しておけばOk wc.Headers[HttpRequestHeader.Cookie] = cookie; var result = wc.DownloadString("https://mixi.jp/show_log.pl"); Console.WriteLine(result); // アクセスできてるのを確認
そう、WebClientでも、ResponseHeaderからSetCookieは取れるのです。なので、ここからCookieにバラしてやれば、あとはHeaderに設定するだけなので簡単です。一見WebRequest並に行数がかかっているのですが、大変なのはCookie分解部分だけです。分解がちょっと面倒なのは否めませんが……。基本的にカンマ区切りとなっていますが、有効期限の設定されているものが含まれていると「expires=Fri, 16-Dec-2011」のようにカンマが入ってしまい、単純なSplit(’,')では失敗します。なので正規表現の否定戻り読みでexpires=***,の場合は除外しています。あとは、バラしてクッツケテ、を繰り返して生成。そういえばSelect三連打ですが、これはもちろん複数行にすることでSelect一つで済ますこともできます。でも、そこはそれぞれ役割を切って3つに分けるのが、私の美意識、でしょうか。効率を考えれば匿名型なんて作らない方がいいぐらいなのですけどね、効率じゃない良さってのがあるんです。Linqには。
やり方はまだあります。WebClientは本当にただのWebRequestのラッパーで、中では普通にWebRequestを呼んで処理しています。よって、継承してoverrideしてGetWebRequestの辺りを書き換えて、CookieContainerを使うようにすれば非常に簡単です。
class CustomWebClient:WebClient { private CookieContainer cookieContainer = new CookieContainer(); // WebClientはWebRequestのラッパーにすぎないので、 // GetWebRequestのところの動作をちょっと横取りして書き換える protected override WebRequest GetWebRequest(Uri address) { var request = base.GetWebRequest(address); if (request is HttpWebRequest) { (request as HttpWebRequest).CookieContainer = cookieContainer; } return request; } } // WebClientを継承してちょっと書き換えてやれば一番簡単 var cwc = new CustomWebClient { Encoding = Encoding.GetEncoding("euc-jp") }; cwc.UploadValues("https://mixi.jp/login.pl", new NameValueCollection { {"next_url", "/home.pl"}, {"email", "める"}, {"password", "ぱす"} }); var result = cwc.DownloadString("https://mixi.jp/show_log.pl"); Console.WriteLine(result); // アクセスできてるのを確認
私的にはこれがお薦め。どうせWebRequestはそのまんまじゃ使い辛いので、多かれ少なかれラッパー作るでしょう。出来の悪いラッパーを作る/使うぐらいなら、WebClientの気の利かない部分だけ書き換えた方が良い。 ちなみにCookieの他にもWebClientの気の利かないところとしては、自動でリダイレクトするところが辛い、場合がある。普段はリダイレクトでいいんですが、リダイレクトされると困るシチュエーションもあります、たまに。そんな問題も、CookieContainerと同じくGetWebRequestの部分で、request.AllowAutoRedirectを設定すれば回避出来ます。
Web上のものをゴニョゴニョ処理するのに「Rubyなどのスクリプト言語の良さが目立つ。」というのは、ライブラリの問題にすぎない、ってことですな。XML処理には今やLinq to XMLがあるし、HTMLの取得にしてもちょっと工夫するだけで回避できるのでC#だから書きにくい、なんてことは無いと思っています。いやまあMechanize便利やん、とかありますがありますが。しかしC#には最終兵器、WebBrowserがあるので何とでもなる。HTML解析ならHtml Agility Packを使えば、物凄く簡単に出来ます。
最後に、Twitterの自分の投稿最新20件をmixiボイスに投げ込む、というコードを例として出してみます。CustomWebClientクラスは上に乗っけた奴を使っています。
static void Main(string[] args) { var encoding = Encoding.GetEncoding("euc-jp"); // ログイン var cwc = new CustomWebClient { Encoding = encoding }; cwc.UploadValues("https://mixi.jp/login.pl", new NameValueCollection { {"next_url", "/home.pl"}, {"email", "めーる"}, {"password", "ぱすわど"} }); // 投稿に必要なpost_keyをhtmlから取り出す var echo = cwc.DownloadString("https://mixi.jp/recent_echo.pl"); var postKey = Regex.Match(echo, "id=\"post_key\" value=\"(.+?)\"").Groups[1].Value; // 例なので簡易化するため認証無しのTwitterステータスを取得します // HttpUtilityの利用にはSystem.Webの参照設定が別途必要 var id = "自分の(じゃなくてもいいけど)TwitterID"; var texts = XDocument.Load("http://twitter.com/statuses/user_timeline/" + id + ".xml") .Descendants("status") .Select(x => HttpUtility.HtmlDecode(x.Element("text").Value)) .Reverse(); foreach (var text in texts) { // mixiボイスに投稿(UTF-8以外の日本語の投稿はUploadValuesが使えない(泣) cwc.Headers[HttpRequestHeader.ContentType] = "application/x-www-form-urlencoded"; cwc.UploadString("http://mixi.jp/add_echo.pl", string.Join("&", new[] { "body=" + HttpUtility.UrlEncode(text, encoding), "post_key=" + postKey, "redirect=recent_echo" })); } }
差分を記録するようにしたり、@付きを除外したりするようにすれば、そこそこ使えるんじゃないかしらん。利用はご自由にどうぞ。
Linqでコマンドラインオプション解析
- C# - 09.12/13
最近、Linqでの副作用について考えこむことが多くなりました。きっかけはSelectメソッド内で、外部のListに対してAddしたあげくreturn null -> ToArrayとかいうForEach代わりに使うかのような超勘違いしたコードを見せられたことなのですが(自信満々にどうだ!って感じで出されたのでモニョるしかなかったという苦い記憶ががが) と、そんな私の愚痴はどうでもよくて、副作用。基本的には邪悪ですよね。個人的に嫌なのは、せっかくスコープが狭く、ラムダ式だけを見つめれば良い状態になっているのに、副作用が入ると広い範囲を意識しなければならないこと。この変数名はどこからきたの? インスタンスの状態はどうなるの? 考えごとが増えるのは嫌なものです。ミスも増えるでしょう。エラーの温床となってしまいます。
とはいえ、使いどころによっては強力な効果を発揮するのも事実。例えば、以前書いたIEnumerableの文字列連結なんて、カウント用変数を一つ用意するだけで、Aggregateでサクッと書けてしまいます。というわけで、無駄な多用は厳禁だけど、使いどころをちゃんとおさえて書きましょう、というイイコな結論を出しつつ本題というか例題。
コマンドラインオプション解析。シーケンスを前方から解析して、次のキーが現れるまでは以前のキーで分類する。という分かるような分からないようなお話です。コマンドラインオプションだけでなく、たとえば決まった形式のテキストファイルを解析するとかでよくありそうなパターンだと思います。これがXMLなら簡単に解析出来るのに、クッ…… みたいな。
さて、グループ分けとなると、じゃあLinqで出来るよね? GroupByかなんかを使えばいいっしょー。と思い浮かぶわけなのですが、素の状態だと上手くいきません。GroupByを使うためのキーを列挙内部だけで保持することは出来ないからです。じゃあどうするか、というと、そうそう、副作用です副作用。はいはいクロージャクロージャ。というわけで列挙中にサクサクッとキーを書き換えてしまいましょう。
// こんな風に来るコマンドラインオプションを解析しよう var args = new[] { "-i", "input.txt", "-hoge", "-huga", "-o", "output.txt" }; // グループ分けといったらLinqだよね? // ディクショナリに分解したい、dict["-i"]で"input.txt"が取れる、というように // コマンドラインオプションをHashSetに格納する var options = new HashSet<string> { "-i", "-hoge", "-huga", "-o" }; string key = null; var result = args .GroupBy(s => options.Contains(s) ? key = s : key) // 副作用! .ToDictionary(g => g.Key, g => g.Skip(1).FirstOrDefault()); // 1番目はキーなのでSkip
とまあ、こうなります。副作用便利!
そういえばコマンドラインオプションの解析は.NET Framework標準では用意されていないのですよねえ。外部ライブラリのものは、当然なのですがあらゆるものに対処するため、どれもこれもヘヴィーすぎです。別にそんな複雑なのいらないよー、-oを解析出来ればそれだけでいいんだよー、的な小さいシチュエーションなら、この程度でも問題ない、はずです、きっと。
例としてコマンドラインオプションの解析を出したのは、id:coma2nさんのNDesk.Options(Mono) - コマンドラインパーサー - Programmable Lifeという記事を見てのことです。このNDesk.Optionsは凄いですね! まだ触ってないので実際の使いかっては分かりませんが、ラムダ式の使い方に驚きました。非常に上手いやり方だと思います。シンプルで。明快で。覚えやすく書きやすく。私もこういう発想が出来るようになりたいなあ。
書評 : More Effective C#
- C# - 09.12/06
結論は「Linq to Objectsの本」です。全編に渡って例題がLinqの再実装となっていて驚きました。「作って学ぶLinq」のほうが題として正しいぐらい。冗談じゃなく本当に、7割ぐらいが実質Linq周りです。実質、と言ったのは本書中では特に明言されていないからですが、見ればすぐにこれLinq to Objects……と突っ込みたくなること請け合いの例が沢山収録されています。
以前からLinq to Objectsに絞った解説書が出るべきだ、と思っていました。Linq to Objectsはこれでいて結構深いのです。どうもLinqというとLinq to SQLとか、データベース周りの喧伝の印象が強いようで、Linq to Objectsの実態が正しく伝わっていない気がします。今時リスト処理に高階関数使うなんてどのLightweight Languageでも常識よねー、というお話でもあります。Rubyのメソッドチェイン+ブロックなんて見た目だけで言えばLinqと丸っきり一緒ですし。昨今のモダンな言語の最も優れた部分を、最も優れた形で掲示しているのがLinqです。(優れた形、というのに異論はあると思いますが突っこまんで下さい)
そんなわけで褒め称えたいところだし、内容は結構良いと思っているのですが難点が一つ。対象範囲がC#3.0までのわりに、書き方が微妙に2.0っぽいこと。これはよろしくない。Linqに関しても再実装であることが本書中に明言がなく、書き方が2.0なので、「2.0でLinqをやるには」になっています。別に原理を知れればいいわけで、何も本書中の書き方を真似る必要はないのですが、それだと人に薦めづらいのですね。Linq知らない人に、これ見て学ぶといいよ、と素直に手渡したいのだけど手渡しにくい感が悔しいです。変にC#2.0と3.0を行ったり来たりするようなフワフワした構成じゃなく、Linqであることを明言した上で、その解説に徹してくれればよかったなあ、なんて思うんですね。
本の意義というか効用は、Linqや高階関数を多用してしまっても、この本が免罪符になるというのが一番大きいですね! C#3.0というのはLinqを使いこなし、更にはLinq風に設計構築していくのがEffectiveなのです、と大手を振って言える、かも。でもまあ、実際Linq風に扱うのが基盤になっているのは確かなので、変に凝るよりはLinq to Xxxみたいになっているほうが嬉しいです<ライブラリのような根幹部分での設計
Moreが先に出ていて、無印の発刊はこれから先です。無印がMoreの後に出るのは、本国では無印はC# 4.0対応の第二版が出るのでそれを待つのかなー、と思ったのですがそういうわけでもないようで。というわけで恐らくC#1.0まで対応のものだと思うので残念のような、そうでもないような。私はC#3.0から入ったにわかC#使いなので、1.0の書き方を見れるというのも新鮮で面白いんじゃないかなー、なんて思ってます。
Reactive Extensions for .NET (Rx) メソッド探訪第六回:exception handling
- C# Rx Framework - 09.11/29
.NET Reactive FrameworkからReactive Extensions for .NET (Rx)に名称が変わったようなので、タイトルも変更。長いね。というわけで久しぶりなのですが、今回はざっとexception handling operators、つまり「Catch, Finally, Retry, OnErrorResumeNext」を見てみることにします。それとRun(ForEachなので説明不要ですが)。Rxって何?という人はHello, Reactive Extensionsをまず参照下さい。
Rxの花形はイベント合流系のメソッドにあると思うので、ひたすら脇役ばかりを紹介してちっとも本流に入ろうとしないのはどうかと思うのですけど、EnumerableExのCatchを見て、あー、こりゃ便利だ、ヤバい、便利だ、用途すぐ浮かんでしまった、というわけでしてCatchを紹介します。まずは、その浮かんだ例であるTwitterのタイムライン取得をどうぞ。例はIEnumerableに対してのものですが、IObservableに対してのものも同じです。
class Twitter { public string Text { get; set; } public DateTime CreatedAt { get; set; } } static IEnumerable<Twitter> EnumerateUserTimeline(string userName) { // {0}はユーザー名、{1}はページ番号 公開ユーザーのものを取得なら認証不要 var format = "http://twitter.com/statuses/user_timeline/{0}.xml?page={1}"; foreach (var page in Enumerable.Range(1, 1000)) { var query = XDocument.Load(string.Format(format, userName, page)) .Descendants("status") .Select(e => new Twitter { Text = e.Element("text").Value, CreatedAt = DateTime.ParseExact(e.Element("created_at").Value, "ddd MMM dd HH:mm:ss zzzz yyyy", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal) }); foreach (var item in query) yield return item; } } static void Main(string[] args) { // 2009/11/23から今日までの投稿を古い順に並べるというもの var test = EnumerateUserTimeline("neuecc") .TakeWhile(t => t.CreatedAt >= new DateTime(2009, 11, 23)) .OrderBy(t => t.CreatedAt) .ToArray(); // これで基本的には問題ないわけですが、TwitterにはAPI制限があるので // ちゃんと全部取得出来るわけではなく、API制限発動 => 死亡になる可能性がある // 死んでもいいんだけど、せっかく取った死ぬ前のデータはがめておきたいよねえ // というわけで、そこで出番なのがRxのCatch! var test2 = EnumerateUserTimeline("neuecc") .TakeWhile(t => t.CreatedAt >= new DateTime(2009, 11, 23)) .Catch((Exception e) => Enumerable.Empty<Twitter>()) .OrderBy(t => t.CreatedAt) .ToArray(); // 例外が発生したら握りつぶして、代わりにEnumerable.Emptyを返します // なので、例外発生前のデータは全て取得出来ています、素晴らしい! }
といった感じです。つまりCatchは、そのまんまCatchです。Linqで全部書くのも良いんだけど、例外処理が出来なくてなあ、という不満がこれで解消されます。残りのFinally, Retry, OnErrorResumeNextですが、全部Catchの派生みたいなものです。とりあえず簡単な例を。
static IEnumerable<int> Iterate1To5() { yield return 1; yield return 2; throw new DivideByZeroException(); // 嘘例外でも投げておく yield return 4; yield return 5; } static void Main(string[] args) { // 1,2 Iterate1To5().Catch((Exception e) => Enumerable.Empty<int>()).Run(Console.WriteLine); // 1,2,100,200 Iterate1To5().Catch((Exception e) => new[] { 100, 200 }).Run(Console.WriteLine); // 1,2 -> 例外発生(ArgumentNullExceptionはDivideByZeroExceptionじゃないのでCatchしない) Iterate1To5().Catch((ArgumentNullException e) => new[] { 100, 200 }).Run(Console.WriteLine); // 1,2,100,200。つまりCatchの簡略版 Iterate1To5().OnErrorResumeNext(new[] { 100, 200 }).Run(Console.WriteLine); // 1,2,Finally。これでtry-catch-finallyが出来あがる Iterate1To5() .Catch((Exception e) => Enumerable.Empty<int>()) .Finally(() => Console.WriteLine("Finally")) .Run(Console.WriteLine); // 1,2 -> 1,2 -> 例外発生。例外を検知したら最初から列挙し直しての再試行 // EnumerableExのRetryはバグっぽくてObservableとは違う動きをする // 明らかにオカシイのでそのうち修正されるでしょう Iterate1To5().ToObservable().Retry(2).Subscribe(Console.WriteLine); }
最後に、中身をちゃんと知るには自分で実装するに限る、ということでIEnumerableでの拡張メソッドで再現してみました。Catchは本当に便利なので、わざわざRx使うのも、と思う場合は以下のコードを是非コピペして使ってくださいな。
// ループをぶん回すだけ、というもの(linq.jsではForce()が同様の働き) public static void Run<TSource>(this IEnumerable<TSource> source) { source.Run(_ => { }); } // ようするにForEach public static void Run<TSource>(this IEnumerable<TSource> source, Action<TSource> action) { foreach (var item in source) action(item); } // try-catch句の中でyield returnが使えないので回りっくどいことに public static IEnumerable<TSource> Catch<TSource, TException>(this IEnumerable<TSource> source, Func<TException, IEnumerable<TSource>> handler) where TException : Exception { using (var enumerator = source.GetEnumerator()) { while (true) { TException exception = null; var hasNext = false; try { hasNext = enumerator.MoveNext(); } catch (Exception e) { exception = e as TException; if (exception == null) throw; } if (exception != null) { foreach (var item in handler(exception)) yield return item; } if (hasNext) yield return enumerator.Current; else yield break; } } } // Rxにはこういう、handlerがActionのオーバーロードが欲しいです // わざわざ空のシーケンス投げるのは面倒くさいし、匿名型に対応できないじゃないか! public static IEnumerable<TSource> Catch<TSource, TException>(this IEnumerable<TSource> source, Action<TException> handler) where TException : Exception { return source.Catch((TException e) => { handler(e); return Enumerable.Empty<TSource>(); }); } // OnErrorResumeNextはCatchの簡略版みたいなもんですね、別に必要ないような public static IEnumerable<TSource> OnErrorResumeNext<TSource>(this IEnumerable<TSource> source, IEnumerable<TSource> next) { return source.Catch((Exception e) => next); } // ToList().ForEach()とRun()ではactionの出るタイミングが変わることに注意 public static IEnumerable<TSource> Finally<TSource>(this IEnumerable<TSource> source, Action action) { try { foreach (var item in source) yield return item; } finally { action(); } } // 本当は無限でやるべきなんでしょうが、int.MaxValueで。 public static IEnumerable<TSource> Retry<TSource>(this IEnumerable<TSource> source) { return source.Retry(int.MaxValue); } // EnumerableExのRetryがバグ臭いのでObservable.Retryの挙動を採用しました public static IEnumerable<TSource> Retry<TSource>(this IEnumerable<TSource> source, int retryCount) { var count = 0; Exception exception = null; while (count < retryCount) { exception = null; foreach (var item in source.Catch((Exception e) => exception = e)) { yield return item; } if (exception == null) yield break; count++; } throw exception; }
どれもCatchの派生のようなものです、CatchイイよCatch。これは使いまくりたくなる。それにしてもtry-catchの中でyield returnが使えないのを、はじめて知りました。こんなことやろうとしたことがなかったので。あと、EnumerableEx.Retryはひっじょーにバグ臭いです。ちなみにEnumerableEx.Mergeもバグ臭い。全体的にEnumerableExはバグ臭さ全開です。明らかに(Observableから)適当に移植した感漂ってます。ヤバい。
TwitterのTL過去ログをHTMLにするツール
- C# TwitterToHTML - 09.11/27
Twitterの他の人のポストは全部読みたいと思っています。数千もフォローしてるアルファーツイッタッターでは無理でしょうけど、せいぜい百ちょいぐらいなら全然いけるわけです。と、思っていたのですが、たかだか200を超えたところで、ん、無理……?と思える感じになってきてしまいました。ツール的限界で。Webから過去ログを見ようとすると、限界点に到達してしまって未読があるのに過去ログが見れない状態になってしまって。ていうか、そもそもWebでログを見るというのはダルい。まあ、ないですよね。私がTwitterで使っているツールはEchofonで、これは過去ログ見るのに適さないし全然昔の見れないし、というわけでどうしたものかなー、と思っていたんですが、作ればいいわけですよね、過去ログ閲覧専用Twitterクライアント。
と、考えてはみたものの、そもそもわざわざツール作るまでもなく、ログをHTMLで吐けばいいんじゃね?と気付いた。YesYesYes。流し読みなら、むしろへっぽこ専用ツールよりもブラウザのほうが見やすいし。家でガッとHTML取得しといてモバイルに転送して電車でゆったり見る、とか出来るし。というわけで、可能な限り過去ログを掘ってHTMLに吐きだすプログラムを書きました。可能な限り、といってもAPI制限の都合上で最大800件まで、のようです。うーん、これじゃあ半日ぐらい前、程度ですよねえ。18-24時とかだと一瞬で吹っ飛びそうかも。3000件ぐらいまでは欲しいとこなのですが……。なお、API消費はたった4か5なので安心です。一回につき200件取れるので。
デザインはCSSで行えます。例えばimgのwidthとheightを0pxにすればアイコン表示を消せます。これで学校や会社で見る時にアニメアイコンが並んで恥ずかしい思いをしなくて済む! あとまあ、デフォルトのCSSはショボい(私がCSSの知識ないので……。float良く分からん、高さ揃わない、50pxで決め打ち!とか)ので、適当に改良して使ってください。
あと、コード(C# 3.0)も同梱してあるので適当に見て突っ込んでくださいな。HTML組み立て部分はLINQ to XMLです。
var urlPattern = new Regex("(s?https?://[-_.!~*'()a-zA-Z0-9;/?:@&=+$,%#]+)"); var xhtml = new XElement("html", new XElement("head", new XElement("link", new XAttribute("rel", "stylesheet"), new XAttribute("href", "style.css"))), new XElement("body", new XElement("ul", EnumerateHomeTimeline(username, password).Select((t, i) => new XElement("li", new XAttribute("class", (i % 2 == 0 ? "even" : "odd")), new XElement("div",new XAttribute("class","name"), t.ScreenName), new XElement("div",new XAttribute("class","date"),t.CreatedAt.ToString("G")), new XElement("div", new XAttribute("class","image"), new XElement("img",new XAttribute("src", t.ProfileImageUrl))), new XElement("div", new XAttribute("class","text"), urlPattern.Split(t.Text).Select(s=> { var href = urlPattern.Match(s); return (!href.Success) ? (XNode)new XText(s) : new XElement("a",new XAttribute("href",href.Value),href.Value); })))))));
えーと…… 汚い、ですね!それでも、このLINQ to XMLの関数型構築がなければどれだけ悲惨なことになっていたか!やはりLINQ to XMLは素晴らしい。さて、しかし困ったのがリンクのaタグ付け。文字列で扱っていれば普通に置換すれば済む話なのですが、XTextにそれを放り込むとタグはエスケープされます。最初驚いたのですが、考えてみると当然ですね、XMLとして不正なものは許されないので。しょうがないのでSplitしてXMLとして組み立ててやりました。
json/xmlを拾ってきてHTMLに整形するだけなのだから、JavaScriptで書いてうぇぶあぷり、的なものにしたほうが利便性とか何とかかんとかが良好なんじゃございませんこと?とか思わなくもなかったのですが、C#、楽なので、ほんと。良い言語なんですって。
Hello, Reactive Extensions
- C# Rx Framework - 09.11/24
Reactive FrameworkがReactive Extensions for .NET (Rx)として、DevLabsで公開されました。紫のうなぎアイコンが可愛い。これは(消滅してしまった)Microsoft Voltaと同様のものなのですが、開発チームが同じだからだそうです。DevLabsには他にAxumやSTM.NETなど、興味深いプロジェクトがいっぱいありますが、日本語による情報がほとんど手に入らないので手を出しづらいところがあります。RxもDevLabsに登場したことでグッと情報が増えましたが、英語ソースによるものばかりなので、私の脳みそ的には相当シンドイことになっています。英語辛いよぅ。
とはいえ、小細工しなくても.NET3.5 SP1上で動かせるのは素敵なので是非試しましょう! Silverlight Toolkitにこっそり収録版からも、かなりパワーアップしています。メソッド大増量、そしてToolkit版でバグい挙動していたのが本当にバグなのか私のやり方が悪いのか悩んでいた部分がサクッと修正されいてホッとしたり。
インストールディレクトリに置いてあるchmのヘルプを見ることも出来るし、IntelliSenseも動きますので、前に比べると触りやすくなりました。ただまあ、例ぐらいは入れてよって感じで、簡素極まりない一行説明から理解するのは、やっぱり難しい。
System.Interactive.dll
Rxには興味ないよ、という人にも大変役立ちなのがこのdll。デフォで参照設定に加えることは確定的に明らか。中のクラスはSystem.Linq.EnumerableExのみで、ようするに拡張メソッド集です。EnumerableExという名の通り、Linq.Enumerableに対する追加版となっています。基本的にはIObservableに用意されていたメソッドをIEnumerable用に持ってきたという感じです。Repeat(value)による無限リピートやReturn(value)による単体シーケンス作成、Generate(所謂Unfold)など、欲しかった生成メソッドが沢山用意されています。
更に当然のようにIEnumerableに対する拡張メソッドもたっぷり。Zip(.NET 4に搭載)やMemoize、それにDo(副作用専用のActionメソッド)、Run(ようはForEachです、そう、ForEachですよ!)が非常にうれしい。他、挙げればキリがない用途不明の拡張メソッドがテンコ盛りなので要研究ですね。
例えばフィボナッチ数列。
EnumerableEx.Generate( new { v1 = 0, v2 = 1 }, // initialState _ => true, // condition a => a.v1, // resultSelector a => new { v1 = a.v2, v2 = a.v1 + a.v2 } // iterate ) .Take(30) .Run(i => Console.WriteLine(i)); // conditionが無限ならこうも書ける(第二引数の戻り値がIEnumerable、だけど平たくされる?) EnumerableEx.Generate( new { v1 = 0, v2 = 1 }, // initial a => EnumerableEx.Return(a.v1), // resultSelector a => new { v1 = a.v2, v2 = a.v1 + a.v2 }); // iterate
んね、素敵。
System.CoreEx.dll/System.Threading.dll
CoreEx.dllにはAction, FuncのT16まで版(.NET 4に搭載)やUnit(voidを表す型)が主なところ。他にIEventやNotificationがありますが、これらは主にRxで使うためのものですね。Threading.dllはLazy, Task, Parallelといった、.NET 4に搭載されるメソッドの先取りといった感じのものががが。真剣に追っかけると大変なのでスルー。
System.Reactive.dll
以前のに比べるとメソッドが増えているのは当然なのですが、目立つところではSystem.Joins.Patternクラスの追加が目新しいです。何に使うのかはまだ知りません。ふむ。自分でお題を探すのも大変なので、Forumで出てる内容を幾つか紹介します。
// 100,1,2,3,....,10 Observable.StartWith(Observable.Range(1, 10), 100); // 順番が奇妙なのは拡張メソッドとして使えるようにしたため Observable.Range(1, 10).StartWith(100);
ConsはStartWithに改名されました。ついでに順番がちょっと変わりました。先頭に付け足すのに、第二引数というのが違和感全開なのは否めない。これは、拡張メソッドとして利用できるようにしたためでしょうね。メソッドチェインを崩さずに、先頭に値を足すことが出来るようになりました。ObservableだけではなくEnumerableにもあります。
public static IObservable<IEvent<MouseEventArgs>> GetMouseDown(this Control control) { return Observable.FromEvent<MouseButtonEventHandler, MouseEventArgs>( h => (sender, e) => h(sender, e), h => control.MouseDown += h, h => control.MouseDown -= h); }
h => (sender, e) => h(sender, e)っていうのが混乱しますな。第一引数のhはEventHandler<MouseEventArgs>です。ここでhをMouseButtonEventHandlerに変換します。この辺も、ActionやFuncと同じく、EventHandler<TEventArgs>だけあればいいのに、その他のゴチャゴチャしたデリゲートは消滅してしまえばいいのに、とか思わなくもないのですがしょうがない。「sender,e => h(sender,e)」は引数がobject,MouseEventArgsで戻り値がvoidのよくあるイベント用のデリゲートです。素の状態でこのラムダ式を書くと型が決まらないので動作しませんが、FromEventの型宣言時にMouseButtonEventHandlerだと明示しているので、変換出来ます。ここで変換されるので、第二、第三引数のhはMouseButtonEventHandlerになります。
メソッド探訪の第一回で警告が出る、とか書いてしまったのですが、こういう風に記述すれば警告も出ず、文字列メソッド名を使わずに利用できたようです。言われてみればなるほど、って感じなのですが、気付けなかったなあ……。
ambはLISPのambを由来として、ambiguous(不明瞭)の略。だそうです。Rxでは、例えば……
var first = Observable.Range(1, 3).Delay(300); var second = Observable.Range(4, 3).Delay(100); var third = Observable.Range(7, 3).Delay(200); Observable.Amb(first, second, third).Subscribe(s => Console.WriteLine(s)); Console.ReadLine();
Delayは発火を指定ミリ秒だけ遅らせるメソッドです。では、何が表示されるでしょうか。答えは、”4,5,6″です。んじゃあthirdをDelay(0)にしたら? “7,8,9″が表示されます。なるほど、分かってきた。つまり最初に到達したものを採用する、というわけです。この例ではわざとらしくDelayを足したシーケンスを投げてみましたが、例えば幾つかのイベントを並べて、最初にイベントが発火したものを。みたいな用途が考えられなくもない。
EnumerableEx.Amb( new[] { 1, 2, 3 } .Select(i => i + i) .Select(i => i + i), new[] { 7, 8, 9 } .Select(i => i + i)) .Run(i => Console.WriteLine(i));
IObservableは分かるとして、何故かIEnumerableにもAmbがあります。上の例は何が表示されるでしょうか?答えは、8割は14,16,18です。残り2割は4,8,12です。チェインを沢山繋いだ方が原則的には「時間がかかる」ため、チェイン数の少ない方が採用される場合が多い。ただし、内部ではThreadを立てているので、必ずしもそうなるわけじゃない。というわけで、結果は非常に不確定で不明瞭で、使い道は完全に謎。
F# TutorialをC#と比較しながらでF#を学ぶ
F#はMicrosoft発の関数型言語で、Visual Studio 2010に標準搭載されます。Visual Studio 2010 Beta 2も出たことだし、話題の?新言語を少し勉強してみることにします。F#の新規プロジェクト一覧にTutorialというのが用意されているので、これの中身を、C#と比較しながら見ていきたいと思います。追記:Microsoft Visual Studio 2010 First Look Beta 2日本語版も公開されました。
基本
open System let int1 = 1 let int2 = int1 + 3
using System; var int1 = 1; var int2 = int1 + 3;
名前空間の利用の設定と基本的な変数の代入方法。といったところでしょうか。そのまんまだし、別にC#と違いは特にないっぽい。C#ではvar、F#ではlet。どちらも推論が効くのでほとんど同じ。末尾セミコロンはいらないようです。#lightがどうたらこうたら、というのは略。それともう一つ、F#はこのように定義したint1に再代入は出来ません。int1 = 100とすると、比較になります(==ではなく=が比較)。再代入的なint1 <- 1000はコンパイルが通らない。不変(immutable)なのです。C#だとreadonly、はフィールドにしかつけられないので、同じことを再現するのは無理なよう。
printfn "peekResult = %d" peekResult printfn "listC = %A" listC
F# TutorialではPrintは最後にあるのですが、あのですね、出来ればprintは冒頭にしていただきたいです。なんというか、私がF#で一番戸惑ったのが、printfn int1ってのが出来ないことなんですね。いやほら、とりあえずlet int1 = 1って書いたじゃないですか、最初に。で、書いたらとりあえず表示して確認したいでしょ?Console.WriteLineにあたるのはprintfnか、って来るわけです。でも、書いても動かないの。で、まあ、つまるところstring.Format的なものであり書式指定が必要、というところまで行くわけですが、そこで書式って何を書けばいいの?ということになるわけです。”%d”とか予告なく言われても分からないし。もうブチ切れですよ。え、Cの書式指定と一緒だって?いやあ、Cの書式指定も全然覚えてられません、あれあんま良くないと思うんですが……(ついでに言えば私はC#の書式指定も全然覚えてない、必要な度にMSDN見に行ってる)。しかもF#のは書式も色々拡張されてない?より一層分からん!int1の出力で挫折する!
ということなので、もっと頭の方にprintfnのきちんとした解説を載せてくれないと辛いです。ただ、どうしてもアレならConsole.WriteLine int1とでも書けば動く。おお、いきなり.NET Frameworkがそのまま使えることの有難味が(笑) と、冗談はさておき、この「書式指定に何が使えるのか分からない」状態はひっじょーに気持ち悪いので、検索してすぐ分かるような場所に一覧が、欲しい、です。真面目にこれは挫折理由になってます。しょうがないので検索して出てきた Google BooksのExpert F#の解説を見てようやくホッとできた。
%b(bool), %s(string), %d(10進), %f(float), %O (Object.ToString()) それと%A(Any)を覚えておけば問題ない、でしょーか。ほんと予告なく%Aとか言われても困るんですよ、泣きたいですよ。%Oとの違いは、人間が見た時に良い感じに整形してくれるのが%A、でしょうか。文字列は”"で囲まれ、配列は展開して出力してくれる。
C#ではcw->TabTab->変数名、といった感じにコードスニペットを活かして手早く記述出来たわけですが、それに比べるとF#は書式指定が必要な時点で、非常にカッタルイ。カッタルイのですが、かわりに、より型に厳格です。printfn “%s” trueとか書くとコンパイル通らない。良し悪し、でしょうか。でも学習用にやってる間は面倒くさいだけですね。どうしても嫌ならば「let p x = printfn “%A” x」とでも定義しておけば良いのでしょうけれど。
gdgd言う前にF C# 言語リファレンスを見ろ、って話なのかもしれない。私は情けないことにここから書式指定を記した部分を見つけられませんでしたが。あと、選択範囲で囲んでAlt+EnterでF# Interactiveに送られるのでそれ見て確認しろって話も少しはありそう。
関数
let f x = 2*x*x - 5*x + 3 let result = f (int2 + 4) let rec factorial n = if n=0 then 1 else n * factorial (n-1)
Func<int, int> f = x => 2 * x * x - 5 * x + 3; var result = f(int2 + 4); Func<int, int> factorial = null; factorial = n => (n == 0) ? 1 : n * factorial(--n);
Tutorialには最大公約数を求めるものもありましたがfactorialと同じなので省略。F#は関数型言語ということで、やっぱ関数ですよね!キーワードはletのままで、ふつーの変数と区別なく定義できる。 C#では汎用デリゲートであるFuncとActionを使うことでそっくり再現できる。C#では型を書いてやらなければならないのだけど、F#ではより強力に推論が効くようで型の明示は不要、のようです。
再帰は、F#はlet recキーワードでそのまま書けるのに対し、C#では一度nullを代入して名前を事前に宣言しておかなければならない。というぐらいで、見た目はほとんど変わらない。そういえばifが式ですね。なのでelseは省略できないようです。else ifの連打はelifで。というわけで、このif式(?)はC#の三項演算子とほとんど同じような感じです。
let add1 x y = x + y printfn "%A" (add1 1.0 2.0) printfn "%A" (add1 1 2) // Compile Error let add2 x y = x + y printfn "%A" (add2 1 2) printfn "%A" (add2 1.0 2.0) // Compile Error
少し脱線して型推論の話を。C#の推論は単純なだけ分かりやすくて、これは型書いてやらないといけないな、推論させるための材料を与えてあげないといけないな、というのが結構直感的だったんですが、F#だと強力な分だけ、どう推論されるのか難しい。今は漠然と、全体を見るんだなー、ぐらいにしか分かっていません。例のコードですが、add1はfloat->float->floatで、add2はint->int->intに推論されます。let add1 x y = x + yの時点ではxの型もyの型も分からないけれど、「最初に呼ばれた時に」引数の型は判明する、ということは戻り値の型も判明する。なので、その型で決定する。ということなのかなー、と。この部分はC#と全然違っていて、面白いし強力だなー、と。
Tuple
let data = (1, "fred", 3.1415) let Swap (a, b) = (b, a)
var data = Tuple.Create(1, "hogehoge"); static Tuple<T2,T1> Swap<T1,T2>(Tuple<T1,T2> tuple) { return Tuple.Create(tuple.Item2, tuple.Item1); }
TupleはC#4.0から導入されます。F#は括弧で括るという専用記法があるので簡単に記述出来る。のに対して、C#ではふつーのclassなのでふつーにclassとして使うしかないのが残念。Swapですが、Tupleはimmutable(不変)なので、新しく生成する。だけ。です。temp用意して入れ替えて、などしない。潔く新しく作る。
Boolean, Strings
let boolean1 = false let boolean2 = not boolean1 && (boolean1 || false) let stringA = "Hello" let stringB = stringA + " world."
var boolean1 = false; var boolean2 = !boolean1 && (boolean1 || false); var stringA = "Hello"; var stringB = stringA + "world.";
F#では否定が!ではなくnotなのですね。あとは一緒。
List
let listA = [ ] let listB = [ 1; 2; 3 ] let listC = 1 :: [2; 3] let oneToTen = [1..10] let squaresOfOneToTen = [ for x in 0..10 -> x*x ]
var listA = Enumerable.Empty<object>(); var listB = new[] { 1, 2, 3 }.ToList(); var listC = Enumerable.Repeat(1, 1).Concat(new[] { 2, 3 }).ToList(); var oneToTen = Enumerable.Range(1, 10 - 1 + 1).ToList(); var squaresOfOneToTen = Enumerable.Range(0, 10 - 0 + 1).Select(x => x * x).ToList();
リストを扱うとC#と大分差が出てきます。まず第一に、空リストは、C#だと該当するものは作れない。と思う。とりあえずobjectで代替することにしましたが、多分正しくありません。listBはただの整数リストなわけですが、F#だと;で区切るようです。一応、配列とリストは違うということで、C#側のコードはListにしていますがListとも違うので、まあ、気分だけ。listCの::はConsということで、一つの値とリストを連結するものです。C#に該当する関数はありません。しいていえばConcatが近いので、Repeat(value, 1)で長さ1のシーケンスを作って連結、という手を取ることにしました。
F#は[1..10]で最小値-最大値の連続したリストが作れるのですが、これはC#のEnumerable.Rangeとは、違います。Rangeの第二引数は最大値ではなく個数なので。正直言って、個数よりも最大値のほうが使いやすいと思うのだけどなー。というわけで、最大値-最小値+1 = 個数。ということにしています。最後のリスト内包表記は、うん、ええと、私は苦手です。値の動きが右行ったり左行ったりなのが嫌です。Linqのほうが好き。C#でイメージするなら、foreach (var x in [0..10]) yield return x * x; ってとこですかね。
パターンマッチ
let rec SumList xs = match xs with | [] -> 0 | y::ys -> y + SumList ys let listD = SumList [1; 2; 3]
Func<IEnumerable<int>, int> SumList = null; SumList = xs => (!xs.Any()) ? 0 : xs.First() + SumList(xs.Skip(1)); var sum1 = SumList(new[] { 1, 2, 3 }); var sum2 = new [] { 1, 2, 3 }.Sum(); // こらこら
まず、listDとかF# Tutorialには書いてあるんですが、これintなのでlistじゃないでしょ!紛らわしい。さて、match with | ->という目新しい記述がパターンマッチという奴ですね? 引数のリストxs(リストは通常変数名にxsとかysとかを用いるようです)が空配列の時は0を、そうでない時はyとysに分解して、ysの方は再帰して足し合わせる。ふむぬん。C#に直すとif-else if-else ifの連打。値を返すから、三項演算子のネストですな。という程度の理解しかしていません。三項演算子ネストより綺麗に書けて素敵。という浅すぎる理解しか、今はしていません。まあ、そのうちそのうち。
y::ysという表記ですが、これは配列中の最初のものがy、それ以外がysになります。つまりLinqだとFirst()とSkip(1)ですね。let x::xs = [3..5]とすれば、xが3でxsが4,5になる。警告出ますが。基本はパターンマッチ時用ってことなのかしらん。この辺はちょっと良く分かりません。
C#のほうの、IEnumerableのままSkipをゴロゴロと繋げていくのは実行効率がアレな悪寒。かといってToArrayを毎回使うのもなあ、というわけで上手い落し所が見つからない。QuickSortのように一本の配列に対し、境界の数字を渡していくってのやるとゴチャゴチャするし。あ、でもF#のも結局ysってのはxsとは別の、新しい配列ですよね?C#で表すのならば、xs.Skip(1).ToArray()ということかしらん。だとしたら、この程度の「効率」なんて奴は、気にしたら負けだと思っている。でいいのかもしれない。よくないかもしれない。
配列・コレクション
let arr = Array.create 4 "hello" arr.[1] <- "world" arr.[3] <- "don" let arrLength = arr.Length let front = arr.[0..2] let lookupTable = dict [ (1, "One"); (2, "Two") ] let oneString = lookupTable.[1]
var arr = Enumerable.Repeat("hello", 4).ToArray(); arr[1] = "world"; arr[3] = "don"; var arrLength = arr.Length; var front = new string[3]; Array.Copy(arr, 0, front, 0, 3); // もしくはSkip->Take. 実行効率は劣りますが、私はこちらの記述方法のほうが好き var front2 = arr.Skip(0).Take(3).ToArray(); var lookupTable = new Dictionary<int, string> { { 1, "One" }, { 2, "Two" } }; var oneString = lookupTable[1];
配列とlistとの違い。listは不変(immutable)で、配列は可変(mutable)ということかしらん。あと配列なら.NET Frameworkのメソッド・プロパティが全部使える。mutableなものへの値の再代入は=ではなく<-で行う。あとは、Array.createは中身がnullな配列ではなく、初期値を指定して全部それで埋めるメソッドのようです。ふむ。あ、最後のslicing notationはいいですね。C#だとArray.Copyを使うのが等しいでしょうけど、記述が冗長すぎてねえ……。どうせ実行時間に対して差は出ないでしょ、と思う場合はLinqでSkip->Takeにしたほうがすっきり書けて良い。あ、あとインデクサは.[]が対応してるようです。ドット。ドット。
辞書の初期化は、タプルを放り投げるだけ。素晴らしい!見た目に分かりやすくスッキリするのがいいです。C#だとコレクション初期化子で近い形にはなりますが、{ {と、全て波括弧で記述するのはどうかなあ、と思うところがあるので。あとは一応、C# 3.0 における疑似 Map 生成リテラル - NyaRuRuの日記なんてことも出来ますけれど、やりませんものね。
関数(その2)
let Square x = x*x let squares1 = List.map Square [1; 2; 3; 4] let squares2 = List.map (fun x -> x*x) [1; 2; 3; 4] let squares3 = [1; 2; 3; 4] |> List.map (fun x -> x*x) let SumOfSquaresUpTo n = [1..n] |> List.map Square |> List.sum
public static IEnumerable<TR> Map<T, TR>(this Func<T, TR> selector, IEnumerable<T> source) { return source.Select(selector); } // ↑という拡張メソッドを定義して Func<int, int> Square = x => x * x; var squares1 = Map(Square, new[] { 1, 2, 3, 4 }); var squares2 = new Func<int, int>(x => x * x).Map(new[] { 1, 2, 3, 4 }); var squares3 = new[] { 1, 2, 3, 4 }.Select(x => x * x).ToArray(); // もしくは Array.ConvertAll(new[] { 1, 2, 3, 4 }, x => x * x) Func<int, int> SumOfSquaresUpTo = n => Enumerable.Range(1, n - 1) .Select(i => Square(i)) .Sum();
関数が先で、それに適用する配列を渡す、という順序はC#ばかり触ってる身としては、新鮮な印象です。そういえばAchiralにも同種のオーバーロードが沢山定義されているのですが、私は違和感から、IEnumerable始点のものばかり使っています。あとSelect->ToArrayはArray.ConvertAllで書けるのですが、私はLinqで書くほうが好き。というかArrayの静的メソッドは、基本Obsoleteなぐらいの気持ちでいたりいなかったりする。
ラムダ式は「fun 引数 -> 本体」ですね。C#のほうがキーワードが必要ない分だけすっきりしてガガガ。でもnew Func<型>という不格好なものをつけなければならなかったりする悪夢。var hoge = (int x) => x * xもダメなんですよねえ。理由は、例えば「delegate int Func2(int i);」というのが定義出来るから。引数intで戻り値intだから、Func
「|>」という見慣れない演算子が、パイプライン演算子で、左から右に値を流す。C#だと、Listに対してはLinqで、値に対しては、そういえば前に書いたような……。neue cc - ver 1.3.0.3 / ちょっとした拡張メソッド群のTapの一個目が近い感じでしょーか。いいですよね、こういうの。
Mutable
let mutable sum = 0 for i in 0..10 do sum <- sum + i while sum < 100 do sum <- sum + 5
var sum = 0; foreach (var i in Enumerable.Range(0, 10)) { sum += i; } while (sum < 100) { sum += 5; }
最初にF#の値はimmutableだと書きましたが、mutableにしたい時は、mutableキーワードを足せばおk。再代入時は<-演算子を使う、と。C#だとデフォルトがmutableなので、まんまです。そして、このforは、foreachですね。インデントが波括弧代わりなので、doだけどendは要りません。普通のforは「for i = 1 to 10 do」ですが、これならforeachでいいやあ、という気はする。
Types: unions
type Expr = | Num of int | Add of Expr * Expr | Mul of Expr * Expr | Var of string let rec Evaluate (env:Map<string,int>) exp = match exp with | Num n -> n | Add (x,y) -> Evaluate env x + Evaluate env y | Mul (x,y) -> Evaluate env x * Evaluate env y | Var id -> env.[id] let envA = Map.of_list [ "a",1 ; "b",2 ; "c",3 ] let expT1 = Add(Var "a",Mul(Num 2,Var "b")) let resT1 = Evaluate envA expT1
F# Tutorialですが、ここで途端に説明が無くなって放り出されます。鬼すぎる。今までのわりとゆるふわなところから途端にコレです。意味分からないし。unionsとか言われても分けわからない。と、嘆いていても始まらないので理解するよう頑張ります。そういえば(env:Map<string,int>)も初出なのよね。推論じゃなく明示的に型を与える時は、こうするそうです。型定義がC#とは逆で、コロン後の末尾。違和感がシンドい。ActionScriptなんかも同じで非常にシンドい。
unionはC#だとenumが近いかなー、と思うのですが、enumがintのみなのに対し、F#のunionはそれぞれが別の型を持てる。といった認識。更に値は外から定義可能。というわけでenumとは全然違いますな。むしろ普通にclassに近い。of intで型を定義している(Expr * ExprはTuple)し、値は外から与えているし(コンストラクタのように!) けれど、値は一個。
じゃあclassで作れるかと言ったら、どうだろー。戻り値の型がバラバラになるので、interfaceで一個に纏められるわけでもなく上手いやり方ってあるのかしらん。パターンマッチと同じく、C#には無い概念、と素直にとらえた方が良いかも。一応、interface、じゃなくてダミーに近い型の下にぶら下げて、Evaluateのところでisで派生型を判定して分岐、といった感じでやってみましたが、ゴミですね……。
public class Expr { // privateにしたいつもり(これは酷い) public class _Num : Expr { public int Value { get; set; } } public class _Add : Expr { public Expr E1 { get; set; } public Expr E2 { get; set; } } public class _Mul : Expr { public Expr E1 { get; set; } public Expr E2 { get; set; } } public class _Var : Expr { public string Value { get; set; } } private Expr() { } public static Expr Num(int value) { return new _Num { Value = value }; } public static Expr Add(Expr e1, Expr e2) { return new _Add { E1 = e1, E2 = e2 }; } public static Expr Mul(Expr e1, Expr e2) { return new _Mul { E1 = e1, E2 = e2 }; } public static Expr Var(string value) { return new _Var { Value = value }; } } static int Evaluate(IDictionary<string, int> env, Expr exp) { return // どうしょうもなく酷い (exp is Expr._Num) ? ((Expr._Num)exp).Value : (exp is Expr._Add) ? Evaluate(env, ((Expr._Add)exp).E1) + Evaluate(env, ((Expr._Add)exp).E2) : (exp is Expr._Mul) ? Evaluate(env, ((Expr._Mul)exp).E1) + Evaluate(env, ((Expr._Mul)exp).E2) : (exp is Expr._Var) ? env[((Expr._Var)exp).Value] : 0; } static void Main(string[] args) { var envA = new Dictionary<string, int> { { "a", 1 }, { "b", 2 }, { "c", 3 } }; var expT1 = Expr.Add(Expr.Var("a"), Expr.Mul(Expr.Num(2), Expr.Var("b"))); var resT1 = Evaluate(envA, expT1); Console.WriteLine(resT1); // 確認 }
見なかったことにしてください。私の脳みそなんてこんなもんです。
Types: records
type Card = { Name : string; Phone : string; Ok : bool } let cardA = { Name = "Alf" ; Phone = "(206) 555-8257" ; Ok = false } let cardB = { cardA with Phone = "(206) 555-4112"; Ok = true } let ShowCard c = c.Name + " Phone: " + c.Phone + (if not c.Ok then " (unchecked)" else "")
class Card { public string Name { get; set; } public string Phone { get; set; } public bool Ok { get; set; } public Card() { } public Card(Card with) { // structならthis=withで一発なのですが // F#のrecordはstructじゃないとのことなので this.Name = with.Name; this.Phone = with.Phone; this.Ok = with.Ok; } } var cardA = new Card { Name = "Alf", Phone = "(206) 555-8257", Ok = false }; var cardB = new Card(cardA) { Phone = "(206) 555-4112", Ok = true }; Func<Card, string> ShowCard = c => c.Name + " Phone: " + c.Phone + (!c.Ok ? " (unchecked)" : "");
こちらは割とすんなりと何なのか分かる。withでコピーが作れているところが面白い。ふーむ、C#だとむしろ匿名型のほうが近い感じに見えるかもしれない。
Types: classes
type Vector2D(dx:float, dy:float) = let length = sqrt(dx*dx + dy*dy) member v.DX = dx member v.DY = dy member v.Length = length member v.Scale(k) = Vector2D(k*dx, k*dy)
class Vector2D { public float DX { get; private set; } public float DY { get; private set; } public float Length { get; private set; } public Func<int, Vector2D> Scale { get; private set; } public Vector2D(float dx, float dy) { var length = (float)Math.Sqrt(dx * dx + dy * dy); this.DX = dx; this.DY = dy; this.Length = length; this.Scale = new Func<int, Vector2D>(k => new Vector2D(k * dx, k * dy)); } }
コンストラクタと定義が一体化していて、随分とシンプルに記述出来るようです。JavaScriptっぽい、なんて思ってしまったりして。C#で再現するとプロパティでメソッドかいな、という違和感があったりなかったり。private変数で蓄える必要がないから、定義が楽といえば楽。ところで思うのは、F#のv.DXとかの、vって何処から来てるの……? これ、別にhogehogeにしてもaaaaaaにしても動くので、何でもいいみたいですが……。
Types: interfaces
type IPeekPoke = abstract Peek: unit -> int abstract Poke: int -> unit type Widget(initialState:int) = let mutable state = initialState interface IPeekPoke with member x.Poke(n) = state <- state + n member x.Peek() = state member x.HasBeenPoked = (state <> 0) let widget = Widget(12) :> IPeekPoke widget.Poke(4) let peekResult = widget.Peek()
interface IPeekPoke { int Peek(); void Poke(int n); } class Widget : IPeekPoke { private int state; public bool HasBeenPoked { get { return state != 0; } } public Widget(int initialState) { state = initialState; } public int Peek() { return state; } public void Poke(int n) { state = state + n; } } static void Main(string[] args) { var widget = (IPeekPoke)new Widget(12); widget.Poke(4); var peekResult = widget.Peek(); }
interfaceはabstractな型定義を並べる。ということらしい。定義方法は「メソッド名:引数->引数->戻り値」ですねん。unitはC#でいうところのvoidみたいなもの。で、interfaceの実装は、そのまま中に記述してしまえばいいらしい。これは楽ちん。見慣れない「:>」はキャストの記号。とても、カッコイイです……。
結論
以上、複数回に分けようかとも思ったのですが一気にやってみました。最初F# Tutorialを開いて、少な!こんなんでチュートリアルになってるの?と思ったのですが、意外とギッシリ詰まってた感じです。しっかりチュートリアルになってました。ただ、やっぱチュートリアルなのでこれを覚えたぐらいじゃF#凄い!F#嬉しい!的にはなりません(比較対象がC#2.0だとなったかもしれませんが)でした。日常的に使って、手に馴染ませないと、良さの理解まではいけなさそうです。
あとまあ、やっぱほとんど説明のない、このTutorialのコードだけじゃ適当な理解になってそうで怖い。きちんと時間割いてMSDN見るなりしないと……。ただ、今のとこがっつし覚えよう!と思えてないところはある。本音として、C#でいいぢゃん、と思っているところがかなりあります。これがJava->Scalaの関係だったら違ったかもしれないんですが、うーん。まあ、あとVisualStudioの補完具合とかかな。IntelliSenseに乗ってゴリゴリ書けるような感触がF#にはないので。別に補完効いてないってわけじゃあないのですけど。
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の使用法と実際のコードになります。同じようなことをやりたい人は、適当に書き替えてどうぞ使ってください。突っ込みどころ多数なのでむしろ突っ込んで欲しい……。
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、と思っても我慢してください。どうしても嫌な場合は私の方にメッセージをくれれば、リストからの撤去と、プログラムから以後の追加をしないようなコードを入れたいと思っています。