HtmlDom は LINQ to HTML を目指していますが、かつ HTML が楽に編集できるように更新系(Update/Delete/Insertなど)のメソッドも準備します。
まあ、内部的には XML に直しているので操作は楽なのですが、なんと HTML のパース部分がちと面倒で。
もともとある System.Forms.HtmlDocument 自体には、Children に相当するコレクションがないので、全 DOM を取ることができないんですよね。
HtmlDocument クラス (System.Windows.Forms)
http://msdn.microsoft.com/ja-jp/library/system.windows.forms.htmldocument(v=vs.110).aspx
トリッキーな作りをすれば、これに沿って LINQ ぐらいは作れるのですが、ちょっと使いづらいということで、独自に HtmlDocument, HtmlNode を作っています。
このとき、HTML 文字列から COM の IHTMLDocument2 を使ってパースするのはこんな感じ。
/// <summary> /// Loading method /// HtmlDocument to create a HTML string /// </summary> /// <param name="html">HTML string</param> /// <returns></returns> public HtmlDocument LoadHtml(string html) { // Creating an object using a mshtml.HTMLDocument var doc = new HTMLDocument() as IHTMLDocument2; doc.write(new object[] { html }); Load(doc); return this; }
非常に簡単で、IHTMLDocument2 インターフェースにキャストして、COM の write メソッドを呼び出すだけです。これは HTML DOM の document.write に対応しているので、javascript まで実行されてしまうのですが、まぁ、大丈夫みたいです。何故か、COM で直接呼び出した時は、javascript 実行でエラーになる(画面のUIコントロールを探してエラーになるという不具合のようです)らしいのですが、実は大丈夫です。
CComPtr<IHTMLDocument2> pDoc; HRESULT hr = CoCreateInstance(CLSID_HTMLDocument, NULL, CLSCTX_INPROC_SERVER, IID_IHTMLDocument2, (void**)&pDoc); //put the code into SAFEARRAY and write it into document SAFEARRAY* psa = SafeArrayCreateVector(VT_VARIANT, 0, 1); VARIANT *param; hr = SafeArrayAccessData(psa, (LPVOID*)¶m); param->vt = VT_BSTR; param->bstrVal = CComBSTR(strHTMLCode).Copy(); hr = pDoc->write(psa); hr = pDoc->close(); SafeArrayDestroy(psa);
ATL COM を使っています。どっかのサンプルから取ってきたので、実は SafeArrayCreateVector は必要ないかもしれません。サンプルを動作させるとエラーになっていたのですが「CComBSTR(strHTMLCode).Copy();」のように、一度コピーを取ることで、うまく実行できます。ためしに、自分の twitter サイトから HTML をダウンロードして来てパースするとうまく動きました。twitter サイトは javascript を多用しているので、これが大丈夫ならば大抵のサイトは大丈夫だと思います。
肝心のスピードですが、C# 経由の COM アクセスは何故か属性の配列を取るところが非常に遅くなっています。
// append attributes IHTMLAttributeCollection attrs = node.attributes; if (attrs != null) { foreach (IHTMLDOMAttribute at in attrs) { if (at.specified) { string nodeValue = ""; if (at.nodeValue != null) nodeValue = at.nodeValue.ToString(); nn.Attrs.Add(new HtmlAttr { Key = at.nodeName, Value = nodeValue }); } } }
部分的なコードですが、foreach のループのところで、attrs の要素を 150 程度廻るのが問題なようです。実は、IHTMLDocument がパースした後の要素では、何故か属性のコレクションが非常にたくさん用意されているのですよね。おそらく onclick などのフック関数のために用意されていると思うのですが、静的なデータを取りたい場合には、これが不要ですし非常に邪魔です。要素数が2,3しかないので、150程度のループを廻すものだから、耐えきれない位おそくなります。
これは、簡単な HTML の場合には問題がなくて、twitter の HTML のように大量な HTML の場合に発覚した現象です。これでは実用に耐えません。
なので、じゃあ、COM アクセス自体を高速化するためにひとまず、C++ で書いてみたのが以下です。
list<XAttr*> *getAttrs( CComQIPtr<IHTMLDOMNode> node ) { auto *xattrs = new list<XAttr*>(); CComPtr<IDispatch> disp; node->get_attributes( &disp ); CComQIPtr<IHTMLAttributeCollection> attrs = disp; if ( attrs ) { long length = 0; attrs->get_length( &length ); CComPtr<IDispatch> dispa; for ( int i=0; i<length; i++ ) { CComVariant vt(i); attrs->item( &vt, &dispa ); CComQIPtr<IHTMLDOMAttribute> attr = dispa ; if ( attr ) { VARIANT_BOOL vtb; attr->get_specified( &vtb ); if ( vtb ) { CComBSTR key; attr->get_nodeName( &key ); CComVariant value; attr->get_nodeValue( &value ); xattrs->push_back(new XAttr( CString(key), CString(value.bstrVal))); } } attr.Release(); dispa.Release(); } } attrs.Release(); disp.Release(); return xattrs; }
ループ変数となる length の値は 150 程度なので同じぐらいループが廻っていますが、非常に高速に動きます。多分、CComBSTR か CComVariant と .NET との相互変換の部分で遅くなっている感じがします。これは後で実測してみるつもりです。
という訳で、IE で使っている IHTMLDocument2 を直接使って自前で DOM を作ることができました。
ですが、このパース部分は C++ となっているので、C# の HtmlNode オブジェクトにしないと LINQ が使えないですよね。
って訳で、C++/CLI の出番なんですよ。ええ、VS2010 では C++/CLI のインテリセンスが効かないので、VS2012 で書いたものを VS2010 に戻しますってな感じです。本当は 2008 が良いんですが、間違ってアンインストールしちゃったんですよね。なので、仕方が無く別マシンの VS2012 を借りるという罰ゲームに(VS2010 に VS2012 を入れると MSTest が正常に動かないんですが、これは RTM でなおっているんでしょうか?)
メガネ アルマーニ レディースadidas http://www.shoessregion.info/