はてなダイアリー to HTML

はてなダイアリーの記事を根こそぎ取得してローカル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。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

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

Twitter:@neuecc GitHub:neuecc

Archive