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 шт):
На ответ "почему работало, а потом нет" вам ответят только авторы данного сайта, ибо нам не знать, что там было и что изменили, но зато мы можем сказать что там сейчас. Ну а сейчас там следующее:
Если очень внимательно посмотреть как ведет себя сайт, то заметим следующую картину
Видите 2 запроса с пометкой HTML? Вот собственно это и делает сайт "под капотом". При первом его посещении, вы получаете страницу "заглушку", на которой визуально крутится индикатор обраборки данных, а "под капотом" генерируется 2 уникальных ключа, которые устанавливаются в Cookie браузера. Собственно на первом запросе вы сейчас и застряли, ибо всякие HttpClient - это сухая работа с запросами, они за вас JS скрипт не выполнят (как делает это браузер), эту работу вы должны брать на себя.
Собственно, как поступить?
Варианта 3.
- Разобрать всю логику генерации ключей и повторить тоже самое на языке C#.
- Выполнить JS скрипт на языке C# (за такое отвечают библиотеки по типу
Jint, но учтите, часть логики вынесена в отдельную, стороннюю библиотеку (на скрине запросов она под # 15)) - Использовать полноценный браузер, который сделает все за нас (
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
