PG日誌

各記事はブラウザの横幅を1410px以上にすると2カラムの見出しが表示されます。なるべく横に広げてみてください。

【はてなブログ】全て記事のURL・タイトル・投稿日を取得する

前回の記事で投稿済みの記事URL一覧をすべて取得するプログラムを書いてみました。

takap-tech.com

今回は、記事のURLのほかに、各記事のタイトルと投稿日時を取得してCSVファイルに出力してみようと思います。

確認環境

  • C# 8.0
  • VisualStudio 2019
  • .NET Core 3.1

実行結果

以下に紹介するプログラムを実行すると以下の通りになります。

はてなブログを使っている自分のサイトにリクエストを送っています。

// 実行するコマンド
> command.exe takap-tech.com d:\url.csv " - PG日誌"

// 実行結果、長いので改行しています。
// url.csv
0001/01/01 00:00,PG日誌
    ,https://takap-tech.com/
0001/01/01 00:00,このブログについて
    ,https://takap-tech.com/about
2020/07/15 00:58,【C言語】コロナ感染拡大で政府がGoToキャンペーンを強行
    ,https://takap-tech.com/entry/2020/07/15/005828
2020/07/09 00:38,【Unity】効果音(SE)の再生方法と音割れ防止について
    ,https://takap-tech.com/entry/2020/07/09/003837
2020/07/09 00:35,【C#】App.config(アプリケーション構成)で設定を読み込む
    ,https://takap-tech.com/entry/2020/07/09/003530
2020/07/09 00:25,"【C#】2,8,10,16進数文字列と数値の相互変換方法まとめ"
    ,https://takap-tech.com/entry/2020/07/09/002557
....

コード

前回記事はURLを取得するだけでしたら今回は Sitemapクラスでサイトマップから記事URL一覧の取得、HtmlPageクラスで各ページ内の情報へアクセスするように変更しています。

private static readonly XNamespace ns = "http://www.sitemaps.org/schemas/sitemap/0.9";

public static void Main(string[] args)
{
    // 書式:
    // > command.exe ${domain-name} ${out-path} [$remove-word}]
    //
    // e.g.
    // > command.exe takap-tech.com d:\url.csv " - PG日誌"

    // コマンドラインからドメイン名を指定(takap-tech.com)
    string domain = args[0];
    string rootUrl = $"https://{domain}/sitemap.xml";

    // 出力先パス
    string outPath = args[1];

    // 表示するときに削除したい単語があれば指定する
    string removeWord = "";
    if (args.Length == 3)
    {
        removeWord = args[2];
    }

    Console.WriteLine($"{DateTime.Now:HH:mm:ss} 開始 >>>");

    int i = 0;
    using var sitemap = new Sitemap();
    using var sw = new StreamWriter(outPath);
    foreach (var page in sitemap.GetContentsAll(rootUrl))
    {
        string line = $"[{i++}],{page.ToString().Replace(removeWord, "")}";
        sw.WriteLine(line);
        Console.WriteLine(line);
    }

    Console.WriteLine($"{DateTime.Now:HH:mm:ss} 終了 <<<");
}

// サイトマップからURLを抽出するクラス
public class Sitemap : IDisposable
{
    private static HttpClient _client;
    public static HttpClient Client => _client ??= new HttpClient();

    public void Dispose()
    {
        using (_client) { }
        GC.SuppressFinalize(this);
    }

    public IEnumerable<HtmlPage> GetContentsAll(string rootUrl)
    {
        foreach (var url in this.GetUrlAll(rootUrl))
        {
            yield return new HtmlPage(url, Client.GetStringAsync(url).Result);
        }
    }

    public IEnumerable<string> GetUrlAll(string rootUrl)
    {
        foreach (var sub in this.GetRootItems(rootUrl))
        {
            foreach (var url in this.GetSubItems(sub))
            {
                yield return url;
            }
        }
    }

    public IEnumerable<string> GetRootItems(string rootUrl)
    {
        string body = Client.GetStringAsync(rootUrl).Result;

        var xml = XDocument.Parse(body);
        var e1 = xml.Element(ns + "sitemapindex");
        var e2 = e1.Elements(ns + "sitemap");

        foreach (var e3 in e2)
        {
            yield return e3.Element(ns + "loc").Value;
        }
    }

    public IEnumerable<string> GetSubItems(string subUrl)
    {
        string body = Client.GetStringAsync(subUrl).Result;

        var xml = XDocument.Parse(body);
        var e1 = xml.Element(ns + "urlset");
        var e2 = e1.Elements(ns + "url");

        foreach (var e3 in e2)
        {
            yield return e3.Element(ns + "loc").Value;
        }
    }
}

// 取得したURLのページを表すクラス
public class HtmlPage
{
    private readonly string _body;

    public string Url { get; private set; }

    public string Title => Regex.Match(this._body, "<title>(?<name>.*)</title>").Groups["name"].Value;

    public DateTime Time
    {
        get
        {
            string timeStr = Regex.Match(this._body,
                "<time data-relative.*>(?<time>.*)</time>").Groups["time"].Value;
            if (DateTime.TryParseExact(timeStr, 
                "yyyy-MM-dd HH:mm", null, System.Globalization.DateTimeStyles.None, out DateTime ret))
            {
                return ret;
            }
            else
            {
                return DateTime.MinValue;
            }
        }
    }

    public HtmlPage(string url, string body)
    {
        this.Url = url;
        this._body = body;
    }

    public override string ToString() =>
        $"\"{this.Time:yyyy/MM/dd HH:mm}\"\t\"{this.Title}\"\t\"{this.Url}\"";
}