HttpClientクラスを使ってWebページを取得する際に、文字化けが発生しないよう、Webページのエンコーディングを推測/設定して取得する方法を解説する。
対象:.NET 4.5以降
.NET Framework 4.5で新設されたHttpClientクラス(System.Net.Http名前空間)のGetStringAsyncメソッドを使うと、簡単にWebページの内容を文字列として取得できる。しかし、文字コードにシフトJISを使っているWebサイトでは文字化けしてしまう。どうすれば文字化けさせることなく取得できるだろうか? 本稿では、そのような文字化けが発生する条件を説明し、そんな場合にWebページの内容を文字列として取得する方法を解説する。
HttpClientクラスのGetStringAsyncメソッドを使ってWebページの内容を文字列として取得する方法は、「.NET TIPS:HttpClientクラスでWebページを取得するには?[C#、VB]」で紹介した(そのコードを、以降では「以前のコード」と呼ぶ)。なお、その記事で解説していることは本稿であらためて説明しないので、併せてお読みいただきたい。
以前のコードでURLを書き換えて、シフトJISのWebサイトにアクセスしてみよう。例えば「http://www.atmarkit.co.jp/」にアクセスすると、次の画像のように文字化けが発生する。
UTF-8などのUnicodeを使ったWebサイト以外では、必ず文字化けしてしまうのだろうか? そうではない。文字コードにシフトJISやEUCやJISを使っているWebページでも、正常に取得できる場合があるのだ。例えば「http://www.shugiin.go.jp/」(このサイトはシフトJIS)のページは文字化けさせることなく取得できる(次の画像)。
両者の違いがどこにあるのかというと、Webサーバーからのレスポンスに含まれているHTTPヘッダー(HTMLのヘッダーではない)の「Content-Type」フィールドである(次の画像)。「Content-Type」フィールドにcharsetパラメーターが設定されていると、HttpClientクラスのGetStringAsyncメソッドで正常に取得できるのだ。
以上のことから、HttpClientクラスのGetStringAsyncメソッドは、HTTPヘッダーの「Content-Type」フィールドでエンコーディングが指定されていればそれに従い、そうでなければUnicodeとして解釈していると推定できる*1。
そのような仕様になっているので、Webサーバーの管理者がきちんとHTTPヘッダーの設定をしていれば、HttpClientクラスのGetStringAsyncメソッドで文字化けは発生しないはずなのだ。ところが、本稿を書くためにあらためていくつかのWebサイトに当たってみたところ、文字コードにシフトJISを使っているサイトのほとんどでcharsetの指定がされていなかった。せっかくの仕様も、日本の現実では役に立たないのである。
そうなると、プログラムの側で何とかするしかない。従来のWebClientクラス(System.Net名前空間)ならば、Encodingプロパティを設定してからDownloadStringメソッドを使えばよかった。しかし、HttpClientクラスにはそのようなプロパティが存在しない。どのようにすればよいだろうか? 以降で解説する。
Webページへのアクセスには、HttpClientクラスのGetAsyncメソッドを使う。そして得られたHttpResponseMessageオブジェクトからストリームを読み出すときに、文字エンコーディングを指定したTextReaderオブジェクトを使えばよい。例えば、文字コードがシフトJISだと分かっているなら、次のコードのようになる。
// ファイルの冒頭に次のインポート文を追加する
using System.IO;
using System.Text;
……省略……
//return await client.GetStringAsync(uri); // ←以前のコード
// HttpClientクラスのGetAsyncメソッドを使ってWebページにアクセスする
HttpResponseMessage res = await client.GetAsync(uri);
// もしも取得に失敗していたら、GetStringAsyncメソッドのときと同じ例外を投げる
res.EnsureSuccessStatusCode();
// 文字エンコーディングはシフトJIS固定とする
Encoding enc = Encoding.GetEncoding("shift_jis");
// 文字エンコーディングを指定してTextReaderオブジェクトを作り、ストリームから読み取る
using (var stream = (await res.Content.ReadAsStreamAsync()))
using (var reader = (new StreamReader(stream, enc, true)) as TextReader)
{
return await reader.ReadToEndAsync();
}
' ファイルの冒頭に次のインポート文を追加する
Imports System.IO
Imports System.Text
……省略……
'Return Await client.GetStringAsync(uri) ' ←以前のコード
' HttpClientクラスのGetAsyncメソッドを使ってWebページにアクセスする
Dim res As HttpResponseMessage = Await client.GetAsync(uri)
' もしも失敗していたら、GetStringAsyncメソッドのときと同じ例外を投げる
res.EnsureSuccessStatusCode()
' 文字エンコーディングはシフトJIS固定とする
Dim enc As Encoding = Encoding.GetEncoding("shift_jis")
' 文字エンコーディングを指定してTextReaderオブジェクトを作り、ストリームから読み取る
Using stream = Await res.Content.ReadAsStreamAsync()
Using reader = DirectCast(New StreamReader(stream, enc, True), TextReader)
Return Await reader.ReadToEndAsync()
End Using
End Using
これで、以前のコードでは文字化けしていた「http://www.atmarkit.co.jp/」にアクセスしてみると、今度は正しく表示される(次の画像)。
アクセスするWebページの文字コードがあらかじめ分かっている場合は、上記のコードでよい。しかし任意のWebページにアクセスする場合には、文字エンコーディングを決め打ちできない。そのWebページの文字エンコーディングを推定する必要があるのだ。
ここで注意してほしいのは、文字エンコーディングを確実に判断できるロジックは存在しないということだ。今でもWebブラウザーでたまに文字化けが起きることがある。絶対確実な判定方法はないのである。以下で説明する方法も、あくまでもっともらしい文字エンコーディングを推定する方法の一つである。
W3Cの勧告によれば、文字エンコーディングの推定は次の順に行うよう推奨されている*2。
HttpClientクラスを使う場合、1.のHTTPヘッダーは、HttpResponseMessageオブジェクトのContentプロパティのHeadersプロパティで取得できる。そして、2.と3.は、HTMLのコンテンツそのものを読み取って解析することになる。読み取るための文字エンコーディングを判断するためには、一度読み取らねばならないのである。文字エンコーディングが分からないのに読み取らねばならないというのは矛盾しているようだが、HTMLのタグ/属性と文字エンコーディング名はASCIIコードの範囲内で書かれているのだから、ASCIIかUTF-8として読み込んでみればよいのだ。それもやはりHttpResponseMessageオブジェクトのContentプロパティから読み込める。すなわち、HttpClientクラスのGetAsyncメソッドで得られたHttpResponseMessageオブジェクトから、文字エンコーディングの推定が可能である。
HttpResponseMessageオブジェクトを受け取って文字エンコーディングを推定するメソッドは、次のコードのように書ける。
// ファイルの冒頭に次のインポート文を追加する
using System.Text.RegularExpressions;
……省略……
static async Task<Encoding> DetermineEncodingAsync(HttpResponseMessage res)
{
// まず、HTTPヘッダーのContent-Typeフィールドを見る
string charset = res.Content.Headers.ContentType.CharSet;
if (!string.IsNullOrWhiteSpace(charset))
{
try
{
// Content-TypeフィールドのcharsetパラメーターからEncodingの生成に成功したら、それを返す
return Encoding.GetEncoding(charset);
}
catch { }
}
// 次に、HTMLの中でcharset属性を探す
// 取りあえずUTF-8だとして読んでみる
string html = null;
using (var ms = new MemoryStream())
{
// ここでReadAsStreamAsyncメソッドを使ってしまうと、
// HttpResponseMessageオブジェクトのストリーム自体が消えてしまう。
// そのため、ここではストリームのコピーを作る
await res.Content.LoadIntoBufferAsync();
await res.Content.CopyToAsync(ms);
ms.Position = 0;
using (var reader = (new StreamReader(ms, Encoding.UTF8, true)) as TextReader)
{
html = await reader.ReadToEndAsync();
}
}
// charset属性を探す
// HTML4の <meta http-equiv="Content-Type" content="text/html; charset={エンコーディング名}">
// HTML5の <meta charset="{エンコーディング名}">
// HTML4/5の <{任意の要素名} charset="{エンコーディング名}">
var charsetEx = new Regex(@"<[^>]*\bcharset\s*=\s*[""']?(?<charset>\w+)\b",
RegexOptions.CultureInvariant
| RegexOptions.IgnoreCase
| RegexOptions.Singleline);
Match charsetMatch = charsetEx.Match(html);
if (charsetMatch.Success)
{
try
{
// 発見した最初のcharset属性からEncodingの生成に成功したら、それを返す
return Encoding.GetEncoding(charsetMatch.Groups["charset"].Value);
}
catch { }
}
// 以上で決定できなかったときは、既定値としてUTF-8を返す
return Encoding.UTF8;
}
// ファイルの冒頭に次のインポート文を追加する
Imports System.Text.RegularExpressions
……省略……
Async Function DetermineEncodingAsync(res As HttpResponseMessage) As Task(Of Encoding)
' まず、HTTPヘッダーのContent-Typeフィールドを見る
Dim charset As String = res.Content.Headers.ContentType.CharSet
If (Not String.IsNullOrWhiteSpace(charset)) Then
Try
' Content-TypeフィールドのcharsetパラメーターからEncodingの生成に成功したら、それを返す
Return Encoding.GetEncoding(charset)
Catch ex As Exception
End Try
End If
' 次に、HTMLの中でcharset属性を探す
' 取りあえずUTF-8だとして読んでみる
Dim html As String = Nothing
Using ms = New MemoryStream()
' ここでReadAsStreamAsyncメソッドを使ってしまうと、
' HttpResponseMessageオブジェクトのストリーム自体が消えてしまう。
' そのため、ここではストリームのコピーを作る
Await res.Content.LoadIntoBufferAsync()
Await res.Content.CopyToAsync(ms)
ms.Position = 0
Using reader = DirectCast(New StreamReader(ms, Encoding.UTF8, True), TextReader)
html = Await reader.ReadToEndAsync()
End Using
End Using
' charset属性を探す
' HTML4の <meta http-equiv="Content-Type" content="text/html; charset={エンコーディング名}">
' HTML5の <meta charset="{エンコーディング名}">
' HTML4/5の <{任意の要素名} charset="{エンコーディング名}">
Dim charsetEx = New Regex("<[^>]*\bcharset\s*=\s*[""']?(?<charset>\w+)\b",
RegexOptions.CultureInvariant _
Or RegexOptions.IgnoreCase _
Or RegexOptions.Singleline)
Dim charsetMatch As Match = charsetEx.Match(html)
If (charsetMatch.Success) Then
Try
' 発見した最初のcharset属性からEncodingの生成に成功したら、それを返す
Return Encoding.GetEncoding(charsetMatch.Groups("charset").Value)
Catch ex As Exception
End Try
End If
' 以上で決定できなかったときは、既定値としてUTF-8を返す
Return Encoding.UTF8
End Function
このDetermineEncodingAsyncメソッドを使って、先ほどの「シフトJISのWebページを取得するコード例」を、次のコードのように汎用的に書き直せる。
// HttpClientクラスのGetAsyncメソッドを使ってWebページにアクセスする
HttpResponseMessage res = await client.GetAsync(uri);
// もしも取得に失敗していたら、GetStringAsyncメソッドのときと同じ例外を投げる
res.EnsureSuccessStatusCode();
// 文字エンコーディングを決める
// Encoding enc = Encoding.GetEncoding("shift_jis");
Encoding enc = await DetermineEncodingAsync(res);
// 文字エンコーディングを指定してTextReaderオブジェクトを作り、ストリームから読み取る
using (var stream = (await res.Content.ReadAsStreamAsync()))
using (var reader = (new StreamReader(stream, enc, true)) as TextReader)
{
return await reader.ReadToEndAsync();
}
' HttpClientクラスのGetAsyncメソッドを使ってWebページにアクセスする
Dim res As HttpResponseMessage = Await client.GetAsync(uri)
' もしも失敗していたら、GetStringAsyncメソッドのときと同じ例外を投げる
res.EnsureSuccessStatusCode()
' 文字エンコーディングを決める
' Dim enc As Encoding = Encoding.GetEncoding("shift_jis")
Dim enc As Encoding = Await DetermineEncodingAsync(res)
' 文字エンコーディングを指定してTextReaderオブジェクトを作り、ストリームから読み取る
Using stream = Await res.Content.ReadAsStreamAsync()
Using reader = DirectCast(New StreamReader(stream, enc, True), TextReader)
Return Await reader.ReadToEndAsync()
End Using
End Using
これで、ほとんどのWebサイトで文字化けなくWebページを取得できるだろう。前述したように、文字エンコーディングを推定する完璧な方法はないので、これでも文字化けする可能性は残っている。プログラムには、エンドユーザーが補助的に文字コードを指定できる仕組みを用意しておくべきであろう。
*1 HTTPヘッダーの「Content-Type」フィールドのcharsetパラメーターに従って文字コードを解釈するという仕様は、.NET Framework 4のWebClientクラス(System.Net名前空間)のDownloadStringメソッド以来のものだ。このメソッドは、.NET Framework 3.5まではcharasetパラメーターを参照しなかったのである。この変更には筆者も少し関わった。
*2 W3C HTML4.01 Specificationの5.2.2を参照(原文、「HTML 4仕様書邦訳計画補完委員会」による日本語訳)。なお、HTTPヘッダーを優先すべきなのは、文字コードを変換するプロキシサーバーへの配慮からだといわれている。
利用可能バージョン:.NET Framework 4.5以降
カテゴリ:クラスライブラリ 処理対象:ネットワーク
使用ライブラリ:HttpClientクラス(System.Net.Http名前空間)
使用ライブラリ:Regexクラス(System.Text.RegularExpressions名前空間)
関連TIPS:HttpClientクラスでWebページを取得するには?[C#、VB]
関連TIPS:Encodingクラスで扱えるエンコーディング名は?[C#、VB]
Copyright© Digital Advantage Corp. All Rights Reserved.