連載
» 2015年01月06日 14時29分 UPDATE

.NET TIPS:HttpClientクラスでWebページを取得するには?[C#、VB]

.NET Framework 4.5で新設されたHttpClientクラスを使い、Webページの内容を非常にシンプルなコードで取得する方法を解説する。

[山本康彦,BluewaterSoft/Microsoft MVP for Windows Platform Development]
.NET TIPS
Insider.NET

 

「.NET TIPS」のインデックス

連載目次

対象:.NET 4.5以降


 HTTP/HTTPSプロトコルを使ってWebページを取得するには、さまざまな方法がある。これまでは、WebClientクラスWebRequest/WebResponseクラス(いずれもSystem.Net名前空間)がよく使われてきた。本稿では、.NET Framework 4.5で新設されたHttpClientクラス(System.Net.Http名前空間)の特徴と、それを使ってWebページの内容を取得する基本的な方法を解説する。

HttpClientクラスの特徴

 ひと言でいえば、とても使いやすくなっている。Webページの内容を取得するコードは、オプション設定や例外処理を無視するなら1行で書けるのだ(次のコード)。

string htmlString = await (new HttpClient()).GetStringAsync("http://dev.windows.com/ja-jp");

Dim htmlString As String = Await (New HttpClient()).GetStringAsync("http://dev.windows.com/ja-jp")

HttpClientクラスでWebページの内容を文字列として取得する端的なコード例(上:C#、下:VB)
端的にはこの1行だけで、Webページの内容を文字列として取得できる。
HttpClientクラスを利用する前に、プロジェクトの参照設定にSystem.Net.Httpアセンブリを追加し、ソースコードの先頭でSystem.Net.Http名前空間をインポートしておく。
HttpClientクラスのGetStringAsyncメソッドは非同期に実行されるメソッドであり、この例のようにawait(C#)/Await(VB)キーワードが必要だ。また、このコードを含むメソッドのシグネチャにはasync/Asyncキーワードを付ける必要がある*1。これまでのWebClientクラスなどを使った実装では、UIをフリーズさせないために非同期処理を書くのは少々面倒だった。非同期対応したことも、HttpClientクラスの特徴である。
実際には、このコードの他にユーザーエージェント文字列などのオプション設定をすることも多いだろうし、通信エラーなどに対処するための例外処理は必須である。そのような実用的なコードは後述する。

 それだけではなく、HttpClientクラスはREST*2にも対応している。RESTのアクセス方法に対応した、PutAsync/GetAsync/PostAsync/DeleteAsyncという四つのメソッドが用意されているのだ。

 また、HttpClientクラスは、HTTPのリクエストをHttpRequestMessageオブジェクト、レスポンスをHttpResponseMessageオブジェクト(いずれもSystem.Net.Http名前空間)として扱う。これらを使うことで、例えばPOSTデータを送信するときに、簡潔にデータをセットできるようになっている。

 さらに、送受信の際に別の処理を割り込ませることも可能になっている。そのような処理は、HttpMessageHandler抽象クラス(System.Net.Http名前空間)を継承したクラスを作り、そのSendAsyncメソッドをオーバーライドして実装する。そして、HttpClientクラスをインスタンス化するときにその(HttpMessageHandlerクラスを継承した)クラスのオブジェクトを渡す。この仕組みを使えば、例えばクッキーの送信*3や、Internet Explorerの設定とは異なるプロキシの利用*4や、ユーザー認証の必要なサイトへの透過的なアクセス*5などが可能になる。

HttpClientクラスを使ってWebページを取得するには?

 前述した1行だけのコードに、ユーザーエージェント文字列の設定や例外処理などを追加して、実用的なものにしてみよう。Webページの内容を取得する部分を「GetWebPageAsync」という名前のメソッドにまとめると、次のコードのようになる。

using System;
using System.Net.Http;
using System.Threading.Tasks;

……省略……

static async Task<string> GetWebPageAsync(Uri uri)
{
  using (HttpClient client = new HttpClient())
  {
    // ユーザーエージェント文字列をセット(オプション)
    client.DefaultRequestHeaders.Add(
        "User-Agent",
        "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko");

    // 受け入れ言語をセット(オプション)
    client.DefaultRequestHeaders.Add("Accept-Language", "ja-JP");

    // タイムアウトをセット(オプション)
    client.Timeout = TimeSpan.FromSeconds(10.0);

    try
    {
      // Webページを取得するのは、事実上この1行だけ
      return await client.GetStringAsync(uri);
    }
    catch (HttpRequestException e)
    {
      // 404エラーや、名前解決失敗など
      Console.WriteLine("\n例外発生!");
      // InnerExceptionも含めて、再帰的に例外メッセージを表示する
      Exception ex = e;
      while (ex != null)
      {
        Console.WriteLine("例外メッセージ: {0} ", ex.Message);
        ex = ex.InnerException;
      }
    }
    catch (TaskCanceledException e)
    {
      // タスクがキャンセルされたとき(一般的にタイムアウト)
      Console.WriteLine("\nタイムアウト!");
      Console.WriteLine("例外メッセージ: {0} ", e.Message);
    }
    return null;
  }
}

Imports System.Net.Http

……省略……

Async Function GetWebPageAsync(uri As Uri) As Task(Of String)

  Using client = New HttpClient()

    ' ユーザーエージェント文字列をセット(オプション)
    client.DefaultRequestHeaders.Add(
        "User-Agent",
        "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko")

    ' 受け入れ言語をセット(オプション)
    client.DefaultRequestHeaders.Add("Accept-Language", "ja-JP")

    ' タイムアウトをセット(オプション)
    client.Timeout = TimeSpan.FromSeconds(10.0)

    Try
      ' Webページを取得するのは、事実上この1行だけ
      Return Await client.GetStringAsync(uri)

    Catch e As HttpRequestException
      ' 404エラーや、名前解決失敗など
      Console.WriteLine(vbCr + "例外発生!")
      ' InnerExceptionも含めて、再帰的に例外メッセージを表示する
      Dim ex As Exception = e
      While (ex IsNot Nothing)
        Console.WriteLine("例外メッセージ: {0} ", ex.Message)
        ex = ex.InnerException
      End While

    Catch e As TaskCanceledException
      ' タスクがキャンセルされたとき(一般的にタイムアウト)
      Console.WriteLine(vbCr + "タイムアウト!")
      Console.WriteLine("例外メッセージ: {0} ", e.Message)
    End Try

    Return Nothing

  End Using
End Function

HttpClientクラスでWebページの内容を文字列として取得するメソッドの例(上:C#、下:VB)
前述の1行だけのコードに対して、ユーザーエージェント文字列の設定や例外処理などを追加した。
文字列をコンソールに出力している部分は、他のプラットフォーム(WPF/Windows Forms/ストアアプリ(Windows Phoneは8.1以降)/ASP.NETなど)では適宜変更してほしい。
HttpClientクラスを利用する前に、プロジェクトの参照設定にSystem.Net.Httpアセンブリを追加し、ソースコードの先頭でSystem.Net.Http名前空間をインポートしておく必要がある。
HttpClientクラスのGetStringAsyncメソッドは非同期に実行されるメソッドであり、この例のようにGetStringAsyncメソッドの呼び出し部にawait(C#)/Await(VB)キーワードを、また、このメソッドのシグネチャにはasync/Asyncキーワードを付ける必要がある*1。また、このメソッドが返す型(=「Task<string>」)についても、注*1を参照してもらいたい。
なお、このVBのコードでは行連結シーケンス(=空白+アンダースコア「_」+改行)を使わずに改行している箇所があるが、Visual Studio 2010から導入された「暗黙の行連結」機能である。

 コンソールプログラムで上の「GetWebPageAsync」メソッドを呼び出すMainメソッドは、次のコードのようになる。

using System;
using System.Net.Http;
using System.Threading.Tasks;

class Program
{
  static void Main(string[] args)
  {
    Console.WriteLine("HttpClientクラスでWebページを取得する");

    // 時間計測用のタイマー
    var timer = new System.Diagnostics.Stopwatch();
    timer.Start();

    // 取得したいWebページのURI
    Uri webUri = new Uri("http://dev.windows.com/ja-jp");
    //Uri webUri = new Uri("https://dev.windows.com/ja-jp"); // HTTPSでもOK!
    //Uri webUri = new Uri("https://dev.windows.com/"); // デフォルトではリダイレクト先を取得してくれる

    //Uri webUri = new Uri("https://appdev.microsoft.com/"); // 403エラー
    //Uri webUri = new Uri("https://appdev.microsoft.com/ja-JP/"); // 404エラー
    //Uri webUri = new Uri("http://notexist.example.com/"); // リモート名を解決できないエラー

    // GetWebPageAsyncメソッドを呼び出す
    Task<string> webTask = GetWebPageAsync(webUri);
    webTask.Wait(); // Mainメソッドではawaitできないので、処理が完了するまで待機する
    string result = webTask.Result;  // 結果を取得

    timer.Stop();
    Console.WriteLine("{0:0.000}秒", timer.Elapsed.TotalSeconds);
    Console.WriteLine();

    // 取得結果を使った処理
    if (result != null)
    {
      // サンプルとして、取得したHTMLデータの<h1>タグ以降を一定長だけ表示
      Console.WriteLine("========");
      int h1pos = result.IndexOf("<h1", StringComparison.OrdinalIgnoreCase);
      if (h1pos < 0) 
        h1pos = 0;
      const int MaxLength = 720;
      int len = result.Length - h1pos;
      if (len > MaxLength)
        len = MaxLength;
      Console.WriteLine(result.Substring(h1pos, len));
      Console.WriteLine("========");
    }

#if DEBUG
    Console.ReadKey();
#endif
  }

  static async Task<string> GetWebPageAsync(Uri uri)
  {
    ……省略……
  }
}

Imports System.Net.Http

Module Module1

  Sub Main()
    Console.WriteLine("HttpClientクラスでWebページを取得する")

    ' 時間計測用のタイマー
    Dim timer = New System.Diagnostics.Stopwatch()
    timer.Start()

    ' 取得したいWebページのURI
    Dim webUri As Uri = New Uri("http://dev.windows.com/ja-jp")
    'Dim webUri As Uri = New Uri("https://dev.windows.com/ja-jp") ' HTTPSでもOK!
    'Dim webUri As Uri = New Uri("https://dev.windows.com/") ' デフォルトではリダイレクト先を取得してくれる

    'Dim webUri As Uri = New Uri("https://appdev.microsoft.com/") ' 403エラー
    'Dim webUri As Uri = New Uri("https://appdev.microsoft.com/ja-JP/") ' 404エラー
    'Dim webUri As Uri = New Uri("http://notexist.example.com/") ' リモート名を解決できませんでした。

    ' GetWebPageAsyncメソッドを呼び出す
    Dim webTask As Task(Of String) = GetWebPageAsync(webUri)
    webTask.Wait() ' Mainメソッドではawaitできないので、処理が完了するまで待機する
    Dim result As String = webTask.Result ' 結果を取得

    timer.Stop()
    Console.WriteLine("{0:0.000}秒", timer.Elapsed.TotalSeconds)
    Console.WriteLine()

    ' 取得結果を使った処理
    If (result IsNot Nothing) Then

      ' サンプルとして、取得したHTMLデータの<h1>タグ以降を一定長だけ表示
      Console.WriteLine("========")
      Dim h1pos As Integer = result.IndexOf("<h1", StringComparison.OrdinalIgnoreCase)
      If (h1pos < 0) Then
        h1pos = 0
      End If
      Const MaxLength As Integer = 720
      Dim len As Integer = result.Length - h1pos
      If (len > MaxLength) Then
        len = MaxLength
      End If
      Console.WriteLine(result.Substring(h1pos, len))
      Console.WriteLine("========")
    End If

#If DEBUG Then
    Console.ReadKey()
#End If
  End Sub

  Async Function GetWebPageAsync(uri As Uri) As Task(Of String)
    ……省略……
  End Function
End Module

「GetWebPageAsync」メソッドを呼び出すMainメソッドの例(上:C#、下:VB)
Visual Studioからデバッグ実行したとき、コンソールがすぐに閉じてしまわないように「Console.ReadKey()」と記述してある。そこで何かキーを押すとプログラムは終了する。

実行結果

 上のコードを試してみよう。そのまま実行すると、次の画像のようにWebページの内容が表示される。

HTTPのページを取得できた HTTPのページを取得できた

 コード中でコメントにしてある他のURLも試してみよう。まず、HTTPS対応とリダイレクト対応であるが、どちらも上の画像と同じ出力が得られ、対応していると確認できる。HTTPSは、表示されることで対応していると分かるだろう。リダイレクトは、日本語のページが表示されることで分かる(リダイレクトされなければ英語ページになる。受け入れ言語をセットしている部分を削って試してほしい)。

 次に、エラーになるURLも見ておこう(次の画像)。画像の上から順に、403エラー/404エラー(どちらもWebサーバーでのエラー)、そしてDNSでの名前解決に失敗したときのエラーである。

エラーが発生した場合の例(403)
エラーが発生した場合の例(404)
エラーが発生した場合の例(DNS) エラーが発生した場合の例
上は、Webサーバーが該当ページを閲覧拒否した場合のエラー(=403)。
中は、Webサーバーで該当ページが見つからなかった場合のエラー(=404)。
下は、DNSで名前解決に失敗した場合のエラー。「リモート名を解決できませんでした」というメッセージは、キャッチした例外のInnerExceptionプロパティに入っている例外のもの。

 また、「GetWebPageAsync」メソッドの中でセットしているタイムアウト時間を極端に短くすると(例えば0.1秒)、次の画像のようなエラーが得られる。

タイムアウトになった場合の例 タイムアウトになった場合の例
タイムアウト時間を短く設定して、わざとタイムアウトを発生させた。

*1 Visual Studio 2012で導入されたasync/awaitキーワードを用いる非同期プログラミングについて、詳しくは次のMSDNのドキュメントや@ITの記事を参照していただきたい。

*2 REST(REpresentational State Transfer)については、次の記事が参考になるだろう。

*3 クッキーをセットするには、HttpMessageHandlerクラスを継承しているHttpClientHandlerクラス(System.Net.Http名前空間)を利用する。HttpClientHandlerオブジェクトを作り、そのCookieContainerプロパティにクッキーをセットする。そして、そのHttpClientHandlerオブジェクトをHttpClientクラスのコンストラクター引数に渡せばよい。クッキーをセットするこの方法は、以前より少々面倒になった部分である。なお、プロキシの設定やリダイレクト可否の設定も同様にして行う。

*4 プロキシを使う方法が次の記事に掲載されている。

*5 ユーザー認証の必要なサイトにアクセスする例として、OAuth認証を使ってtwitterにアクセスする方法が次の記事に掲載されている(英文)。


利用可能バージョン:.NET Framework 4.5以降
カテゴリ:クラスライブラリ 処理対象:ネットワーク
使用ライブラリ:HttpClientクラス(System.Net.Http名前空間)
関連TIPS:WebClientクラスでWebページを取得するには?関連TIPS:WebRequest/WebResponseクラスでWebページを取得するには?


「.NET TIPS」のインデックス

.NET TIPS

Copyright© 1999-2017 Digital Advantage Corp. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。