我如何从基于Blazor的网站解析HTML?C#

4

问题

有一个学生课表的网页:https://education.khai.edu/union/schedule/

当我通过链接发送请求时,我收到的是“空”HTML。这意味着响应的HTML只包含JS脚本,没有课程表HTML:

<!DOCTYPE html>
<html lang="ru-UA">
<head>

    ...

    <!--Blazor:{"sequence":1,"type":"server","descriptor":"CfDJ8MBhIHG65FtKlX56pWtbUtQZ6T285HAgOYeCRtbbe9JO4U4cZsYQJ9xvkrUrO01rnP\u002BgDnPMCl0MnI0E/fW58mYoqDZ3J1ztRz/DKm9\u002BABDrmL5ArBFfTFdeO82HHavNnd1E10j7gHBU9uqKmOW2otP1y5s/a\u002BnMT/P2jvdetcCcDQvdfLnX2/w747D4dYNA1MuoeBRlst63xJlks\u002BYeAfhhNMi1s961JEi777JANAEi\u002B9g\u002BNf7aS9sLn\u002BbJZ4m0IBrUnCcHbu3idntWrD/GDpgDVCwhrIhUIPhs8ITgqZHJdQprUnffKWflcMbJ6YyyWBBABTi2eOX/VMHvtFWxT8ABDgmXbyqC3vTfRe6VlwN5ibDYH/UKDkULoJuX\u002Bw\u002BQB2e3sSP1OddN/ud8pWe5\u002BuCo3\u002BkQ9OG6x2GLMXJHWgah"}-->
</head>
<body class="x-background">

<!--Blazor:{"sequence":0,"type":"server","descriptor":"CfDJ8MBhIHG65FtKlX56pWtbUtTdcyRUeUr\u002BhT344Mo3B4Gc0Gg3YwX1FY0c9owxv7oR1MDnLFR1BTJFjhuwYAjnROc3JT8UhSCkRbOdLVMuG0iwpNvwHNc47\u002BrguaHCTkDZKvZ9GKc0Jp\u002BCX0hcssqhCnp6eka\u002BG9Q7XF2B4ARhWnuJDKvUT\u002BbuWra063kFqG0Ixs4eWc4KrPRNS1KnTVu3QZrmx8r9dx6iyQXHjN/YgTqJhcv9LoQqWTfncbhBLwGm9l0BCTBLn3fGdJsOB6ES0lRwvVygmY7DA/2OGzhY7jGppr6UNaUXhdgo4xZDi3FkZgY3OL5xGS1p0bkc14UU9TM="}-->
    <script async src="/ui/app.js?v=3hNtGnhO8Vl6rh70OirKX4BnS6mxiiS5k9p3XAvofZA"></script>
    <script src="_framework/blazor.server.js" autostart="false"></script>
    <script src="_content/Blazor-Analytics/blazor-analytics.js"></script>
    <script>
    async function connectionDown() {
        console.log("Blazor disconnected");
        location.reload();
    }

    function connectionUp() {
        console.log("Blazor connected");
    }

    window.Blazor.start({
        reconnectionOptions: {
            maxRetries: 10,
            retryIntervalMilliseconds: 500,
        },
        reconnectionHandler: {
            onConnectionDown: e => connectionDown(e),
            onConnectionUp: e => connectionUp(e)
        }
    });
</script>
</body>
</html>

当我从浏览器打开网页时,该网站启动 SignalR-流并加载所需的日程表 HTML。

我该如何从.NET获得类似的结果?


一些成功案例

我发现该网站使用blazor.server.js脚本来处理SignalR-流。 这是它的源代码:点击这里

连接

我尝试将JS连接代码重写为C#,并且我成功了。JS:

