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

异步新闻爬虫去重优化:无需Semaphore且不损失性能

异步爬虫重复数据问题的解决方案

问题背景

我开发了一个带有后台网页爬虫服务的网站,最初用同步模式爬取186000篇新闻需要约24小时。改成异步模式后,发现爬取的30条新闻里仅5条是唯一的,其余全是重复内容。虽然确认过代码逻辑,但问题依然存在。尝试用Semaphore锁定URL列表后解决了重复问题,但爬完全部新闻仍需约23.5小时,完全没达到性能提升的预期。现在需要不用Semaphore就能解决重复问题的方案。

问题代码

private async Task<List<News>> GetTheNewsDataAsync(HtmlWeb web, HtmlDocument doc,
    List<string> links)
{
  var news = new List<News>();
  var baseUrl = "https://www.dnes.bg";

  var date = "";
  var dateParsed = new DateTime(1000, 1, 1, 12, 30, 0);
  var imageSrc = "";

  var title = "";
  var sterilizedContent = "";
  var link = "";

  var tasks = new List<Task<News?>>();

  for (int i = 0; i < links.Count; i++) {
    var index = i;
    tasks.Add(Task.Run(async () => {
      link = links[index];
      doc = await web.LoadFromWebAsync(baseUrl + link);

      var titleElement = doc.DocumentNode.SelectSingleNode("//h1[@class='title']");
      if (titleElement != null) {
        title = titleElement.InnerText;
      }

      if (!await CheckIfNewsExistsAsync(title) && title != "") {
        var contentElement = doc.DocumentNode.SelectNodes("//p");
        if (contentElement != null) {
          sterilizedContent = SterilizeTheNews(contentElement);
        }

        var dateElement = doc.DocumentNode
            .SelectSingleNode("//div[@class='art_author']");

        if (dateElement != null) {

          if (dateElement.InnerText.StartsWith("Обновена")) {
            var pipeIndex = dateElement.InnerText.IndexOf('|');
            date = dateElement.InnerText.Substring("Обновена: ".Length,
                pipeIndex - "Обновена: ".Length).Trim();
          } else {
            date = dateElement.InnerText.Split(',')[0];
          }

          try {
            if (date.Contains("мар")) {
              date = date.Replace("мар", "март");
            }

            dateParsed = DateTime.ParseExact(date, "d MMM yyyy HH:mm",
                new CultureInfo("bg-BG"));
          } catch (Exception e) {
            Console.WriteLine($"Exception parsing the date! {e.Message}");
            dateParsed = new DateTime(1000, 1, 1, 12, 30, 0);
          }
        }

        var imageElement = doc.DocumentNode.SelectSingleNode(
            "//div[@id='article_text']//img");
        if (imageElement != null) {
          imageSrc = imageElement.Attributes["src"].Value;
        } else {
          imageSrc = "https://digitalfinger.id/wp-content/uploads/2019/12/" +
              "no-image-available-icon-6.png";
        }

        var currentNews = new News {
          Title = title,
            Content = sterilizedContent,
            Date = dateParsed,
            ImageUrl = imageSrc
        };

        return currentNews;
      }
      return null;
    }));
  }

  var result = await Task.WhenAll(tasks);

  foreach(var item in result) {
    if (item != null) {
      news.Add(item);
    }
  }
  return news;
}

问题根源

重复数据的核心原因是方法级共享变量的竞态条件:你定义的date、dateParsed、imageSrc、title、sterilizedContent、link、doc都是方法级别的变量,所有异步任务都会同时读写这些变量。当多个任务并行执行时,后执行的任务会覆盖这些变量的值,导致最终生成的News对象使用了被覆盖后的内容,出现大量重复。

解决方案

把所有共享变量移到异步任务内部,让每个任务拥有独立的局部变量,避免跨任务的变量覆盖。同时可以将单条新闻的处理逻辑提取为独立方法,提升代码可读性:

修改后的代码

