-1

我一直在做一个网页抓取项目。

我有两个问题,一个是以百分比形式处理的 url 数量,但更大的问题是我无法弄清楚我如何知道我创建的所有线程何时完全完成。

注意:我知道一旦完成并行 foreach 就会移动,但这是在递归方法中。

我的代码如下:

    public async Task Scrape(string url)
    {
        var page = string.Empty;

        try
        {
            page = await _service.Get(url);

            if (page != string.Empty)
            {
                if (regex.IsMatch(page))
                {

                    Parallel.For(0, regex.Matches(page).Count,
                        index =>
                        {
                            try
                            {
                                if (regex.Matches(page)[index].Groups[1].Value.StartsWith("/"))
                                {
                                    var match = regex.Matches(page)[index].Groups[1].Value.ToLower();
                                    if (!links.Contains(BaseUrl + match) && !Visitedlinks.Contains(BaseUrl + match))
                                    {
                                        Uri ValidUri = WebPageValidator.GetUrl(match);
                                        if (ValidUri != null && HostUrls.Contains(ValidUri.Host))
                                            links.Enqueue(match.Replace(".html", ""));
                                        else
                                            links.Enqueue(BaseUrl + match.Replace(".html", ""));

                                    }
                                }
                            }
                            catch (Exception e)
                            {
                                log.Error("Error occured: " + e.Message);
                                Console.WriteLine("Error occured, check log for further details."); ;
                            }
                        });

                WebPageInternalHandler.SavePage(page, url);
                var context = CustomSynchronizationContext.GetSynchronizationContext();

                Parallel.ForEach(links, new ParallelOptions { MaxDegreeOfParallelism = 25 },
                    webpage =>
                    {
                        try
                        {
                            if (WebPageValidator.ValidUrl(webpage))
                            {
                                string linkToProcess = webpage;
                                if (links.TryDequeue(out linkToProcess) && !Visitedlinks.Contains(linkToProcess))
                                {

                                        ShowPercentProgress();
                                        Thread.Sleep(15);
                                        Visitedlinks.Enqueue(linkToProcess);
                                        Task d = Scrape(linkToProcess);
                                        Console.Clear();


                                }
                            }
                        }
                        catch (Exception e)
                        {
                            log.Error("Error occured: " + e.Message);
                            Console.WriteLine("Error occured, check log for further details.");
                        }
                    });

                Console.WriteLine("parallel finished");
            }
        }

        catch (Exception e)
        {
            log.Error("Error occured: " + e.Message);
            Console.WriteLine("Error occured, check log for further details.");
        }

    }

注意Scrape多次调用(递归)

像这样调用方法:

    public Task ExecuteScrape()
    {
        var context = CustomSynchronizationContext.GetSynchronizationContext();
        Scrape(BaseUrl).ContinueWith(x => {

            Visitedlinks.Enqueue(BaseUrl);
        }, context).Wait();

        return null;
    }

反过来又被这样调用:

    static void Main(string[] args)
    {
        RunScrapper();
        Console.ReadLine();
    }

    public static void RunScrapper()
    {
        try
        {

            _scrapper.ExecuteScrape();

        }
        catch (Exception e)
        {
            Console.WriteLine(e);
            throw;
        }
    }

我的结果:

在此处输入图像描述

我该如何解决这个问题?

4

2 回答 2

1

(我回答有关网页抓取的问题是否合乎道德?)

不要Scrape递归调用。将要抓取的 url 列表放入 aConcurrentQueue并开始处理该队列。由于抓取页面的过程会返回更多 url,只需将它们添加到同一个队列中即可。

我也不会只使用一个字符串。我建议创建一个类

public class UrlToScrape //because naming things is hard
{        
    public string Url { get; set; }
    public int Depth { get; set; }
}

无论您如何执行此操作,它都是递归的,因此您必须以某种方式跟踪您的深度。网站可能会故意生成将您送入无限递归的 URL。(如果他们这样做了,那么他们不希望您抓取他们的网站。有人希望人们抓取他们的网站吗?)

当您的队列为空时,这并不意味着您已完成。队列可能是空的,但是抓取最后一个出队的 url 的过程仍然可以将更多项目添加回该队列,因此您需要一种方法来解决这个问题。

您可以使用线程安全计数器(intusing Interlocked.Increment/Decrement),当您开始处理 url 时递增,完成时递减。当队列为空并且进程内 url 的计数为零时,您就完成了。

这是一个非常粗略的模型来说明这个概念,而不是我所说的精炼解决方案。例如,您仍然需要考虑异常处理,而我不知道结果去了哪里,等等。

public class UrlScraper
{
    private readonly ConcurrentQueue<UrlToScrape> _queue = new ConcurrentQueue<UrlToScrape>();
    private int _inProcessUrlCounter;
    private readonly List<string> _processedUrls = new List<string>();

    public UrlScraper(IEnumerable<string> urls)
    {
        foreach (var url in urls)
        {
            _queue.Enqueue(new UrlToScrape {Url = url, Depth = 1});
        }
    }

    public void ScrapeUrls()
    {
        while (_queue.TryDequeue(out var dequeuedUrl) || _inProcessUrlCounter > 0)
        {
            if (dequeuedUrl != null)
            {
                // Make sure you don't go more levels deep than you want to.
                if (dequeuedUrl.Depth > 5) continue;
                if (_processedUrls.Contains(dequeuedUrl.Url)) continue;

                _processedUrls.Add(dequeuedUrl.Url);
                Interlocked.Increment(ref _inProcessUrlCounter);
                var url = dequeuedUrl;
                Task.Run(() => ProcessUrl(url));
            }
        }
    }

    private void ProcessUrl(UrlToScrape url)
    {
        try
        {
            // As the process discovers more urls to scrape,
            // pretend that this is one of those new urls.
            var someNewUrl = "http://discovered";
            _queue.Enqueue(new UrlToScrape { Url = someNewUrl, Depth = url.Depth + 1 });
        }
        catch (Exception ex)
        {
            // whatever you want to do with this
        }
        finally
        {
            Interlocked.Decrement(ref _inProcessUrlCounter);
        }
    }
}

如果我真的这样做,该ProcessUrl方法将是它自己的类,它会采用 HTML,而不是 URL。在这种形式下,很难进行单元测试。如果它在一个单独的类中,那么您可以传入 HTML,验证它是否在某处输出结果,并且它调用一个方法来将它找到的新 URL 加入队列。

将队列维护为数据库表也不是一个坏主意。否则,如果您正在处理一堆 url 并且您必须停止,您将重新开始。

于 2018-01-16T18:30:20.963 回答
0

您不能将所有任务添加Task d到您通过所有递归调用(通过方法参数)线程化的某种类型的并发集合中,然后简单地调用Task.WhenAll(tasks).Wait()吗?

您需要一个启动基本Scrape调用并传入空任务集合的中间方法(使其更清晰)。当基本调用返回时,您手头有所有任务,您只需等待它们完成。

public async Task Scrape (
    string url) {
    var tasks = new ConcurrentQueue<Task>();

    //call your implementation but
    //change it so that you add
    //all launched tasks d to tasks
    Scrape(url, tasks);
    
    //1st option: Wait().
    //This will block caller
    //until all tasks finish 
    Task.WhenAll(tasks).Wait(); 

    
    //or 2nd option: await 
    //this won't block and will return to caller.
    //Once all tasks are finished method
    //will resume in WriteLine
    await Task.WhenAll(tasks);
    Console.WriteLine("Finished!"); }

简单的规则:如果你想知道某件事什么时候结束,第一步就是跟踪它。在您当前的实现中,您实际上是在触发并忘记所有已启动的任务......

于 2018-01-16T17:25:10.757 回答