Задать вопрос
@skosterin88

Почему парсер работает так странно при распараллеливании?

Ситуация такая.
Пишу парсер, который натравливаю на сайт decathlon.ru (конкретно - на раздел "Виды спорта"). Суть парсера простая - обходим адреса из заданного списка (это каталоги товаров), в каталогах считываем, к какому виду спорта они отнесены, какая категория и подкатегория товаров представлены, берем ссылки на сами товары и парсим страницу каждого товара, выдергивая оттуда его название и цену.
Непосредственно сам парсинг выполняю с помощью библиотеки AngleSharp.

Привожу код метода, где обходится отдельный каталог товаров:
private static readonly IConfiguration _config = Configuration.Default.WithDefaultLoader();

        static async Task<IList<Product>> ParseProductsCatalogPageAsync(string catalogUrl, string filenameOutput,
            bool isAppend)
        {
            var products = new List<Product>();


            var document = await BrowsingContext.New(_config).OpenAsync(catalogUrl);

            string selBreadcrumbText = ".m-breadcrumbs ul[data-breadcrumb-size='5'] li a span.text";
            var breadcrumbElements = document.QuerySelectorAll(selBreadcrumbText).ToArray();

            string meansOfSport = breadcrumbElements[0].TextContent;
            string productsCategory = breadcrumbElements[1].TextContent;

            string selProductsSubcategory = "span.link_breadcrumb.link_breadcrumb_no_child_last";
            var elemProductsSubcategory = document.QuerySelector(selProductsSubcategory);
            string productsSubcategory = elemProductsSubcategory.TextContent.Trim();

            int qProducts = Convert.ToInt32(document.QuerySelector("span.nb-products").TextContent);

            if (qProducts > 0)
            {
                int numPage = 1;
                int id = 1;
                string rootLink = "https://www.decathlon.ru";

                if (qProducts > 40)
                {
                    while (true)
                    {
                        document =
                            await BrowsingContext.New(_config)
                                .OpenAsync(catalogUrl + "/I-Page" + numPage.ToString() + "_40");
                        string selProductLink = "a.thumbnail-link";

                        var productElements = document.QuerySelectorAll(selProductLink).AsEnumerable();

                        var currDocument = document;

                        Parallel.ForEach(productElements, (elem) =>
                        {
                            string link = rootLink + elem.GetAttribute("href");
                            var tskProduct = ParseProductPageAsync(link);
                            var product = tskProduct.Result;
                            product.Id = id;
                            product.SportsName = meansOfSport;
                            product.Category = productsCategory;
                            product.Subcategory = productsSubcategory;

                            id++;

                            if (Math.Abs(product.Price) < 0.001)
                            {
                                string selProductPrice = selProductLink + "[href='" + elem.GetAttribute("href") +
                                                         "'] " +
                                                         "div.zone-price-selling-price div.price";

                                string trimmedMinPriceString = currDocument.QuerySelector(selProductPrice)
                                    .TextContent
                                    .Trim()
                                    .Replace(" ", "").Replace(" ", "");
                                string minPriceString = trimmedMinPriceString.Substring(0,
                                    trimmedMinPriceString.Length - "руб.".Length).Replace(" ", "");
                                product.Price = Convert.ToDouble(minPriceString);
                            }

                            products.Add(product);
                        });

                        if (products.Count == qProducts)
                        {
                            break;
                        }
                        numPage++;
                    }
                }
                else
                {
                    string selProductLink = "a.thumbnail-link";

                    var productElements = document.QuerySelectorAll(selProductLink).AsEnumerable();

                    var currDocument = document;

                    Parallel.ForEach(productElements, (elem) =>
                    {
                        string link = rootLink + elem.GetAttribute("href");
                        try
                        {
                            var tskProduct = ParseProductPageAsync(link);
                            var product = tskProduct.Result;
                            product.Id = id;
                            product.SportsName = meansOfSport;
                            product.Category = productsCategory;
                            product.Subcategory = productsSubcategory;

                            id++;

                            if (Math.Abs(product.Price) < 0.001)
                            {
                                string selProductPrice = selProductLink + "[href='" + elem.GetAttribute("href") +
                                                         "'] " +
                                                         "div.zone-price-selling-price div.price";

                                string trimmedMinPriceString = currDocument.QuerySelector(selProductPrice)
                                    .TextContent
                                    .Trim()
                                    .Replace(" ", "").Replace(" ", "");
                                string minPriceString = trimmedMinPriceString.Substring(0,
                                    trimmedMinPriceString.Length - "руб.".Length).Replace(" ", "");
                                product.Price = Convert.ToDouble(minPriceString);
                            }

                            products.Add(product);
                        }
                        catch (Exception)
                        {
                            Debug.WriteLine(link);
                        }
                    });

                }
            }

            SaveProductsDataToFile(filenameOutput, products, isAppend);

            Console.WriteLine("Обработана категория {0} > {1} > {2}", meansOfSport, productsCategory,
                productsSubcategory);
            
            return products;
        }


Код метода, где парсится страница отдельного товара:
static async Task<Product> ParseProductPageAsync(string productUrl)
        {
            var document = await BrowsingContext.New(_config).OpenAsync(productUrl);

            string selArticle = "div.ref-product";
            string selProductName = "span#productName";
            string selPrice = "span#real_price_value";

            var elemArticle = document.QuerySelector(selArticle);
            var elemProductName = document.QuerySelector(selProductName);
            var elemPrice = document.QuerySelector(selPrice);

            string fullArticleString = elemArticle.TextContent.Replace("\n", "").Replace("\t", "");
            string articleStringName = "Артикул : ";

            string fullPriceString = elemPrice == null ? "0руб." : elemPrice.TextContent;
            string priceOnlyString = fullPriceString.Substring(0, fullPriceString.Length - "руб.".Length);

            long article = Convert.ToInt64(fullArticleString.Substring(articleStringName.Length));
            string productName = elemProductName.TextContent;
            double price = Convert.ToDouble(priceOnlyString);

            var product = new Product
            {
                Article = article,
                Name = productName,
                Price = price
            };

            return product;
        }


Проблема вот в чем. Если я использую в методе ParseProductsCatalogPageAsync() для обхода каталога обычный цикл foreach (т.е. foreach(var elem in productElements){...}), то программа работает без нареканий. Но когда я использую для этой же цели Parallel.ForEach (т.е. Parallel.ForEach(productElements, (elem) => {...})), то программа на большинстве адресов работает нормально, а некоторые страницы товаров парсить просто отказывается. В этом случае она выводит адреса этих страниц в Debug. Каждый раз это одни и те же страницы, которые она в случае использования простого foreach парсит без лишних претензий.
Вопрос: что не так с Parallel.ForEach в данной ситуации?

UPD: Код метода, где вся эта радость вызывается, не поместился в тексте. Привожу его в комментарии.
  • Вопрос задан
  • 345 просмотров
Подписаться 1 Оценить 1 комментарий
Пригласить эксперта
Ответы на вопрос 1
Young_khv
@Young_khv
ASP.NET Developer
List is not thread-safe collection, try to use ConcurrentBag instead
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы