はてなダイアリー to HTML
- 2010-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。