private async Task<List<News>> GetTheNewsDataAsync(HtmlWeb web, List<string> links)
{
  var news = new List<News>();
  var baseUrl = "https://www.dnes.bg";
  var tasks = new List<Task<News?>>();

  // 遍历链接,为每个链接创建独立的处理任务
  foreach (var link in links)
  {
    tasks.Add(ProcessSingleNewsAsync(web, baseUrl, link));
  }

  var result = await Task.WhenAll(tasks);
  news.AddRange(result.Where(item => item != null));
  return news;
}

// 单独处理单条新闻,所有变量均为局部变量,完全隔离
private async Task<News?> ProcessSingleNewsAsync(HtmlWeb web, string baseUrl, string link)
{
  var doc = await web.LoadFromWebAsync(baseUrl + link);

  // 提取标题
  var titleElement = doc.DocumentNode.SelectSingleNode("//h1[@class='title']");
  if (titleElement == null)
    return null;
  
  var title = titleElement.InnerText.Trim();
  if (string.IsNullOrEmpty(title) || await CheckIfNewsExistsAsync(title))
    return null;

  // 提取内容
  var sterilizedContent = string.Empty;
  var contentElement = doc.DocumentNode.SelectNodes("//p");
  if (contentElement != null)
  {
    sterilizedContent = SterilizeTheNews(contentElement);
  }

  // 解析日期
  var dateParsed = new DateTime(1000, 1, 1, 12, 30, 0);
  var dateElement = doc.DocumentNode.SelectSingleNode("//div[@class='art_author']");
  if (dateElement != null)
  {
    var date = string.Empty;
    if (dateElement.InnerText.StartsWith("Обновена"))
    {
      var pipeIndex = dateElement.InnerText.IndexOf('|');
      date = dateElement.InnerText.Substring("Обновена: ".Length, pipeIndex - "Обновена: ".Length).Trim();
    }
    else
    {
      date = dateElement.InnerText.Split(',')[0].Trim();
    }

    try
    {
      if (date.Contains("мар"))
      {
        date = date.Replace("мар", "март");
      }
      dateParsed = DateTime.ParseExact(date, "d MMM yyyy HH:mm", new CultureInfo("bg-BG"));
    }
    catch (Exception e)
    {
      Console.WriteLine($"Exception parsing the date! {e.Message}");
    }
  }

  // 提取图片链接
  var imageSrc = "https://digitalfinger.id/wp-content/uploads/2019/12/no-image-available-icon-6.png";
  var imageElement = doc.DocumentNode.SelectSingleNode("//div[@id='article_text']//img");
  if (imageElement != null && imageElement.Attributes["src"] != null)
  {
    imageSrc = imageElement.Attributes["src"].Value;
  }

  return new News
  {
    Title = title,
    Content = sterilizedContent,
    Date = dateParsed,
    ImageUrl = imageSrc
  };
}

额外性能优化建议

  1. 移除不必要的Task.Run:LoadFromWebAsync本身就是异步方法,不需要用Task.Run包装,直接调用即可减少线程池资源消耗。
  2. 控制并发数:用SemaphoreSlim限制同时执行的任务数量(比如设置为10-20),避免目标网站触发反爬机制,同时避免本地线程过载:
    private async Task<List<News>> GetTheNewsDataAsync(HtmlWeb web, List<string> links)
    {
      var news = new List<News>();
      var baseUrl = "https://www.dnes.bg";
      var semaphore = new SemaphoreSlim(15); // 限制15个并发任务
      var tasks = new List<Task<News?>>();
    
      foreach (var link in links)
      {
        tasks.Add(ProcessSingleNewsWithSemaphoreAsync(web, baseUrl, link, semaphore));
      }
    
      var result = await Task.WhenAll(tasks);
      news.AddRange(result.Where(item => item != null));
      return news;
    }
    
    private async Task<News?> ProcessSingleNewsWithSemaphoreAsync(HtmlWeb web, string baseUrl, string link, SemaphoreSlim semaphore)
    {
      await semaphore.WaitAsync();
      try
      {
        return await ProcessSingleNewsAsync(web, baseUrl, link);
      }
      finally
      {
        semaphore.Release();
      }
    }
    
  3. 缓存CheckIfNewsExistsAsync结果:如果该方法是查询数据库,重复查询相同标题会浪费资源,可以用ConcurrentDictionary<string, bool>缓存已检查过的标题,减少数据库访问。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 06:30:11