...
async function initializeConnection(options: CircuitStartOptions, logger: Logger, circuit: CircuitDescriptor): Promise<HubConnection> {
  const hubProtocol = new MessagePackHubProtocol();
  (hubProtocol as unknown as { name: string }).name = 'blazorpack';

  const connectionBuilder = new HubConnectionBuilder()
    .withUrl('_blazor')
    .withHubProtocol(hubProtocol);

  options.configureSignalR(connectionBuilder);

  const connection = connectionBuilder.build();
...

.NET - BlazorPackHubProtocol.cs(我找不到其他更改协议名称的方法,因此为MessagePackHubProtocol创建了一个壳):

.NET - BlazorPackHubProtocol.cs(无法找到其他更改协议名称的方法,因此创建了一个MessagePackHubProtocol的外壳):
using System;
using System.Buffers;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.SignalR.Protocol;
using Microsoft.Extensions.Options;

namespace KuzCode.SignalR.Protocols.BlazorPack
{
    public class BlazorPackHubProtocol : IHubProtocol
    {
        private MessagePackHubProtocol _protocol;

        public string Name => "blazorpack"; // if the protocol has another name, connection fails
        public int Version => _protocol.Version;
        public TransferFormat TransferFormat => _protocol.TransferFormat;

        public BlazorPackHubProtocol(IOptions<MessagePackHubProtocolOptions> options)
        {
            _protocol = new(options);
        }

        public BlazorPackHubProtocol() : this(Options.Create(new MessagePackHubProtocolOptions())) { }

        public bool IsVersionSupported(int version) => _protocol.IsVersionSupported(version);

        public bool TryParseMessage(ref ReadOnlySequence<byte> input, IInvocationBinder binder, out HubMessage message)
            => _protocol.TryParseMessage(ref input, binder, out message);

        public void WriteMessage(HubMessage message, IBufferWriter<byte> output)
            => _protocol.WriteMessage(message, output);

        public ReadOnlyMemory<byte> GetMessageBytes(HubMessage message) => _protocol.GetMessageBytes(message);
    }
}

.NET - BlazorPackProtocolDependencyInjectionExtensions.cs(用于连接构建器的易用性):

using KuzCode.SignalR.Protocols.BlazorPack;
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.SignalR.Protocol;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System;

namespace Microsoft.Extensions.DependencyInjection
{
    public static class BlazorPackProtocolDependencyInjectionExtensions
    {
        public static TBuilder AddBlazorPackProtocol<TBuilder>(this TBuilder builder) where TBuilder : ISignalRBuilder
            => builder.AddBlazorPackProtocol(_ => { });

        public static TBuilder AddBlazorPackProtocol<TBuilder>(this TBuilder builder, Action<MessagePackHubProtocolOptions> configure)
            where TBuilder : ISignalRBuilder
        {
            builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IHubProtocol, BlazorPackHubProtocol>());
            builder.Services.Configure(configure);

            return builder;
        }
    }
}

.NET - KhaiClient.cs(连接的主要类):

using Microsoft.AspNetCore.Http.Connections;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace KuzCode.KhaiApiClient
{
    public class KhaiClient
    {
        private HubConnection _hubConnection;

        public KhaiClient()
        {
            _hubConnection = new HubConnectionBuilder()
                .WithUrl("wss://education.khai.edu/_blazor", configuration =>
                {
                    configuration.SkipNegotiation = false;
                    configuration.Transports = HttpTransportType.WebSockets;
                })
                .AddBlazorPackProtocol()
                .ConfigureLogging(logging =>
                {
                    logging.AddConsole();
                    logging.SetMinimumLevel(LogLevel.Debug);
                })
                .Build();
        }

        public async Task ConnectAsync() => await _hubConnection.StartAsync();

        public async Task DisconnectAsync() => await _hubConnection.StopAsync();
    }
}

.NET - Program.cs (for testing):

var khaiClient = new KhaiClient();
khaiClient.ConnectAsync().Wait();

while (true) {}

它正在工作!我有以下日志:

...
info: Microsoft.AspNetCore.Http.Connections.Client.Internal.WebSocketsTransport[1]
      Starting transport. Transfer mode: Binary. Url: 'wss://education.khai.edu/_blazor?id=_f5aporohBDfopQF-CExsA'.

...

info: Microsoft.AspNetCore.SignalR.Client.HubConnection[24]
      Using HubProtocol 'blazorpack v1'.
dbug: Microsoft.AspNetCore.SignalR.Client.HubConnection[28]
      Sending Hub Handshake.

...

dbug: Microsoft.AspNetCore.SignalR.Client.HubConnection[47]
      Receive loop starting.
info: Microsoft.AspNetCore.SignalR.Client.HubConnection[44]
      HubConnection started.

