C# GetStringAsync возвращает "неправильный" html

Хочу спарсить сайт магазина "перекресток".

https://www.perekrestok.ru/cat/c/104/rastitelnoe-maslo

Написал код, все работает, получаю html, через регулярки делаю что мне надо. Проходит неделя и код работать перестает. Вышибает исключение 403. Добавил хедер (притворился браузером), опять начало возвращать html но не тот, который надо. Раньше получал большой текст примерно на 1MB такой же как если в браузере нажать ПКМ, посмотреть код страницы. А теперь получаю мелкий в 22kB, в котором никаких товаров, их цен и т.п. нет. Через браузер все работало так и работает. Я бы понял, если сайт подумал что спамлю, но я парсил 15 ссылок раз в 2 дня. Не знаю куда приложить пример "неправильного" html. Копипастить сюда будет негуманно

https://dropmefiles.com/N9fRZ (22кБ текстовик с html внутри)

Нужно получить такой же html как если бы прошел по ссылке, нажал ПКМ и нажал "посмотреть код страницы" или из которого можно было бы вытащить товары, цены и т.д.

var client = new HttpClient();
client.DefaultRequestHeaders.Add("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36");
string html = await client.GetStringAsync("https://www.perekrestok.ru/cat/c/104/rastitelnoe-maslo");```


Ответы (1 шт):

Автор решения: EvgeniyZ

На ответ "почему работало, а потом нет" вам ответят только авторы данного сайта, ибо нам не знать, что там было и что изменили, но зато мы можем сказать что там сейчас. Ну а сейчас там следующее:

Если очень внимательно посмотреть как ведет себя сайт, то заметим следующую картину

Requests

Видите 2 запроса с пометкой HTML? Вот собственно это и делает сайт "под капотом". При первом его посещении, вы получаете страницу "заглушку", на которой визуально крутится индикатор обраборки данных, а "под капотом" генерируется 2 уникальных ключа, которые устанавливаются в Cookie браузера. Собственно на первом запросе вы сейчас и застряли, ибо всякие HttpClient - это сухая работа с запросами, они за вас JS скрипт не выполнят (как делает это браузер), эту работу вы должны брать на себя.

Собственно, как поступить?

Варианта 3.

  1. Разобрать всю логику генерации ключей и повторить тоже самое на языке C#.
  2. Выполнить JS скрипт на языке C# (за такое отвечают библиотеки по типу Jint, но учтите, часть логики вынесена в отдельную, стороннюю библиотеку (на скрине запросов она под # 15))
  3. Использовать полноценный браузер, который сделает все за нас (WebView2, Selenium, и др.).

Я лично пойду первым путем, ибо считаю его менее затратным по ресурсам и более надежным, да и в данном примере там нет ничего особо сложного.

Генерируем нужные Cookie вручную

Анализируем сайт

Сперва, отправим запрос на сайт программно:

private static readonly HttpClient client = new();

static async Task Main(string[] args)
{
    var url = "https://www.perekrestok.ru/cat/c/104/rastitelnoe-maslo";
    var html = await client.GetStringAsync(url);
}

И... Получаем 403 ошибку. Почему? Все дело в том, что сайт проверяет еще и UserAgent, если он не задан, то сайт сразу перестает нас пускать. Собственно, добавляем как сделали вы (client.DefaultRequestHeaders.Add(...)) и пробуем по новой. Теперь сайт дает нам нужные данные, смотрим на них и видим там сделующее:

function get_cookie_spsn() {
  return "spsn=1733103360750_";
}
function get_cookie_spid() {
  return "spid=1733103360750_2ad559e121cac55bda82f87420944787_sn1qo9g9rhfn5l0n";
}
function get_cookie_spsc_encrypted_part() {
  let func = function () {/*-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDCUJbXKTolYbHG5pFEq/iL8kY603vI7M8D3hTfi7CE9mQ20fzs
z0qKfhl0K3zpQuGPX9vdUbtnfpI/TuzWH6acmq4lL9yCP8Wbx+ONaOMdChvb39+c
jRPW1W+k6GopuQCOtcnLx9OzJIQG29bnF0HVQL2JgdKiAAASeay7hDsO7QIDAQAB
AoGBAISojDJcPQwkREBsTKS7WzX/sx6aHxovQa18QnfTYDGGHSin96qcYmFmcW4z
+lUtidxeLzZLhEvFx4ZdFaeheBajczGA4MDIMJl2siuzudVEG3+sTfwPuKAsYHAz
DL7HjYUnJuxPYBC5J6E8+aaiid8/0UdvgvCwmn8odm3H5YMpAkEA+2QI7yqm9Q1I
8ghmwfXO+TO8MYHJ/4MFH507oSRIkLDMQ6uSNFrs/TwNAblMB5SRHwj9ddJ6f+jq
MgM3ZTExMwJBAMXgp3Mc43fvtigUMalDmvRZ4jszxBQrmmAwA80nuigdhOBi17AI
Rga78OZa4DZo55Fg1N45k3ioK1ddZ50o/18CQHmliZE6IXpZSGAeYqMe8F20hC+s
r3OeEf+fVTh/10F03BMu1dvR1/YedejMopbUdHkBH61BAZgdvB4hYk/sQvMCQFXl
lKrqsm+g9kDlqz0f5McHsaYjbY2X8/anQS8wfKXnUoQZRCndHZDUytkkP8o+ta8t
CprBAZxR3CabnFvjrR8CQQDVXv4jmj5R6N8qw8DmoCS7tOpjWNBCgvAHcPLZCHDw
Y+yj975UBfWAgeZ8uQEplmW+zCKRupAKA52lJHBO/Irx
-----END RSA PRIVATE KEY-----
*/};
  let pem = func.toString().match(/[^]*\/\*([^]*)\*\/\}$/)[1];
  return KJUR.crypto.Cipher.decrypt("3a5dc07e2df1c2a51c24cb28d378e6bc2c7bc77108e4a00c7780fc6258dca6d5e2f61c7a1230f1d98a85e5a45044872ce9d1c77e228e51a0116a2f4560d7182ae0cdc2588ef16417aea865a70eb5cb2335c72b34b371ffbb08019d2bfb641d382f3853413c530a0e5940a0bfae9ce911101fbd592529e4d4d1e9c2a571c684c4", KEYUTIL.getKey(pem));
}
function get_cookie_spsc_uncrypted_part() {
  return "";
}
function get_cookie_spsc() {
  const ret = get_cookie_spsc_uncrypted_part() + get_cookie_spsc_encrypted_part();
  return "spsc=" + ret;
}

Из этой всей портянки нам нужно то, что отвечает за spid и spsc. Первый валяется в открытом доступе, нам надо будет его просто забрать, а вот второй генерируется при помощи двух методов get_cookie_spsc_uncrypted_part() + get_cookie_spsc_encrypted_part().

Смотрим эти методы:

  • get_cookie_spsc_uncrypted_part - там пустота (может что и появится в скором времени).
  • get_cookie_spsc_encrypted_part - а вот тут интересней, есть приватный ключ, лежащий в переменной pem, а также есть расшифровка некой длинной строки при помощи KJUR.crypto.Cipher.decrypt(...);. И тут, если немного пошерстить интернет, мы понимаем, что неким RSA ключом закодирована некая строка, а для повторения этих действий, нам нужен сам ключ и соответсвенно закодированная строка.

Собираем данные программно

С анализом закончили, теперь давайте получим все это. На данном этапе у нас есть HTML из которого надо выдрать нужное, вопрос как? Самый простой способ - Regex, ну а если разбираете HTML, то у вас скорей всего есть спец. библиотеки для этого способа. Я для простоты буду использовать голый Regex. Дописываю в код выше такое:

var spid = Regex.Match(html, @"spid=(.*)"";").Groups[1].Value;
var pemKey = Regex.Match(html, "-----BEGIN RSA PRIVATE KEY-----[\\s\\S]*?-----END RSA PRIVATE KEY-----").Value;
var encryptedDataHex = Regex.Match(html, @"KJUR.crypto.Cipher.decrypt\(""(.*)""").Groups[1].Value;

Замечу кстати, сайт не всегда дает ключ у которого начало BEGIN RSA PRIVATE KEY, иногда он отдает без RSA (BEGIN PRIVATE KEY). Я это учитывать тут не стал, но вам стоит.

Данный код должен в итоге "выдрать" из HTML сайта все нужные нам данные. Осталось дело за малым.

Расшифровка и генерация Cookie

Ну тут все очень просто.

  • Создаем RSA - using var rsa = RSA.Create();
  • Импортируем в него Pem ключ - rsa.ImportFromPem(pemKey);
  • Конвертируем hex строку в массив байт - var encryptedData = Convert.FromHexString(encryptedDataHex);
  • Расшифровываем - var decryptedData = rsa.Decrypt(encryptedData, RSAEncryptionPadding.Pkcs1);
  • Формируем из полученного массива байт строку - var spsc = Encoding.UTF8.GetString(decryptedData);

Весь код:

using var rsa = RSA.Create();
rsa.ImportFromPem(pemKey);
var encryptedData = Convert.FromHexString(encryptedDataHex);
var decryptedData = rsa.Decrypt(encryptedData, RSAEncryptionPadding.Pkcs1);
var spsc = Encoding.UTF8.GetString(decryptedData);

Отправляем запрос повторно

Нужные Cookie мы успешно получили, осталось отправить их в новом запросе:

client.DefaultRequestHeaders.Add("Cookie", $"spid={spid}; spsc={spsc}");
html = await client.GetStringAsync(url);

Иии.. Сайт успшно отдал данные, поздравляю!

Весь код получается такой:

private static readonly HttpClient client = new();

static async Task Main(string[] args)
{
    client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36");

    var url = "https://www.perekrestok.ru/cat/c/104/rastitelnoe-maslo";
    var html = await client.GetStringAsync(url);

    var spid = Regex.Match(html, @"spid=(.*)"";").Groups[1].Value;
    var pemKey = Regex.Match(html, "-----BEGIN RSA PRIVATE KEY-----[\\s\\S]*?-----END RSA PRIVATE KEY-----").Value;
    var encryptedDataHex = Regex.Match(html, @"KJUR.crypto.Cipher.decrypt\(""(.*)""").Groups[1].Value.Trim();

    using var rsa = RSA.Create();
    rsa.ImportFromPem(pemKey);
    var encryptedData = Convert.FromHexString(encryptedDataHex);
    var decryptedData = rsa.Decrypt(encryptedData, RSAEncryptionPadding.Pkcs1);
    var spsc = Encoding.UTF8.GetString(decryptedData);

    client.DefaultRequestHeaders.Add("Cookie", $"spid={spid}; spsc={spsc}");
    html = await client.GetStringAsync(url);
}

Пример работы: dotnetfiddle

→ Ссылка