HTML DOM access with C#

備忘録です。

System.Net.WebRequest / System.Net.WebResponse クラスを使ってWeb上のHTMLを取得するのは、非常に簡単にできます。MSDNドキュメントにもサンプルがありますし、ネット上でも豊富にサンプルがあるので問題はないです。

が!、取得したHTMLファイルを元に、DOMアクセスさせようとすると途端に.NET Frameworkプログラマーの初心者には、行き詰まってしまいます。 その原因として二つの壁にぶち当たりました。

  1. そもそもHTML の DOMパーサーが、標準でクラスライブラリにない。
  2. 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 を利用するあたりのマーシャリングとかの仕組みがいまいち分かってないので、また勉強しないといけないな~。

Out of sync with server ??

VIsual C# 2008 と MySQL Connector / NET 6.4.3 で、ブロードキャストされるUDPパケットを受信してDBのテーブルへ格納する、という一連のコードをC#で組んでましたら、ローカルに立てたMySQLには普通に接続できてSQLクエリーも処理されるんだけど、LAN内の他のマシン上のMySQLサーバーへSQL文を投げると・・・

Out of sync with server

という今まで見たことのないメッセージとともに例外がスローされてしまって困ってました。

コードはごく普通のものなんだけど・・・。

/*
  myInsertQuery は、テーブルへINSERTするSQL文(string型)
*/
using System.Data.Common;
...
try
{
  lock(DBConnection)
    {
      DBConnection.Open();
      
      DbCommand command = DBConnection.CreateCommand();
      command.CommandText = myInsertQuery;
      command.ExecuteNonQuery();
      
      DBConnection.Close();
    }
}
catch(Exception e)
{
  MessageBox.Show(e.Message);
  return false;
}

クライアントと同じマシン上のMySQLサーバーへのクエリーは成功するんですが、LAN内の他のマシン上のMySQLサーバーへのクエリーがどうやっても “Out of sync with server”というエラー。

ローカルマシンは、WindowsXP + MySQL 5.1 (正常に処理される)
リモートマシンは、CentOS 5.5 + MySQL 5.0.77 (エラーが出る)

訳が分からない。で、MySQL Connector/NET が6.4.4にマイナーアップしていたので、入れ替えたら、普通に成功した。

なんだよ! ただのConnectorのバグかよ・・・orz

ググったら、同じエラー・メッセージでbugレポート出てた・・・。

今年はC#を本気で使っていこう

と、思ってます(^^;

本来は年始の挨拶ですが、喪中なので、「寒中お見舞い申し上げます」 です(m_m)

C#はちょこちょこと触ってつまみ食い程度に使っていたのですが、JScript/Perlなどの便利なLL言語の手軽さに負けてあまり本気で使っていなかったのですが、とある解説本を読んでC#に傾倒しつつあります。

C# 3.0 になって、これまでのC# 2.0 のときに感じた僕の不満点が一気に解決されていました。特にラムダ式とLINQは、非常に煩わしいコード記述を、まるでスクリプトを書いているような感覚で、うまく使えば非常に読みやすいコードの記述ができる武器になりそうです。 普通ならスクリプトで組むようなものでも、これからは気軽にC#を使っていこうかな、と思うようになってきました。

C#といえば、事実上、Windows の .NET Framework上でしか動作しない、という認識でしたが、オープンソースで開発されている Monoをインストールすれば、Windows以外のプラットフォームでも十分使えるようです。CentOSに mono をインストールして、Windows上でビルドしたプログラムが動くかテストしてみましたが、すんなり動くことにちょっと感動しました(笑) フォームを使ったものも含めて、完全に互換性が保てるようになれば、状況が変わると思いますねぇ。

いままでは、どんな小さなプログラムでも、使うか使わないかは別にしてWebサーバーでの使用も考えて、Perl や PHP を使ってコーディングすることがほとんどでしたが、Mono がもっとオープンソース系のサーバー系OSで普及してくれれば、今まで以上にC# を気軽に使えるんですがねぇ~。どうなんでしょうねぇ~。

よくC# と Java は設計思想が似てて、言語的にC#はJavaを真似してる、という意見もありますが、C# を知れば知るほど、Javaとは全く違う言語だということが分かります。

いろんな意見がありますが、僕的に思うことは、 趣味でプログラムするなら、JavaよりC#の方が断然面白い、です。

が、ちょっと、「それはやりすぎじゃね?」 と思うこともあります。
たとえば・・・

/* C# 2.0のときは、*/
List<string> a = new List<string>();

と記述しますが、C++のtemplateときもそうだったんですが・・・同じ語が繰り返し出てきたりして、ずーーーーーと前から不満でした。が、 C# 3.0になって var キーワードが使えるようになったおかげで、スマートに書くことができますよね。

/* C# 3.0 では・・・*/
var a = new List<string>();

これはいい。a の型をコンパイラが考えてくれる。

/* でもこれは・・・すでに何をやってるのか僕には見当もつきません(^^;;;*/
var ar = new[]{1,2,3};

/*  これは下と同じですが */
int[] ar = new int[]{1,2,3};

/* こっちの方がわかりやすいし何をやっているかも一目瞭然だと思うのですが・・・。 */
var ar = new int[]{1,2,3};

こんなやりすぎ感が漂う C# ですが、Perl ほどワケワカメな書き方はできないし、この、程々のやり過ぎ感がちょうどいいんじゃないかと思います(^^;;;

.NET Framework のライブラリが膨大すぎて、必要なクラスがどの名前空間にあるか、すでにライブラリ・クラスの海の中におぼれかけてますが、今年からC#を極力使っていきたいと思います。

でもWindowsでちょっと凝ったことしたいときは、やっぱり Windows APIを直接叩く必要があるのを、次のWindows8でどうにかしてほしいです。。。