接下来做什么?

现在我尝试重新创建一些请求,但没有成功。

选择另一个组请求(我选择了组613п): enter image description here

第一个响应: enter image description here


源代码:https://github.com/iiKuzmychov/KhaiApiClient

更新:

我找到了一个我想要的中心实现,我可以复制粘贴代码,但我不知道如何初始化它。


你使用什么来浏览网站?如何“打开”链接?请展示一些代码。 - Leandro Bardelli
你需要渲染这个页面,实际上执行 JavaScript 并构建可视化树,就像浏览器一样。你可以使用例如 Chromium 或其各种包装器(Puppeteer、CefSharp)来完成这个任务。 - Evk
@LeandroBardelli 我刚刚使用 HttpClient.GetAsync 创建了一个请求。 - iikuzmychov
@Evk 感谢你的建议,我会尝试这个方法,但我认为这种方法可能需要很多时间和内存。 - iikuzmychov
在一般情况下没有其他方法,因为你必须执行JavaScript。但是,如果你只需要这个特定的页面,那么你可能能够直接连接到它们的SignalR端点,而无需加载任何HTML页面或执行JavaScript。 - Evk
显示剩余2条评论
3个回答

1

一种解决方法是使用CefSharp浏览器(或类似的工具)创建一个桌面应用程序。然后,您可以加载网站,获取生成的源代码和/或执行JavaScript来解析它。


0
在我看来,最好使用基于浏览器的工具来完成这个任务。只需加载网页,等待其加载完毕,然后访问其DOM。
这种方法非常直接,不需要深入研究该页面的工作原理。
以下是一个快速而简单的示例,基于DotNetBrowser及其DevTools.WinForms示例来检查页面内容(我认为也可以通过CefSharp来实现,但我还没有尝试过)。
public partial class Form1 : Form
{
    private IBrowser browser1;
    private IBrowser browser2;
    private IEngine engine;

    #region Constructors

    public Form1()
    {
        Task.Run(() =>
             {
                 engine = EngineFactory
                    .Create(new EngineOptions.Builder
                                {
                                    RenderingMode = RenderingMode.HardwareAccelerated,
                                    RemoteDebuggingPort = 9222
                                }
                               .Build());
                 browser1 = engine.CreateBrowser();
                 browser2 = engine.CreateBrowser();
             })
            .ContinueWith(t =>
             {
                 browserView1.InitializeFrom(browser1);
                 browserView2.InitializeFrom(browser2);

                 browser1.Navigation.LoadUrl("https://education.khai.edu/union/schedule/").ContinueWith(t1=>AccessPageData());
                 browser2.Navigation.LoadUrl(browser1.DevTools.RemoteDebuggingUrl);
             }, TaskScheduler.FromCurrentSynchronizationContext());
        InitializeComponent();
    }

    private void AccessPageData()
    {
        IElement table = browser1.MainFrame.Document
                                .GetElementsByTagName("table")
                                 .FirstOrDefault(el => el.Attributes["class"].Contains("table"));
        if (table != null)
        {
            IEnumerable<IElement> rows = table.GetElementsByTagName("tr");
            foreach (IElement row in rows)
            {
                IEnumerable<IElement> dataCells = row.GetElementsByTagName("td");
                foreach (IElement cell in dataCells)
                {
                    Debug.Write($"\t{cell.InnerText}");
                }
                Debug.WriteLine(string.Empty);
            }
        }


    }

    #endregion

    #region Methods

    private void Form1_FormClosed(object sender, FormClosedEventArgs e)
    {
        engine?.Dispose();
    }

    #endregion
}

输出:

        Понеділок
08:00 - 09:35   405к2 (корпус-2), Правова компетентність (практика), доцент Голубов Артем Євгенович
09:50 - 11:25   426г (головний к.), Геометричне моделювання та графічні інформаційні технології (лекція), доцент Мсаллам Катерина Петрівна
11:55 - 13:30The thread 0x40c8 has exited with code 0 (0x0).
249г (головний к.), Геометричне моделювання та графічні інформаційні технології (лаб. практикум), доцент Мсаллам Катерина Петрівна
13:45 - 15:20   338г (головний к.), Хімія (лаб. практикум), старший викладач Середенко Вікторія Валентинівна
247г (головний к.), Геометричне моделювання та графічні інформаційні технології (практика), доцент Мсаллам Катерина Петрівна
    Вівторок
