備忘録です。
System.Net.WebRequest / System.Net.WebResponse クラスを使ってWeb上のHTMLを取得するのは、非常に簡単にできます。MSDNドキュメントにもサンプルがありますし、ネット上でも豊富にサンプルがあるので問題はないです。
が!、取得したHTMLファイルを元に、DOMアクセスさせようとすると途端に.NET Frameworkプログラマーの初心者には、行き詰まってしまいます。 その原因として二つの壁にぶち当たりました。
- そもそもHTML の DOMパーサーが、標準でクラスライブラリにない。
- Web上から取得したHTMLファイル内に使われている文字コードの判別
と、文字コード変換を担うクラスライブラリが標準で提供されていない。
このうち(1)のDOMパーサーについては・・・
Internet Explorer で使われている(正確には WebBrowser コントロールだけど)mshtml.dllが提供するパーサーに任せることができます。mshtml.dll のタイプライブラリが登録されているので、参照を追加することでDOMアクセスができます。具体的には・・・
//HTML内のリンクを列挙するサンプル
using Microsoft;
public static void DomAccessSample(string html)
{
var hd = new mshtml.HTMLDocument();
var hd2 = hd as mshtml.IHTMLDocument2;
hd2.write(html);
foreach (mshtml.IHTMLElement link in hd2.links)
{
Console.WriteLine((string)link.getAttribute("href", 0));
}
}
こんな感じでしょうか。Visual StudioのIDEを使わず、コマンドラインからビルドするには、
#アセンブリの位置はあくまで僕の環境です。
csc /r:C:\Windows\assembly\GAC\Microsoft.mshtml\7.0.3300.0__b03f5f7f11d50a3a\Microsoft.mshtml.dll ソースファイル.cs
のように、GACに登録されているアセンブリ(DLLファイル)を参照に加えてビルド。
さて、次は、(2)なんですが・・・
MSDNでのサンプルなんかでは、System.Net.WebResponse.GetResponseStream() メソッドから得られるストリーム(Webから取得したバイト列)を、適切なSystem.Text.EncodingとともにSystem.IO.StreamReaderクラスのコンストラクタに渡して、ReadLIneメソッドなどで文字列(string)などを得るようなコードが提示されています。↓のような感じ。
http://msdn.microsoft.com/ja-jp/library/system.net.webresponse.getresponsestream(v=VS.90).aspx
しかしながら・・・このMSDNのサンプルコードには不満があります。というか、このコードの実用性は全く無い。
(サンプルなんだから当然っちゃー、当然なんですけどね。)
だって、取得したストリームに使用されている文字コードをUTF8と決めうちしてるんだもんねぇ~。 Web上のテキスト・リソースは、日本に限っただけでも、主にShift_JIS/EUC-JP/ISO-2022-JP/UTF-8 があるし・・・。
Perlで言えばEncode モジュールのような文字コードを判別する処理が必須なんですけども .NET Framework の標準クラスライブラリにはそういったクラスが無い。まぁ、彼ら(.NET Framework の開発者)からしてみれば同じ言語なのに3つも4つも文字コード規格がある、なんてことにいちいち対応してられないんでしょうね。
で、ストリームから得られたバイト配列の文字コードを判別する方法は、検索してみると、下記に詳しく解説されていますね。
文字コードを判別する (DOBON.NET)
この、mlang を使用する方法がお手軽でいいんじゃないかと・・・。Internet Explorer で使われている mlang.dll を利用します。上記サイトで解説されているのように、mlang.idlファイル(WindowsSDKに同梱されています) から、タイプライブラリを生成してレジストリに登録し、Visual C#のプロジェクトに参照を加えることでmlang(MultiLanguage)を利用します。Visual StudioのIDEを使わない場合は、tlbimp.exe で タイプライブラリからプロキシ(ラッパー?)アセンブリ(DLLファイル)を生成して、コンパイルするときにそのDLLを/rオプションで読み込ませます。
//コードの大半は、上記サイトからのコピペ。
//バイト配列から文字コードを推測して、System.Text.Encoding を返す
using System.Text;
using MultiLanguage;
public static Encoding DetectEncoding(byte[] bytes)
{
sbyte[] sb = (sbyte[])(object)bytes;
int len = sb.Length;
var ml = new CMultiLanguageClass() as IMultiLanguage2;
int scores = 5; //実際には1で十分だけど
var detects = new tagDetectEncodingInfo[scores];
//文字コード判別
ml.DetectInputCodepage(8|4,0,ref sb[0],ref len,out detects[0],ref scores);
//DetectInputCodepageメソッドから返ると、scoresには実際にdetect配列に格納された候補数が入っている。
//返された、DetectEncodingInfoとscores(候補数)を見てみる。
//DetectEncodingInfo.nCodePageがコードページ。
//Windows(日本語)なら932とかUTF-8なら65001とか入っているはず。
Console.Error.WriteLine("Scores = {0}",scores);
for(int i=0;i<scores;i++)
{
Console.Error.Write("CodePage[{0}] = {1}; ",i,(int)detects[i].nCodePage);
Console.Error.Write("Confidence[{0}] = {1}; ",i,(int)detects[i].nConfidence);
Console.Error.Write("Percentage[{0}] = {1}; ",i,(int)detects[i].nDocPercent);
Console.Error.WriteLine("");
}
//一番初めのtagDetectEncodingInfoのEncodingを取得
Encoding enc = Encoding.GetEncoding((int)detects[0].nCodePage);
//後始末らしい。
System.Runtime.InteropServices.Marshal.ReleaseComObject(ml);
return enc;
}
COMインターフェイスのIMultiLanguage2::DetectInputCodepage() メソッドは、複数の候補を返すことができ、DetectEncodeInfo 構造体にどのくらい正確かを示すint値が入っているようで、それらから判断するようです。ちなみに、IMultiLanguage2::DetectInputCodepage() メソッドの第一引数には、判別するバイト列がどのようなものかを予め入力できて、僕は、4(MLDETECTCP_DBCS) | 8(MLDETECTCP_HTML) = 12 を指定しました。
ただ、僕の環境~WIndows7 x64版~では、WindowsSDK v7.0 + .NET Framework 3.5 だと、ソースをビルドするとき、ターゲットプラットフォームを X86 (つまり32ビットね) にしないと、メチャクチャなコードページ値が返るか、COM参照の例外(HRESULT: E_FAIL)が発生して落ちてしまいます。
ノートパソコン(VAIO Z)の方は、WindowsSDK v7.1 + .NET Framework 4 なんですが、こちらはエラーが出ず正常。
ちょっと原因分からず。タイプライブラリはちゃんと64bit,32bitとともに登録しているんですが・・・やり方が間違ってんでしょうかねぇ・・・IE9をインストールしているので、古いWindowsSDKだとダメなのかもしれませんね(あくまで推測)
ただ、.NET Framework から COM を利用するあたりのマーシャリングとかの仕組みがいまいち分かってないので、また勉強しないといけないな~。