【问题标题】:Can I use multithreading and parallel programming for web scraping?我可以使用多线程和并行编程进行网络抓取吗?
【发布时间】:2021-11-24 11:51:19
【问题描述】:

我很难理解多线程和并行编程。我有一个小应用程序(Scraper)。我将 Selenium 与 C# .NET 一起使用。我有一个包含企业地址的文件。然后我用我的爬虫来查找公司名称和他们的网站。之后,我根据他们的公司网站再次抓取通用电子邮件地址

这就是问题所在。如果我手动执行此操作,我需要 3 年时间才能完成 50,000 条记录。我做了数学。哈哈。这就是我创建刮板的原因。一个普通的控制台应用程序需要 5 到 6 天才能完成。然后,我决定也许使用多线程和并行编程可以减少时间。

所以,我做了一个小样本测试。我注意到 1 条记录需要 10 秒。完成。然后有 10 条记录需要 100 秒。我的问题是为什么多线程需要同样的时间?

我不确定我对多线程的期望和理解是否错误。我想通过使用Parallel.ForEach 将启动所有十个记录并在 10 秒内完成,为我节省了 90 秒。这是正确的假设吗?有人可以澄清一下多线程和并行编程的实际工作原理吗?

private static List<GoogleList> MultiTreadMain(List<FileStructure> values)
{
        List<GoogleList> ListGInfo = new List<GoogleList>();
        var threads = new List<Thread>();
        Parallel.ForEach (values, value =>
        {
            if (value.ID <= 10)
            {
                List<GoogleList> SingleListGInfo = new List<GoogleList>();
                var threadDesc = new Thread(() =>
                {
                   lock (lockObjDec)
                   {
                      SingleListGInfo = LoadBrowser("https://www.google.com", value.Address, value.City, value.State,
                                 value.FirstName, value.LastName,
                                 "USA", value.ZipCode, value.ID);
                        SingleListGInfo.ForEach(p => ListGInfo.Add(p));
                    }
                });
                threadDesc.Name = value.ID.ToString();
                threadDesc.Start();
                threads.Add(threadDesc);

            }
        });

        while (threads.Count > 0)
        {
            for (var x = (threads.Count - 1); x > -1; x--)
            {
                if (((Thread)threads[x]).ThreadState == System.Threading.ThreadState.Stopped)
                {
                    ((Thread)threads[x]).Abort();
                    threads.RemoveAt(x);
                }
            }
            Thread.Sleep(1);
        }
     

       return ListGInfo;
}

【问题讨论】:

  • 多线程并不总是更快。首先,您的网络延迟不会变得更短。它实际上可能会变得更糟,因为您正在增加网络连接上的流量。其次,多线程并不能提高服务器响应请求的时间——它实际上会因为服务器负载的增加而减慢它的速度。三、谷歌CPU上下文切换
  • 如果你有 CPU 密集型工作 - Parallel.ForEach,如果你有 IO(读/写 http/file/任何其他异步控制器) - 使用任务。假设您只是在抓取网站,您应该只使用 async+Task 范例(因为,在 Parallel 产生的完整的 CPU 密集型线程上无需等待 10 秒)。任务很轻,通过发回信号而不是自旋锁等待来处理来自网站的异步响应。根据我的经验,您在抓取时的主要顾虑 - 异步 + 内存池(尽可能多的 IP)
  • > 我想通过使用 parallel.Foreach 将启动所有十个记录并在 10 秒内完成,为我节省了 90 秒。是的。这个假设是正确的。如果您的代码表现不同,则说明其他问题。
  • So, I did a small sample test. 我们不能评论我们看不到的代码。
  • 这是 .NET Core 还是 Framework?哪个版本?控制台或网络应用程序(是的,这很重要)?

标签: c# multithreading selenium web-scraping parallel-processing


【解决方案1】:

这可能不是您所面临的具体问题的答案,但它可能暗示了一般问题“为什么多线程不是更快”。假设 Selenium 有一个公共类 EdgeDriver,它是这样实现的:

public class EdgeDriver
{
    private static object _locker = new();

    public void GoToUrl(string url)
    {
        lock (_locker)
        {
            GoToUrlInternal(url);
        }
    }

    internal void GoToUrlInternal(string url) //...
}

作为该类的使用者,您无法看到私有 _locker 字段或内部方法。这些是实现细节,对你隐藏,而了解这个类在做什么的唯一方法是阅读文档。所以 if 实现看起来像上面人为的例子,任何通过创建多个EdgeDriver 实例并在Parallel.ForEach 循环中调用它们的GoToUrl 方法来加速程序的尝试都是徒劳的.静态对象上的lock 将确保一次只允许一个线程调用GoToUrlInternal,而所有其他线程都必须等待轮到它们。这称为“调用被序列化”。这只是多线程可能不会比在单线程上运行的代码快的众多可能原因之一。

【讨论】:

  • 感谢您的回答。这是否意味着如果我删除“锁定”它不会成为一个顺序过程?它会加快这个过程吗?
  • @SANOSUKE lock 是假设的。我对 Selenium 库的内部没有具体的了解。如果那里确实有一个lock,无论出于何种原因,您自己都无能为力。您必须联系图书馆作者并寻求指导。您可能会得到的一个可能答案是:“这种行为是设计使然”。
【解决方案2】:

我希望下面的代码 sn-p 能给你一些指导。我在 FileStructure 列表中的记录之间划分工作。根据问题陈述,我认为这里没有必要加锁

private static List<GoogleList> MultiTreadMain(List<FileStructure> values)
{
    var tasks = new List<Task<List<GoogleList>>>();
    var toBeScraped = values.Where(p => p.Id >= 10);
    Parallel.ForEach (toBeScraped, value =>
    {
        Task<List<GoogleList>> task = Task<List<GoogleList>>.Factory.StartNew(() =>
        {
            return ProcessRequestAsync(value);
        });
        tasks.Add(task);
    });

    var mergedTask = Task.WhenAll(tasks);
    List<GoogleList> ListGInfo = new List<GoogleList>();
    
    foreach(var item in mergedTask.GetAwaiter().GetResult())
    {
        ListGInfo.AddRange(item.GetAwaiter().GetResult());
    }

   return ListGInfo;
}

public static List<GoogleList> ProcessRequestAsync(FileStructure value)
{
     List<GoogleList> SingleListGInfo = new List<GoogleList>();
     SingleListGInfo = LoadBrowser("https://www.google.com", value.Address, value.City, value.State,
                         value.FirstName, value.LastName,
                         "USA", value.ZipCode, value.ID);
     SingleListGInfo.ForEach(p => ListGInfo.Add(p));
     return SingleListGInfo;
}

【讨论】:

  • 为什么要使用并行循环来创建一些任务?使用Task.Factory.StartNew 方法创建任务非常快,并且在一个简单的foreach 循环中创建它们几乎是瞬时的。通过使用并行循环,您的代码现在存在线程安全问题。 List&lt;T&gt;is not thread safe.
  • 我遇到的问题是,通过使用“foreach”,它会遍历每一个。在一个简单的 10 条记录中是快速的。但目标是超过 50,000 条记录。这些记录驻留在文件中。这就是为什么我在 MultiTreadMain 方法中将其称为“FileStructure”列表的原因。想法是同时发送 10 或 100 条记录,以便该过程可以在不到 6 天的过程中完成,这将只需要一个步骤。我对多线程不是很熟悉。我在互联网上寻找并给了我如何去做的想法。我确实注意到它总是与文件写入或读取有关。这种情况不是。
猜你喜欢
  • 2013-07-19
  • 1970-01-01
  • 2020-11-07
  • 2010-11-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多