Как динамически генерировать HTML-код с помощью .NET WebBrowser или mshtml.HTMLDocument?

Большинство ответов, которые я прочитал относительно этой темы, указывают либо на класс System.Windows.Forms.WebBrowser, либо на интерфейс COM mshtml.HTMLDocument из библиотеки Microsoft HTML Object Library.

Класс WebBrowser не привел меня нигде. Следующий код не может получить код HTML, отображаемый моим веб-браузером:

[STAThread]
public static void Main()
{
    WebBrowser wb = new WebBrowser();
    wb.Navigate("https://www.google.com/#q=where+am+i");

    wb.DocumentCompleted += delegate(object sender, WebBrowserDocumentCompletedEventArgs e)
    {
        mshtml.IHTMLDocument2 doc = (mshtml.IHTMLDocument2)wb.Document.DomDocument;
        foreach (IHTMLElement element in doc.all)
        {
                    System.Diagnostics.Debug.WriteLine(element.outerHTML);
        }     
    };
    Form f = new Form();
    f.Controls.Add(wb);
    Application.Run(f);
} 

Вышеприведенный пример. Мне не очень интересно найти обходной путь для выяснения названия города, где я находилюсь. Мне просто нужно понять, как программно извлекать данные динамически генерируемых данных.

(Вызов новой System.Net.WebClient.DownloadString( "https://www.google.com/#q=where+am+i" ), сохраните полученный текст где-нибудь, найдите название города, в котором вы в настоящее время расположены, и дайте мне знать, сможете ли вы найти его.)

Но когда я получаю доступ к https://www.google.com/#q=where+am+i из своего веб-браузера (то есть или firefox), я вижу название моего города, написанное на веб-странице. В Firefox, если я нажимаю правой кнопкой мыши на название города и выбираю "Осмотреть элемент (Q)", я отчетливо вижу название города, написанное в HTML-коде, которое, похоже, сильно отличается от необработанного HTML, возвращаемого WebClient.

После того, как я устал играть в System.Net.WebBrowser, я решил дать mshtml.HTMLDocument выстрел, чтобы закончить тем же бесполезным сырым HTML:

public static void Main()
{
    mshtml.IHTMLDocument2 doc = (mshtml.IHTMLDocument2)new mshtml.HTMLDocument();
    doc.write(new System.Net.WebClient().DownloadString("https://www.google.com/#q=where+am+i"));

    foreach (IHTMLElement e in doc.all)
    {
            System.Diagnostics.Debug.WriteLine(e.outerHTML);
    }
} 

Я предполагаю, что должен быть элегантный способ получить такую ​​информацию. Сейчас я могу только добавить элемент управления WebBrowser в форму, перейти к рассматриваемому URL-адресу, отправить ключи "CLRL, A" и скопировать все, что будет отображаться на странице в буфер обмена, и попытаться проанализируйте его. Это ужасное решение.

Ответ 1

Я хотел бы внести некоторый код в Алексей ответить. Несколько моментов:

  • Строго говоря, не всегда возможно определить, когда страница закончила рендеринг со 100% -ной вероятностью. Некоторые страницы являются довольно сложными и используют непрерывные обновления AJAX. Но мы может быть довольно близко, путем опроса текущего моментального снимка HTML для изменений и проверка свойства WebBrowser.IsBusy. То, что LoadDynamicPage ниже.

  • Некоторая логика тайм-аута должна присутствовать над вышесказанным, в случае, если рендеринг страниц бесконечен (примечание CancellationTokenSource).

  • Async/await - отличный инструмент для кодирования этого, поскольку он дает линейную кода к нашей асинхронной логике опроса, что значительно упрощает ее.

  • Важно включить рендеринг HTML5 с помощью Функция браузера Control, поскольку WebBrowser работает в режиме эмуляции IE7 по умолчанию. То, что SetFeatureBrowserEmulation делает ниже.

  • Это приложение WinForms, но концепция может быть легко преобразована в консольное приложение.

  • Эта логика хорошо работает с указанным вами URL-адресом: https://www.google.com/#q=where+am+i.

using Microsoft.Win32;
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WbFetchPage
{
    public partial class MainForm : Form
    {
        public MainForm()
        {
            SetFeatureBrowserEmulation();
            InitializeComponent();
            this.Load += MainForm_Load;
        }

        // start the task
        async void MainForm_Load(object sender, EventArgs e)
        {
            try
            {
                var cts = new CancellationTokenSource(10000); // cancel in 10s
                var html = await LoadDynamicPage("https://www.google.com/#q=where+am+i", cts.Token);
                MessageBox.Show(html.Substring(0, 1024) + "..." ); // it too long!
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }

        // navigate and download 
        async Task<string> LoadDynamicPage(string url, CancellationToken token)
        {
            // navigate and await DocumentCompleted
            var tcs = new TaskCompletionSource<bool>();
            WebBrowserDocumentCompletedEventHandler handler = (s, arg) =>
                tcs.TrySetResult(true);

            using (token.Register(() => tcs.TrySetCanceled(), useSynchronizationContext: true))
            {
                this.webBrowser.DocumentCompleted += handler;
                try 
                {           
                    this.webBrowser.Navigate(url);
                    await tcs.Task; // wait for DocumentCompleted
                }
                finally
                {
                    this.webBrowser.DocumentCompleted -= handler;
                }
            }

            // get the root element
            var documentElement = this.webBrowser.Document.GetElementsByTagName("html")[0];

            // poll the current HTML for changes asynchronosly
            var html = documentElement.OuterHtml;
            while (true)
            {
                // wait asynchronously, this will throw if cancellation requested
                await Task.Delay(500, token); 

                // continue polling if the WebBrowser is still busy
                if (this.webBrowser.IsBusy)
                    continue; 

                var htmlNow = documentElement.OuterHtml;
                if (html == htmlNow)
                    break; // no changes detected, end the poll loop

                html = htmlNow;
            }

            // consider the page fully rendered 
            token.ThrowIfCancellationRequested();
            return html;
        }

        // enable HTML5 (assuming we're running IE10+)
        // more info: https://stackoverflow.com/a/18333982/1768303
        static void SetFeatureBrowserEmulation()
        {
            if (LicenseManager.UsageMode != LicenseUsageMode.Runtime)
                return;
            var appName = System.IO.Path.GetFileName(System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName);
            Registry.SetValue(@"HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_BROWSER_EMULATION",
                appName, 10000, RegistryValueKind.DWord);
        }
    }
}

Ответ 2

Ваш код веб-браузера выглядит разумным - дождаться чего-то, что захватит текущий контент. К сожалению, официального "Я не выполнял JavaScript, не стесняйтесь, чтобы украсть контент" из браузера и JavaScript.

Может потребоваться какое-то активное ожидание (не Sleep, но Timer) и зависит от страницы. Даже если вы используете браузер без браузера (т.е. PhantomJS), у вас будет такая же проблема.