Использование XPath и WebBrowser Control для выбора нескольких узлов

В примере приложения C# WinForms я использовал элемент управления WebBrowser и JavaScript-XPath, чтобы выбрать один узел и изменить этот узел .innerHtml следующим кодом:

    private void MainForm_Load(object sender, EventArgs e)
    {
        webBrowser1.DocumentText = @"
            <html>
            <head>
                <script src=""http://svn.coderepos.org/share/lang/javascript/javascript-xpath/trunk/release/javascript-xpath-latest-cmp.js""></script>
            </head>
            <body>
            <img alt=""0764547763 Product Details"" 
                src=""http://ecx.images-amazon.com/images/I/51AK1MRIi7L._AA160_.jpg"">
            <hr/>
            <h2>Product Details</h2>
            <ul>
            <li><b>Paperback:</b> 648 pages</li>
            <li><b>Publisher:</b> Wiley; Unlimited Edition edition (October 15, 2001)</li>
            <li><b>Language:</b> English</li>
            <li><b>ISBN-10:</b> 0764547763</li>
            </ul>
            </body>
            </html>
        ";
    }

    private void cmdTest_Click(object sender, EventArgs e)
    {
        string xPath = "//li";
        string code = string.Format("document.evaluate('{0}', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;", xPath);
        var li = webBrowser1.Document.InvokeScript("eval", new object[] { code }) as mshtml.IHTMLElement;

        li.innerHTML = string.Format("<span style='text-transform: uppercase;font-family:verdana;color:green;'>{0}</span>", li.innerText);

    }

Результат выполнения этого кода выглядит следующим образом:

Пример результата выполнения кода

Теперь я хотел бы использовать ту же технику для выбора нескольких узлов <li>под узлом <ul>, и я пишу:

        xPath = "//ul//*";
        code = string.Format("document.evaluate('{0}', document, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);", xPath);
        var allLI = webBrowser1.Document.InvokeScript("eval", new object[] { code }) as mshtml.IHTMLElementCollection;

но возвращаемое значение переменной allLI равно NULL .

если я напишу

        xPath = "//ul//*";
        code = string.Format("document.evaluate('{0}', document, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);", xPath);
        var allLI = webBrowser1.Document.InvokeScript("eval", new object[] { code }); 

тогда возвращаемая переменная allLI не является нулевой, а ее тип значения — COM Object, но к какому более конкретному типу может быть приведено это COM Object, мне неясно.

Есть ли способ выбрать несколько узлов с помощью используемой здесь техники?

[ОТРЕДАКТИРОВАНО]

xPath = "ul//*";

to

xPath = "//ul//*";

[Дополнение]

Я добавил две функции javaScript в свой пример HTML:

<script type=""text/javascript"">
    function GetElementsText (XPath) {
            var xPathRes = document.evaluate ( XPath, document, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);              
            var nextElement = xPathRes.iterateNext ();
            var text = """";
            while (nextElement) {
               text += nextElement.innerText;
               nextElement = xPathRes.iterateNext ();
            }
        return text;
        };

    function GetElements (XPath) {
            var xPathRes = document.evaluate ( XPath, document, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);              
            var nextElement = xPathRes.iterateNext ();
            var elements = new Object();
            var elementIndex = 1;
            while (nextElement) {
               elements[elementIndex++] = nextElement;
               nextElement = xPathRes.iterateNext ();
            }
        return elements;
        };
</script>

Теперь, когда я запускаю следующую строку кода С# в моем методе cmd_TestClick:

var text = webBrowser1.Document.InvokeScript("eval", new object[] { "GetElementsText('//ul')" });

Я получаю текст всех элементов li:

"Paperback: 648 pages \r\nPublisher: Wiley; Unlimited Edition edition (October 15, 2001) \r\nLanguage: English \r\nISBN-10: 0764547763 "

И когда я запускаю следующую строку кода C# в своем методе cmd_TestClick:

var elements = webBrowser1.Document.InvokeScript("eval", new object[] { "GetElements('//ul')" });

Я получаю COM Object, которое не могу преобразовать в IEnumerable<mshtml.IHtmlElement>.

Есть ли способ обработать в коде С# коллекцию javaScript узлов HTML, возвращаемую

var elements = webBrowser1.Document.InvokeScript("eval", new object[] { "GetElements('//ul')" });

?


person ShamilS    schedule 18.08.2014    source источник
comment
Может ли это помочь? stackoverflow.com/a/20783420/1768303   -  person noseratio    schedule 18.08.2014
comment
@Noseratio: я бы хотел избежать использования HTML Agility Pack - я хотел напрямую манипулировать содержимым DOM элемента управления WebBrowser mshtml.IHTMLElement с помощью mshtml.IHTMLElement и/или mshtml.IHTMLElementCollection с помощью mshtml.IHTMLElementCollection.   -  person ShamilS    schedule 18.08.2014


Ответы (1)


Я нашел решение, вот код:

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Windows.Forms;

namespace myTest.WinFormsApp
{
public partial class MainForm : Form
{
    public MainForm()
    {
        InitializeComponent();
    }

    private void MainForm_Load(object sender, EventArgs e)
    {
        webBrowser1.DocumentText = @"
            <html>
            <body>
            <img alt=""0764547763 Product Details"" 
                src=""http://ecx.images-amazon.com/images/I/51AK1MRIi7L._AA160_.jpg"">
            <hr/>
            <h2>Product Details</h2>
            <ul>
            <li><b>Paperback:</b> 648 pages</li>
            <li><b>Publisher:</b> Wiley; Unlimited Edition edition (October 15, 2001)</li>
            <li><b>Language:</b> English</li>
            <li><b>ISBN-10:</b> 0764547763</li>
            </html>
        ";
    }

    private void cmdTest_Click(object sender, EventArgs e)
    {
        var processor = new WebBrowserControlXPathQueriesProcessor(webBrowser1);

        // change attributes of the first element of the list
        {
            var li = processor.GetHtmlElement("//li");
            li.innerHTML = string.Format("<span style='text-transform: uppercase;font-family:verdana;color:green;'>{0}</span>", li.innerText);
        }

        // change attributes of the second and subsequent elements of the list
        var list = processor.GetHtmlElements("//ul//li");
        int index = 1;
        foreach (var li in list)
        {
            if (index++ == 1) continue;
            li.innerHTML = string.Format("<span style='text-transform: uppercase;font-family:verdana;color:blue;'>{0}</span>", li.innerText);
        }

    }

    /// <summary>
    /// Enables IE WebBrowser control to evaluate XPath queries 
    /// by injecting http://svn.coderepos.org/share/lang/javascript/javascript-xpath/trunk/release/javascript-xpath-latest-cmp.js
    /// and to return XPath queries results to the calling C# code as strongly typed
    /// mshtml.IHTMLElement and IEnumerable<mshtml.IHTMLElement>
    /// </summary>
    public class WebBrowserControlXPathQueriesProcessor
    {
        private System.Windows.Forms.WebBrowser _webBrowser;
        public WebBrowserControlXPathQueriesProcessor(System.Windows.Forms.WebBrowser webBrowser)
        {
            _webBrowser = webBrowser;
            injectScripts();
        }

        private void injectScripts()
        {
            // Thanks to: http://stackoverflow.com/questions/7998996/how-to-inject-javascript-in-webbrowser-control

            HtmlElement head = _webBrowser.Document.GetElementsByTagName("head")[0];
            HtmlElement scriptEl = _webBrowser.Document.CreateElement("script");
            mshtml.IHTMLScriptElement element = (mshtml.IHTMLScriptElement)scriptEl.DomElement;
            element.src = "http://svn.coderepos.org/share/lang/javascript/javascript-xpath/trunk/release/javascript-xpath-latest-cmp.js";
            head.AppendChild(scriptEl);

            string javaScriptText = @"
                    function GetElements (XPath) {
                            var xPathRes = document.evaluate ( XPath, document, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);              
                            var nextElement = xPathRes.iterateNext ();
                            var elements = new Object();
                            var elementIndex = 1;
                            while (nextElement) {
                            elements[elementIndex++] = nextElement;
                            nextElement = xPathRes.iterateNext ();
                            }
                        elements.length = elementIndex -1;
                        return elements;
                        };
                   ";
            scriptEl = _webBrowser.Document.CreateElement("script");
            element = (mshtml.IHTMLScriptElement)scriptEl.DomElement;
            element.text = javaScriptText;
            head.AppendChild(scriptEl);
        }

        /// <summary>
        /// Gets Html element's mshtml.IHTMLElement object instance using XPath query
        /// </summary>
        public mshtml.IHTMLElement GetHtmlElement(string xPathQuery)
        {
            string code = string.Format("document.evaluate('{0}', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;", xPathQuery);
            return _webBrowser.Document.InvokeScript("eval", new object[] { code }) as mshtml.IHTMLElement;
        }

        /// <summary>
        /// Gets Html elements' IEnumerable<mshtml.IHTMLElement> object instance using XPath query
        /// </summary>
        public IEnumerable<mshtml.IHTMLElement> GetHtmlElements(string xPathQuery)
        {
            // Thanks to: http://stackoverflow.com/questions/5278275/accessing-properties-of-javascript-objects-using-type-dynamic-in-c-sharp-4
            var comObject = _webBrowser.Document.InvokeScript("eval", new object[] { string.Format("GetElements('{0}')", xPathQuery) });
            Type type = comObject.GetType();
            int length = (int)type.InvokeMember("length", BindingFlags.GetProperty, null, comObject, null);

            for (int i = 1; i <= length; i++)
            {
                yield return type.InvokeMember(i.ToString(), BindingFlags.GetProperty, null, comObject, null) as mshtml.IHTMLElement;
            }
        }
    }

}
}

И вот результаты выполнения кода:

Результаты выполнения кода

Я поместил ссылки на кредиты в свой код. Если вы обнаружите, что я что-то пропустил, укажите мне в своих комментариях, и я добавлю их.

Если вы знаете лучшее решение - более короткий код, более эффективный код - прокомментируйте и/или опубликуйте свой ответ.

person ShamilS    schedule 18.08.2014
comment
Этот js для заполнения массива элементами не будет работать для сайта google.com/ дает усеченное название компании для xpath //div[@class='_pl _ki']/descendant-or-self::text()[1] как только Broadway не Broadway Chiropractic & Wellness - person SIslam; 28.10.2015
comment
В одном конкретном примере это решение возвращает null, в то время как тот же JavaScript, выполненный в Chrome, возвращает правильный элемент. - person Alex Pandrea; 20.03.2020
comment
кроме того, вы можете заключить XPath в двойные кавычки, потому что XPath может содержать двойные кавычки вместо простых: string code = string.Format("document.evaluate(\"{0}\", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;", xPathQuery); вместо document.evaluate('{0}' ... - person Alex Pandrea; 20.03.2020