You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于C#与Selenium的动态网站爬取:无头Chrome加载判断与加载屏实现

体育博彩赛事爬取问题解决方案

问题背景

为大学项目开发体育博彩网站爬取应用,需获取当日足球、网球、篮球及手球的赛事与联赛信息。当前使用Selenium做页面导航和滚动爬取,耗时且资源占用高;改用Headless Chrome后,无法判断页面是否完全加载,同时需要实现爬取完成前的加载屏。


一、Headless Chrome页面加载判断优化

原代码仅依赖document.readyState判断页面加载,无法覆盖动态滚动加载的内容。以下是针对性优化方案:

核心优化点

  • 显式等待核心元素:不再仅等待页面状态,改为等待页面中第一个赛事分组标题(group-header__details-title)加载完成,确保核心渲染结构就绪。
  • 滚动加载结束判断:用「连续两次滚动后赛事分组数量无变化」替代滚动位置判断,避免因页面高度计算误差导致的提前终止;同时增加滚动后的固定等待,保证动态内容渲染完成。

修改后的爬取函数

private static List<string> ScrapeLeagues(string url)
{
    List<string> leagues = new List<string>();
    Dictionary<string, double> pozitii = new Dictionary<string, double>();
    IWebElement parent;
    string echipa1, echipa2;
    var timeout = 15000;
    var wait = new WebDriverWait(webDriver, TimeSpan.FromMilliseconds(timeout));

    webDriver.Url = url;
    // 等待核心分组元素加载,替代单一的readyState判断
    wait.Until(d => d.FindElements(By.ClassName("group-header__details-title")).Count > 0);

    // 初始抓取分组与赛事
    CaptureLeaguesAndMatches(leagues, pozitii);

    int previousLeagueCount = leagues.Count;
    int consecutiveNoChange = 0;
    const int maxConsecutiveNoChange = 2; // 连续2次无新增则判定加载完成

    while (consecutiveNoChange < maxConsecutiveNoChange)
    {
        // 平滑滚动到最后一个赛事元素
        ((IJavaScriptExecutor)webDriver).ExecuteScript(
            "const elements = document.getElementsByClassName('event-row-container'); " +
            "if(elements.length > 0) elements[elements.length-1].scrollIntoView({behavior: 'smooth', block: 'end'});");
        
        // 等待滚动后动态内容加载
        Thread.Sleep(1000); // 可根据目标网站加载速度调整
        wait.Until(d => ((IJavaScriptExecutor)d).ExecuteScript("return document.readyState").Equals("complete"));

        // 抓取新增的分组与赛事
        CaptureLeaguesAndMatches(leagues, pozitii);

        // 更新判断状态
        if (leagues.Count == previousLeagueCount)
        {
            consecutiveNoChange++;
        }
        else
        {
            consecutiveNoChange = 0;
            previousLeagueCount = leagues.Count;
        }
    }

    return leagues;
}

// 提取重复抓取逻辑为独立方法,提高代码复用性
private static void CaptureLeaguesAndMatches(List<string> leagues, Dictionary<string, double> pozitii)
{
    var divs = webDriver.FindElements(By.ClassName("group-header__details-title"));
    foreach (IWebElement div in divs)
    {
        string leagueTitle = div.GetAttribute("title");
        if (!pozitii.ContainsKey(leagueTitle))
        {
            leagues.Add(leagueTitle);
            // 获取赛事分组的父容器
            parent = (IWebElement)((IJavaScriptExecutor)webDriver)
                .ExecuteScript("return arguments[0].parentNode.parentNode.parentNode;", div);
            var meciuri = parent.FindElements(By.ClassName("event-row-container"));
            _evenimente.Add(new Eveniment(leagueTitle, meciuri.Count));

            foreach (IWebElement meci in meciuri)
            {
                echipa1 = meci.FindElement(By.ClassName("event-summary__competitors-team1")).GetAttribute("title");
                echipa2 = meci.FindElement(By.ClassName("event-summary__competitors-team2")).GetAttribute("title");
                var cote = meci.FindElements(By.ClassName("e2e-odd-current-value"));

                if (cote.Count == 3)
                {
                    _evenimente.Last().AdaugaMeci(echipa1, echipa2, 
                        cote[0].GetAttribute("innerHTML"), 
                        cote[1].GetAttribute("innerHTML"), 
                        cote[2].GetAttribute("innerHTML"));
                }
                else if (cote.Count == 2)
                {
                    _evenimente.Last().AdaugaMeci(echipa1, echipa2, 
                        cote[0].GetAttribute("innerHTML"), 
                        cote[1].GetAttribute("innerHTML"));
                }
            }
            pozitii.Add(leagueTitle, Convert.ToDouble(div.Location.Y));
        }
    }
}

二、WinForms加载屏实现

利用异步线程分离爬取任务与UI线程,避免主窗体卡顿,同时在爬取过程中显示加载提示:

步骤1:创建加载窗口

新建WinForms窗口LoadingForm,添加一个Label控件显示文本(如「正在爬取赛事数据,请稍候...」),可配合ProgressBar实现动画效果。

步骤2:修改主窗体代码

private async void Form1_Load(object sender, EventArgs e)
{
    using (var loadingForm = new LoadingForm())
    {
        loadingForm.Show(); // 显示加载屏
        try
        {
            // 异步执行爬取任务,避免阻塞UI线程
            await Task.Run(() => 
            {
                webProcessor = WebsiteProcessor.getInstance().Result;
                // 根据业务需求调用ScrapeLeagues,传入对应体育项目的URL
                // var footballLeagues = ScrapeLeagues("足球赛事页面URL");
                // var tennisLeagues = ScrapeLeagues("网球赛事页面URL");
            });
            evenimente = WebsiteProcessor._evenimente;
            // 将爬取数据绑定到主窗体控件(如DataGridView)
            // dataGridViewEvents.DataSource = evenimente;
        }
        catch (Exception ex)
        {
            MessageBox.Show($"爬取失败:{ex.Message}");
        }
        finally
        {
            loadingForm.Close(); // 爬取完成后关闭加载屏
        }
    }
}

内容的提问来源于stack exchange,提问作者sergiutzuu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 22:15:33