はてなダイアリー 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。
Comment (0)
Trackback(1) | http://neue.cc/2010/03/09_246.html/trackback
- はてなダイアリーの記事をダウンロードしてローカルで読む | Web scratch : (05/02 18:22)
[…] はてなダイアリーで運営されているブログの記事をまとめてダウンロードしてローカルのHTMLとして読みたいというときに、neue cc – はてなダイアリー to HTML というコマンドラインソフトを使うと便利です。実行するとダウンロードしたい対象のはてなIDなどが要求されるのでそれに対して入力していくだけ、そのブログの記事を一つのhtmlファイルにまとめてダウンロードできます。 […]