Парсинг сайта на котором при входе загрузка
Мне необходимо спарсить сайт, но когда заходишь на него, появляется загрузка и из-за этого я получаю только сам загрузочный экран сайта. Как это обойти? Может какой нибудь sleep?
Пробовал до и после запроса ставить Thread.Sleep(), ещё пробовал библиотеку AngleSharp, через async метод, но это тоже не помогло.
Делал на AngleSharp
var address = "https://www.rustreaper.com/";
var document = await BrowsingContext.New(config).OpenAsync(address);
string text = document.QuerySelector("div").Text();```
И на обычном HttpWebRequest
var request = (HttpWebRequest)WebRequest.Create("https://www.rustreaper.com");
var response = (HttpWebResponse)request.GetResponse();
request.UserAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36";
if (response.StatusCode == HttpStatusCode.OK)
{
Thread.Sleep(2000);
var receiveStream = response.GetResponseStream();
if (receiveStream != null)
{
StreamReader readStream;
if (response.CharacterSet == null)
readStream = new StreamReader(receiveStream);
else
readStream = new StreamReader(receiveStream, Encoding.GetEncoding(response.CharacterSet));
result = readStream.ReadToEnd();
readStream.Close();
}
response.Close();
}
Но ничего из этого не помогло.
Ответы (1 шт):
Для вашей задачи не нужны парсеры, ведь если мы заглянем в "Средства разработчика" (F12 в браузере), то увидим там следующую картину
Заметили один постоянно выполняющийся запрос? Вот он и содержит в себе все данные, ведь это WebSocket, к которому нам достаточно лишь подключиться и запросить всю необходимую информацию.
В C# для таких целей есть специальный класс, зовется ClientWebSocket. Я однажды уже давал ответ на подобный вопрос, можно увидеть его здесь, сейчас я на его основе постараюсь реализовать клиент именно для указанного сайта, где конечной целью будет чтение сообщений с чата.
И так, приступим:
Создадим пустой класс, назовем его к примеру
RUSTreaperClientи унаследуем отIDisposable, ведь мы делаем некую обертку надClientWebSocket, а он должен быть закрыт.Создадим внутри приватное поле
ClientWebSocketи реализуем методDispose.Получим в итоге следующее:
public class RUSTreaperClient : IDisposable { private readonly ClientWebSocket client = new(); public void Dispose() => client.Dispose(); }Теперь давайте реализуем 3 метода:
Соединения:
public async Task ConnectAsync(CancellationToken cancellationToken = default) { await client.ConnectAsync(uri, cancellationToken); }Отправки запроса:
public Task SendAsync(string message, CancellationToken cancellationToken = default) { if (!string.IsNullOrEmpty(message)) { ArraySegment<byte> arraySegment = new(Encoding.UTF8.GetBytes(message)); return client.SendAsync(arraySegment, WebSocketMessageType.Text, true, cancellationToken); } return Task.CompletedTask; }Чтение ответа:
public async Task<string> ReadAsync(CancellationToken cancellationToken = default) { if (client is { State: WebSocketState.Open }) { ArraySegment<byte> bytesReceived = new ArraySegment<byte>(new byte[1024]); WebSocketReceiveResult result = await client.ReceiveAsync(bytesReceived, cancellationToken); return Encoding.UTF8.GetString(bytesReceived.Array, 0, result.Count); } return "Closed"; }
Имея это, давайте попробуем подключиться
static async Task Main() { using var client = new RUSTreaperClient(); await client.ConnectAsync(); Console.WriteLine(await client.ReadAsync()); }Но получаем вдруг ошибку
System.Net.WebSockets.WebSocketException: "The server returned status code '400' when status code '101' was expected."
400 - это Bad Request, то есть нашему запросу не хватает неких данных, обычно это заголовки. Посмотрим опять в браузере на подключение и пытаемся методом подбора подставить заголовок. Находим, что сервер требует обязательно
Origin, ну так давайте его добавим нашему клиенту в конструкторе класса:public RUSTreaperClient() { client.Options.SetRequestHeader("Origin", "https://www.rustreaper.com"); }Пробуем повторно соединится и видим ответ от сервера, который имеет вид
0{"sid":"HU29rYgkIM8s1Ss5BmLY","upgrades":[],"pingInterval":25000,"pingTimeout":5000}На вид простой JSON (с мусором), в котором нас интересует
pingInterval, ведь это значение, в течении которого сервер ждет от нас пинг запрос.Давайте приведем ответ в удобный для нас вид:
Создадим класс, я назову его
Session, пусть содержит id и нужный timeout:public class Session { [JsonPropertyName("sid")] public string Id { get; set; } [JsonPropertyName("pingInterval")] public int Timeout { get; set; } }Далее допишем метод подключения, пусть дополнительно десериализует первый ответ сервера и заносит это в приватное поле класса:
public async Task ConnectAsync(CancellationToken cancellationToken = default) { await client.ConnectAsync(uri, cancellationToken); var response = await ReadAsync(cancellationToken); session = JsonSerializer.Deserialize<Session>(response.Substring(1)); Console.WriteLine($"Сессия {session.Id}, Ping/Pong раз в {session.Timeout}ms."); }
Отлично, получили данные, десериализовали их, осталось дело за малым, а именно начать непрерывное чтение данных, а также Ping запрос отправлять.
Чтение данных фоном. Создадим еще одну асинхронную задачу, пусть в цикле бесконечно вызывает метод чтения ответа:
private async Task ReadMessages(CancellationToken cancellationToken = default) { while (!cancellationToken.IsCancellationRequested) { var message = await ReadAsync(cancellationToken); Console.WriteLine(message); } }Ping запрос. Цель - отправить сообщение с цифрой
2раз в N сек, где N - значение с сервера.private async Task PingMessage(CancellationToken cancellationToken = default) { while (!cancellationToken.IsCancellationRequested) { await Task.Delay(session.Timeout); await SendAsync("2", cancellationToken); } }Допишем метод соединения, добавив в конец запуск этих двух задач:
var pingMessage = PingMessage(cancellationToken); var readMessages = ReadMessages(cancellationToken); await Task.WhenAll(new[] { pingMessage, readMessages });
На данном этапе, если попытаться подключиться, то мы увидим нечто такое:
Сессия -5iYNPHK-NmTzGn_BmMQ, Ping/Pong раз в 25000сек.
40
3
3Это означает, что соединение есть, оно стабильно, сервер ждет указаний.
Осталось нам подключиться к чату. Для этого сервер просит отправить 2 запроса:
40/chat- цифра, это то, что отправляет нам сервер в последующих ответах после инициализации, можно (и наверно нужно) брать от туда, но я лично не стал, ибо она всегда 40. Ну аchat- это некая команда инициализации.42/chat,12["join",["english","russian"]]-42- эта цифра всегда больше цифры "инициализации" на 2,12- это скажем так "вкладка чата" (начинается с0),join- команда подключения, ну а языки - это вроде как какие языки слушать.
Стоит учесть тот факт, что между первой и 2-й командой сервер ожидает задержу.
В итоге у нас получается новая задача:private async Task JoinToChat(string[] languages, CancellationToken cancellationToken = default) { var data = JsonSerializer.Serialize(new object[] { "join", languages }); await SendAsync("40/chat", cancellationToken); await Task.Delay(1000); await SendAsync($"42/chat,0{data}"); }Я как видите, не стал тут мудрить, делать каналы и ожидать ответа от сервера (задержка), это оставлю на вас. Не забываем вызвать этот
Taskв том же методе подключенияawait JoinToChat(new[] { "english", "russian" }, cancellationToken);
Собственно, вот и все, сервер нам успешно отдает данные чата. Если нужна еще информация, то отправляем ему аналогичные запросы (например 40/general). Весь код получается следующий:
public class Session
{
[JsonPropertyName("sid")]
public string Id { get; set; }
[JsonPropertyName("pingInterval")]
public int Timeout { get; set; }
}
public class RUSTreaperClient : IDisposable
{
private readonly Uri uri = new Uri("wss://www.rustreaper.com/socket.io/?EIO=3&transport=websocket");
private readonly ClientWebSocket client = new();
private Session session;
public RUSTreaperClient()
{
client.Options.SetRequestHeader("Origin", "https://www.rustreaper.com");
}
public async Task ConnectAsync(CancellationToken cancellationToken = default)
{
await client.ConnectAsync(uri, cancellationToken);
var response = await ReadAsync(cancellationToken);
session = JsonSerializer.Deserialize<Session>(response.Substring(1));
Console.WriteLine($"Сессия {session.Id}, Ping/Pong раз в {session.Timeout}сек.");
await JoinToChat(new[] { "english", "russian" }, cancellationToken);
var pingMessage = PingMessage(cancellationToken);
var readMessages = ReadMessages(cancellationToken);
await Task.WhenAll(new[] { pingMessage, readMessages });
}
public Task SendAsync(string message, CancellationToken cancellationToken = default)
{
if (!string.IsNullOrEmpty(message))
{
ArraySegment<byte> arraySegment = new(Encoding.UTF8.GetBytes(message));
return client.SendAsync(arraySegment, WebSocketMessageType.Text, true, cancellationToken);
}
return Task.CompletedTask;
}
public async Task<string> ReadAsync(CancellationToken cancellationToken = default)
{
if (client is { State: WebSocketState.Open })
{
ArraySegment<byte> bytesReceived = new ArraySegment<byte>(new byte[1024]);
WebSocketReceiveResult result = await client.ReceiveAsync(bytesReceived, cancellationToken);
return Encoding.UTF8.GetString(bytesReceived.Array, 0, result.Count);
}
return "Closed";
}
private async Task ReadMessages(CancellationToken cancellationToken = default)
{
while (!cancellationToken.IsCancellationRequested)
{
var message = await ReadAsync(cancellationToken);
Console.WriteLine(message);
}
}
private async Task PingMessage(CancellationToken cancellationToken = default)
{
while (!cancellationToken.IsCancellationRequested)
{
await Task.Delay(session.Timeout);
await SendAsync("2", cancellationToken);
}
}
private async Task JoinToChat(string[] languages, CancellationToken cancellationToken = default)
{
var data = JsonSerializer.Serialize(new object[] { "join", languages });
await SendAsync("40/chat", cancellationToken);
await Task.Delay(1000);
await SendAsync($"42/chat,0{data}");
}
public void Dispose() => client.Dispose();
}
class Program
{
static async Task Main()
{
using var client = new RUSTreaperClient();
await client.ConnectAsync();
}
}
Результат:
Что поправить:
- Как я уже писал ранее, первое число 40, приходит ответом от сервера, стоит его также как и интервал взять и использовать.
- Ответ от сервера может быть в разы больше, чем указанный буфер в размере
1024. - Если соединение закрывается, то идет спам в консоль (стоит дописать в условие циклов есть ли соединение или нет).
- Ping/Pong - не уверен, что всегда 2 и 3, стоит проверить и сделать проверку ответа от сервера.
- Полученные JSON значения десериализировать в классы. Но там правда каша, сервер присылает
object[], что не есть хорошо, ибо придется писать конвертор. - Метод
ConnectAsyncвыполняет не свои обязанности, стоит вынести инициализацию и прочее в другие методы. - В некоторые асинхронные методы не закинул
CancellationToken(например вTask.Delay. - Ну и другие мелочи... Оставлю все это для вас)


