TERRYのブログ

なにやら書くかもしれません

【C#】niconicoAPIを使ったりスクレイピングしたり

この記事はC# Advent Calendar 2013 - Adventarの20日目の記事です。
昨日はyfakariyaさんの主に技術日記: .NET の動的コード生成技術の紹介でした。

ゆるふわだと聞いていたのですがやっぱりというかみなさんレベル高すぎですね……。
しかし今更背伸びしてもしょうがないので自分なりに書いていきたいと思います。

niconicoAPI

昔はWebRequestは煩雑すぎ、WebClientは機能少なすぎ……といった感じでC#からWebにアクセスすることをためらっていたのですが、
.Net4.5からHttpClientという便利なクラスが追加されたので、これを使ってあれこれしていきます。
HttpClientについてはneue cc - HttpClient詳解、或いはAsyncOAuthのアップデートについてなどが参考になるかと。
とりあえずは例としてニコニコ動画APIを使ってみましょう。

ログイン

まずはログインですね。
@IT:.NET TIPS クッキーを使ってWebページを取得するには? - C# VB.NET
この辺を参考にしながらやっていきます。
早速ログインフォームのソースを見てみると、

<form action="https://secure.nicovideo.jp/secure/login?site=niconico" method="post" onsubmit="if (tooAdvancedClock) alert('お使いの PC の時計が進みすぎているため、正常にログインができない場合がございます。');">
	<input type="hidden" name="next_url" value="">
	<dl>
		<dt><label for="mail">ログインメールアドレス/電話番号</label></dt>
		<dd><input id="mail" name="mail_tel" type="text" class="txt"></dd>
		<dt><label for="password">パスワード</label></dt>
		<dd>
			<input id="password" name="password" type="password" class="txt">
			<p class="forgetPass">※パスワードを忘れた方は<a href="https://secure.nicovideo.jp/secure/remind_pass">再発行の手続き</a></p>
		</dd>
		<dd class="buttons">
			<div class="wrongPass"></div>
			<div class="login_button"><input type="submit" value="ログイン"></div>
						
		</dd>
	</dl>
</form>

mailに登録メールアドレスを、passwordにパスワードを設定すればよさそうですね。
next_urlの欄にはログイン後のurlが入るみたいですが、空文字でも問題なさそうです。
これらをPOSTしてやりましょう。HttpClientを使うので、System.Net.Httpの参照を忘れずに。

using System.Net;
using System.Net.Http;

/// <summary>
/// ニコニコ動画にログインし、クッキーを受け取ります。
/// </summary>
/// <param name="id">メールアドレス</param>
/// <param name="password">パスワード</param>
/// <returns>ログイン用CookieContainer</returns>
public static async Task<CookieContainer> LoginAsync(string id, string password)
{
    const string loginUrl = "https://secure.nicovideo.jp/secure/login?site=niconico";

    using (var handler = new HttpClientHandler())
    using (var client = new HttpClient(handler))
    {
        var content = new FormUrlEncodedContent(new Dictionary<string, string>
            {
                { "next_url", string.Empty },
                { "mail", id },
                { "password", password }
            });

        await client.PostAsync(loginUrl, content);

        return handler.CookieContainer;
    }
}

エラー処理とかはサボってるので適宜追加してやってください。

niconicoAPIの利用

ともあれこれでログインができたはずなので、早速APIを使っていきます。
ニコニコ動画のAPIまとめ | Web scratchあたりを参考にしながら「とりあえずマイリスト」の中身を列挙してやりましょう。

using System.Json;
using System.Net;
using System.Net.Http;

/// <summary>
/// とりあえずマイリストに登録された動画一覧を表示します。
/// </summary>
/// <param name="cookies">ログイン用CookieContainer</param>
/// <returns></returns>
public static async Task DisplayDefaultMylistAsync(CookieContainer cookies)
{
    const string url = "http://www.nicovideo.jp/api/deflist/list";

    using (var handler = new HttpClientHandler())
    using (var client = new HttpClient(handler))
    {
        handler.CookieContainer = cookies;

        dynamic json = JsonObject.Load(await client.GetStreamAsync(url)).AsDynamic();

        foreach (var item in json.mylistitem)
        {
            Console.WriteLine("{0}: {1}", item.item_data.video_id.Value, item.item_data.title.Value);
        }
    }
}

