備忘録です。
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つも文字コード規格がある、なんてことにいちいち対応してられないんでしょうね。
で、ストリームから得られたバイト配列の文字コードを判別する方法は、検索してみると、下記に詳しく解説されていますね。
この、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 を利用するあたりのマーシャリングとかの仕組みがいまいち分かってないので、また勉強しないといけないな~。