08:00 - 09:35   222с (самолетка), Українські студії (практика), доцент Медведь Олена Вікторівна
09:50 - 11:25   337г (головний к.), Хімія (лекція), доцент Захарченко Микола Іванович
11:55 - 13:30   
404лк (літальний.к), Мовні компетентності (практика), асистент Федотова Олена Михайлівна
13:45 - 15:20   413к2 (корпус-2), Українські студії (практика), доцент Медведь Олена Вікторівна
    Середа
08:00 - 09:35   426г (головний к.), Правова компетентність (лекція), доцент Голубов Артем Євгенович
413г (головний к.), Мовні компетентності (практика), асистент Кудлай Ольга Ігорівна
09:50 - 11:25   225с (самолетка), Вступ до фаху (лекція), професор Федотов Михайло Миколайович

11:55 - 13:30   Фізичне виховання (практика)
13:45 - 15:20   342г (головний к.), Хімія (лаб. практикум), старший викладач Середенко Вікторія Валентинівна
    Четвер
08:00 - 09:35   129к2 (корпус-2), Вступ до фаху (практика), професор Федотов Михайло Миколайович
304м (моторний к.), Українські студії (лекція), доцент Медведь Олена Вікторівна
09:50 - 11:25   206лк (літальний.к), Лінійна алгебра та аналіт. геом. (лекція), доцент Кощавець Петро Тихонович
11:55 - 13:30   415г (головний к.), Мовні компетентності (практика), асистент Федотова Олена Михайлівна
13:45 - 15:20   401лк (літальний.к), Лінійна алгебра та аналіт. геом. (практика), доцент Кощавець Петро Тихонович
    П'ятниця
08:00 - 09:35   412к2 (корпус-2), Правова компетентність (практика), доцент Голубов Артем Євгенович
09:50 - 11:25   343г (головний к.), Мовні компетентності (практика), асистент Кудлай Ольга Ігорівна
11:55 - 13:30   303с (самолетка), Вступ до фаху (лекція), старший викладач Миронова Світлана Юріївна
303с (самолетка), Вступ до фаху (практика), старший викладач Миронова Світлана Юріївна
13:45 - 15:20   Фізичне виховання (практика)

因此,所有数据都可用作C#字符串。然后,您可以模拟一些操作在网页上切换到另一个日程表,并再次访问该页面的DOM。


不幸的是,它对我没有起作用,在调试窗口中没有得到任何时间表文本。 - iikuzmychov
网页可能在完全加载后异步构建其DOM树。没有特定的事件可以等待来处理这种情况,但您可以尝试在尝试访问表格之前添加一个小超时时间。 - Anna Dolbina
好的,无论如何还是谢谢! - iikuzmychov

0

我知道Stack Overflow不适用于URL重定向,但简短的回答是“不行”,你无法使用httpClient或WebClient实现。所以...选择的方法是使用Selenium,但是为了满足问题,解释的扩展要复杂得多。

我会给你在这种情况下起作用的教程的URL,但是你必须注意,你的问题的简短答案基本上是“不行”。我认为社区会认为这个问题太笼统,没有重点,无法很快和明确地获得你想要的结果。

使用Selenium进行爬虫的教程(如果您按关键字查找:scrape dynamic web javascript c#,可以找到许多类似的教程)。

https://www.lambdatest.com/blog/scraping-dynamic-web-pages/


谢谢您的回答,但我认为这个解决方案太慢了。我希望能够使用SignalR连接到服务器,这看起来更加优雅。 - iikuzmychov
@KuzCode 确实如此,但您必须了解您要抓取的系统的深层结构,或者使用某种虚假代理来捕获连接内部的工作,例如gatling。 - Leandro Bardelli
1
网站使用blazor.server.js进行连接操作,我找到了源代码,现在尝试将js代码翻译成.net。 - iikuzmychov
@KuzCode 如果你获得更多信息并需要帮助,请更新你的问题,我很乐意更新我的答案。 - Leandro Bardelli
1
感谢您的关注和愿意帮助,我明天会更新我的问题。 - iikuzmychov
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接