C#のWebRequestとWebClientでCookie認証をする方法(と、mixiボイスへの投稿)
- 2009-12-17
WebからHTMLをダウンロードするにはWebClientが便利です。が、そのまんまだとCookie認証で躓きます。せっかく便利にダウンロード出来るのに、認証を超えられないんじゃ意味が無いよ!というわけかで幾つかのやり方を紹介したいと思います。海外だと沢山情報が出回っているのですが、日本だとWebClientはクッキーがとれないが検索上位に出てくるので、WebClientの利用を諦めて面倒くさいWebRequestを使う羽目になっている人が多いんじゃないかしらん。WebRequestなら@ITの記事、@IT:.NET TIPS クッキーを使ってWebページを取得するには?が引っかかりますからね。
とりあえず、@ITのmixiへの認証を例題に、まずはWebRequestでのやり方を見てみます。
// WebRequestによるCookie認証
// POSTしてCookieContainerに書き込む
var data = Encoding.ASCII.GetBytes(string.Join("&",
new[] { "next_url=/home.pl", "email=めるあど", "password=ぱすわど" }));
var cookieContainer = new CookieContainer();
var req = (HttpWebRequest)WebRequest.Create("https://mixi.jp/login.pl");
req.CookieContainer = cookieContainer;
req.Method = "POST";
req.ContentType = "application/x-www-form-urlencoded";
req.ContentLength = data.Length;
using (var stream = req.GetRequestStream())
{
stream.Write(data, 0, data.Length);
}
var res = req.GetResponse(); // ここでCookieContainerに書き込まれる
// 以下、そのCookieを使えばアクセスし放題
var reqLog = (HttpWebRequest)WebRequest.Create("https://mixi.jp/show_log.pl");
reqLog.CookieContainer = cookieContainer; // CookieContainerセット
var resLog = reqLog.GetResponse();
using (var stream = resLog.GetResponseStream())
using (var sr = new StreamReader(stream, Encoding.GetEncoding("euc-jp")))
{
Console.WriteLine(sr.ReadToEnd()); // アクセスできてるのを確認
}
CookieContainerを設定すれば、Cookieのサーバーからの取得も送信も全部自動でやってくれる、というのがポイント。そこは楽です。楽なのですが、WebRequest自体が使いづらい。何をやるにも、いちいちStreamがどうだのこうだのなんてウンザリです。ていうか何だこのvarの多さ、変数乱れ打ち! そうなるとついつい、よーしパパ、ラッパー作っちゃうぞー、とか言ってしまいますが、もう見てらんない。.NET FrameworkにはWebClientというMS謹製のラッパーがあるわけなので、それを使いましょう。認証?Cookie?自前で取ればいいんですよ、ヘッダーから。
// WebClientならポストは超簡単!
var wc = new WebClient { Encoding = Encoding.GetEncoding("euc-jp") };
wc.UploadValues("https://mixi.jp/login.pl", new NameValueCollection
{
{"next_url", "/home.pl"},
{"email", "めるあど"},
{"password", "ぱすわど"}
});
// じゃあCookieはどうするの?というと、ResponseHeaderから自前で抽出します
var setCookie = wc.ResponseHeaders[HttpResponseHeader.SetCookie];
var cookies = Regex.Split(setCookie, "(?<!expires=.{3}),")
.Select(s => s.Split(';').First().Split('='))
.Select(xs => new { Name = xs.First(), Value = string.Join("=", xs.Skip(1).ToArray()) })
.Select(a => a.Name + "=" + a.Value)
.ToArray();
var cookie = string.Join(";", cookies);
// 以降は取得したCookieをHeaderに設定しておけばOk
wc.Headers[HttpRequestHeader.Cookie] = cookie;
var result = wc.DownloadString("https://mixi.jp/show_log.pl");
Console.WriteLine(result); // アクセスできてるのを確認
そう、WebClientでも、ResponseHeaderからSetCookieは取れるのです。なので、ここからCookieにバラしてやれば、あとはHeaderに設定するだけなので簡単です。一見WebRequest並に行数がかかっているのですが、大変なのはCookie分解部分だけです。分解がちょっと面倒なのは否めませんが……。基本的にカンマ区切りとなっていますが、有効期限の設定されているものが含まれていると「expires=Fri, 16-Dec-2011」のようにカンマが入ってしまい、単純なSplit(',')では失敗します。なので正規表現の否定戻り読みでexpires=***,の場合は除外しています。あとは、バラしてクッツケテ、を繰り返して生成。そういえばSelect三連打ですが、これはもちろん複数行にすることでSelect一つで済ますこともできます。でも、そこはそれぞれ役割を切って3つに分けるのが、私の美意識、でしょうか。効率を考えれば匿名型なんて作らない方がいいぐらいなのですけどね、効率じゃない良さってのがあるんです。Linqには。
やり方はまだあります。WebClientは本当にただのWebRequestのラッパーで、中では普通にWebRequestを呼んで処理しています。よって、継承してoverrideしてGetWebRequestの辺りを書き換えて、CookieContainerを使うようにすれば非常に簡単です。
class CustomWebClient:WebClient
{
private CookieContainer cookieContainer = new CookieContainer();
// WebClientはWebRequestのラッパーにすぎないので、
// GetWebRequestのところの動作をちょっと横取りして書き換える
protected override WebRequest GetWebRequest(Uri address)
{
var request = base.GetWebRequest(address);
if (request is HttpWebRequest)
{
(request as HttpWebRequest).CookieContainer = cookieContainer;
}
return request;
}
}
// WebClientを継承してちょっと書き換えてやれば一番簡単
var cwc = new CustomWebClient { Encoding = Encoding.GetEncoding("euc-jp") };
cwc.UploadValues("https://mixi.jp/login.pl", new NameValueCollection
{
{"next_url", "/home.pl"},
{"email", "める"},
{"password", "ぱす"}
});
var result = cwc.DownloadString("https://mixi.jp/show_log.pl");
Console.WriteLine(result); // アクセスできてるのを確認
私的にはこれがお薦め。どうせWebRequestはそのまんまじゃ使い辛いので、多かれ少なかれラッパー作るでしょう。出来の悪いラッパーを作る/使うぐらいなら、WebClientの気の利かない部分だけ書き換えた方が良い。 ちなみにCookieの他にもWebClientの気の利かないところとしては、自動でリダイレクトするところが辛い、場合がある。普段はリダイレクトでいいんですが、リダイレクトされると困るシチュエーションもあります、たまに。そんな問題も、CookieContainerと同じくGetWebRequestの部分で、request.AllowAutoRedirectを設定すれば回避出来ます。
Web上のものをゴニョゴニョ処理するのに「Rubyなどのスクリプト言語の良さが目立つ。」というのは、ライブラリの問題にすぎない、ってことですな。XML処理には今やLinq to XMLがあるし、HTMLの取得にしてもちょっと工夫するだけで回避できるのでC#だから書きにくい、なんてことは無いと思っています。いやまあMechanize便利やん、とかありますがありますが。しかしC#には最終兵器、WebBrowserがあるので何とでもなる。HTML解析ならHtml Agility Packを使えば、物凄く簡単に出来ます。
最後に、Twitterの自分の投稿最新20件をmixiボイスに投げ込む、というコードを例として出してみます。CustomWebClientクラスは上に乗っけた奴を使っています。
static void Main(string[] args)
{
var encoding = Encoding.GetEncoding("euc-jp");
// ログイン
var cwc = new CustomWebClient { Encoding = encoding };
cwc.UploadValues("https://mixi.jp/login.pl", new NameValueCollection
{
{"next_url", "/home.pl"},
{"email", "めーる"},
{"password", "ぱすわど"}
});
// 投稿に必要なpost_keyをhtmlから取り出す
var echo = cwc.DownloadString("https://mixi.jp/recent_echo.pl");
var postKey = Regex.Match(echo, "id=\"post_key\" value=\"(.+?)\"").Groups[1].Value;
// 例なので簡易化するため認証無しのTwitterステータスを取得します
// HttpUtilityの利用にはSystem.Webの参照設定が別途必要
var id = "自分の(じゃなくてもいいけど)TwitterID";
var texts = XDocument.Load("http://twitter.com/statuses/user_timeline/" + id + ".xml")
.Descendants("status")
.Select(x => HttpUtility.HtmlDecode(x.Element("text").Value))
.Reverse();
foreach (var text in texts)
{
// mixiボイスに投稿(UTF-8以外の日本語の投稿はUploadValuesが使えない(泣)
cwc.Headers[HttpRequestHeader.ContentType] = "application/x-www-form-urlencoded";
cwc.UploadString("http://mixi.jp/add_echo.pl", string.Join("&", new[]
{
"body=" + HttpUtility.UrlEncode(text, encoding),
"post_key=" + postKey,
"redirect=recent_echo"
}));
}
}
差分を記録するようにしたり、@付きを除外したりするようにすれば、そこそこ使えるんじゃないかしらん。利用はご自由にどうぞ。