public static async Task Test()
{
    var cookies = await LoginAsync("hoge@fugafuga.com", "pass1234");
    await DisplayDefaultMylistAsync(cookies);
}

APIはJSON形式のデータを返してくれるのですが、DataContractJsonSerializerを使うためにクラス定義するのは大変なので、
先日紹介したNuGet Gallery | JsonValue 0.6.0を利用しています。
LoadしてAsDynamicするとそれだけで動的な型として扱えるようにしてくれます。偉いですね。
全体の流れとしては、ログインで受け取ったCookieをセットして、HTTP GETしてJSONをdynamicで受けて列挙、という形です。

sm21914855: 【BOF2013】Shiratsuyu
sm21894469: 【FRENZ2013】 B.B.K.K.B.K.K. 【BOF2013】
sm21892733: 【艦これ】艦隊これくしょん/リアル劇場【IL-2】
sm17286161: 【発狂・第二】難易度表のAirを比較してみた【LN・DP・OJ】
...

実行結果としてはだいたいこんな感じになると思います。

Html Agility Packでスクレイピング

APIが用意されてるサイトはこのようにGETしてくればいいのですが、
そうでないサイトの方が多いので、そういうときはスクレイピングしちゃいましょう。
スクレイピングとは"削る"という意味で、Webから得られたデータから必要な情報だけを抽出することを指します。
ただ当然サーバーに負荷をかけることにはなるので、やり過ぎには注意。ということで。

スクレイピング用ライブラリとして、以前は酢酸先生の本に載っていたSgmlReaderを利用していました。
しかし先日拙作診断なんちゃらで使おうとしたらどうもWindowsストアアプリでは使えないようで……。
そんなこんなで悩んでいたら雪猫さんHtml Agility Packを紹介して頂きました。感謝!

早速NuGet経由で落としてきます。

題材は何にしようかなと思ったのですが、折角なのでAdvent Calendarの一覧を表示させることにしましょうか。
htmlソースを見ながら書き書き。

using System.Net.Http;
using System.Web;
using HtmlAgilityPack;

/// <summary>
/// Advent Calenderの一覧を取得します。
/// </summary>
/// <returns></returns>
public static async Task<IEnumerable<string>> EnumerateAdventarTitlesAsync()
{
    const string url = "http://www.adventar.org/calendars";

    using (var client = new HttpClient())
    {
        var html = new HtmlDocument();

        html.LoadHtml(await client.GetStringAsync(url));

        var titles = html.DocumentNode.Descendants("div")
            .Where(node => node.GetAttributeValue("class", string.Empty) == "mod-calendarList-title")
            .Select(node => node.InnerText.Trim())
            .Select(HttpUtility.HtmlDecode);

        return titles;
    }
}

public static async Task Hoge()
{
    foreach (string title in await EnumerateAdventarTitlesAsync())
    {
        Console.WriteLine(title);
    }
}

サイトからGETしてきたソースをHtmlDocument.LoadHtml()で読み込ませてあれこれしていきます。
Linq to Xmlを使っている人ならあんまり違和感なく使えるんじゃないでしょうか?僕は使っていないので分かりませんが。
HtmlNode.GetAttributeValue()はAttributeを取得するヘルパーメソッドで、指定した属性が見つからなかったときのデフォルト値を指定できます。ここではstring.Emptyをデフォルトに。
今回はclass属性が"mod-calendarList-title"となっているdivタグを探すだけだったので楽だったのですが、サイトによってこの辺が煩雑になりやすいのが悩みどころ。
まあ元々抽出向けに作られたデータじゃないので仕方ないですね。
Select二連打してますが、個人的にこちらの方がパイプライン処理っぽく見えて好きです。

実行するとこんな感じでAdvent Calendarのタイトルを表示してくれます。

Node.js
CSS Property
X'mas Card Design
Editors' & Writers'
BEM
Frontrend
#LOVEFONT
Web Accessibility
Kosen
JavaScriptおれおれ
Illumination Photo #IllPhoto
Graphical Web
クックパッドのレシピで冬のあったか料理を作ろう!
寿司
WebPay
仏
...

しかし世界は広いですね……。寿司 Advent Calendarとか仏 Advent Calendarとか……。


とまあ今日はこんな感じで。いつにも増してまとまりがない感じでした。
明日はhiroyuki_moriさんです。よろしくお願いします!