C#のWebRequestとWebClientでCookie認証をする方法(と、mixiボイスへの投稿)

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"
        }));
    }
}

差分を記録するようにしたり、@付きを除外したりするようにすれば、そこそこ使えるんじゃないかしらん。利用はご自由にどうぞ。

Comment (5)

takeshi : (01/06 11:09)

凄く参考になりました

でも何故かGreeはログイン出来ないですね

internal static void GreeLogin()
{
var cwc = new CustomWebClient
{
Encoding = Encoding.GetEncoding(”euc-jp”)
};

cwc.UploadValues(”http://gree.jp/”, new NameValueCollection
{
{”mode”, “common”},
{”act”, “login”},
{”backto”, “”},
{”user_mail”, “メールアドレス”},
{”user_password”, “パスワード”},
{”login_status”, “1″}

});

var result = cwc.DownloadString(”http://gree.jp/?action=home”);

Console.WriteLine(result); // アクセスできてるのを確認
}

う~ん何故なんでしょうか?

neuecc : (01/06 22:09)

どうもです。
Greeですか、ちょっと試してみようと思ったんですが、
GreeってWilcomじゃ登録出来ないんですね……
というわけで試せなかったので推測ですが、CookieContainerの問題があるかもしれません。
CookieContainerは正直なところ色々とよろしくない部分が多くて、上手くいかない場合があります。
詳細は語ると長くなりそうなので省きます。

というわけで、やり方2のCookieをResponseHeaderから自前で抽出して自分でセット
という手法のほうを試してみてください。
もしCookieContainerが問題ならば、こっちならうまく行くはずです。

kaorun : (12/03 11:12)

どもども、いつもお世話になってます。
ResponseHeadersから生のcookieを引っこ抜くコードなのですが、
var setCookie = wc.ResponseHeaders[HttpResponseHeader.SetCookie];
var cookies = Regex.Split(setCookie, “(?<!expires=.{3}),”)
setCookieのnullチェックをしないとCookie返さないページでRegexがどげしゃんと謎のExceptionを出して死んでしまうので、間に
if (setCookie == null)

とか入れるか??でダミーを挿入した方が親切かもしれません。
もちろんサンプルコードなのでエラーハンドリングは無い方が良いという考え方もあると思うのでお好みですが。

neuecc : (12/04 23:50)

おおおー、ありがとうございます。
クッキーを取ってくるという例なので、サーバーがクッキーを返さない場合、
ということ自体を全く考えていませんでした。
ダミーを返す、?? “”はいいですね、と思ったんですが
var setCookie = wc.ResponseHeaders[HttpResponseHeader.SetCookie] ?? “”;
とすると、最終的にcookieの値が”=”になってしまって、ダメですね……。
やっぱ丁寧にif() としてcookie = “” にするのが無難でしょうか。

kaorun : (12/05 16:46)

ありゃ、そうなっちゃいますか~、そうですよね(汗)。 >??
やっぱ手抜きは難しい(ぇ

Name
WebSite(option)
Comment

Trackback(0) | http://neue.cc/2009/12/17_230.html/trackback

Search/Archive

Category

Profile


Yoshifumi Kawai
Microsoft MVP for Developer Technologies(C#)

April 2011
|
July 2020

Twitter:@neuecc
GitHub:neuecc
ils@neue.cc