Ситуация такая.
Пишу парсер, который натравливаю на сайт 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: Код метода, где вся эта радость вызывается, не поместился в тексте. Привожу его в комментарии.