Парсинг сайта на котором при входе загрузка

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

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

Для вашей задачи не нужны парсеры, ведь если мы заглянем в "Средства разработчика" (F12 в браузере), то увидим там следующую картину

Browser data

Заметили один постоянно выполняющийся запрос? Вот он и содержит в себе все данные, ведь это WebSocket, к которому нам достаточно лишь подключиться и запросить всю необходимую информацию.


В C# для таких целей есть специальный класс, зовется ClientWebSocket. Я однажды уже давал ответ на подобный вопрос, можно увидеть его здесь, сейчас я на его основе постараюсь реализовать клиент именно для указанного сайта, где конечной целью будет чтение сообщений с чата.

И так, приступим:

  1. Создадим пустой класс, назовем его к примеру RUSTreaperClient и унаследуем от IDisposable, ведь мы делаем некую обертку над ClientWebSocket, а он должен быть закрыт.

  2. Создадим внутри приватное поле ClientWebSocket и реализуем метод Dispose.

    Получим в итоге следующее:

    public class RUSTreaperClient : IDisposable
    {
        private readonly ClientWebSocket client = new();
        public void Dispose() => client.Dispose();
    }
    
  3. Теперь давайте реализуем 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";
      }
      
  4. Имея это, давайте попробуем подключиться

    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, ведь это значение, в течении которого сервер ждет от нас пинг запрос.

  5. Давайте приведем ответ в удобный для нас вид:

    • Создадим класс, я назову его 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.");
      }
      
  6. Отлично, получили данные, десериализовали их, осталось дело за малым, а именно начать непрерывное чтение данных, а также 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

    Это означает, что соединение есть, оно стабильно, сервер ждет указаний.

  7. Осталось нам подключиться к чату. Для этого сервер просит отправить 2 запроса:

    1. 40/chat - цифра, это то, что отправляет нам сервер в последующих ответах после инициализации, можно (и наверно нужно) брать от туда, но я лично не стал, ибо она всегда 40. Ну а chat - это некая команда инициализации.

    2. 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();
    }
}

Результат:

Result

Что поправить:

  • Как я уже писал ранее, первое число 40, приходит ответом от сервера, стоит его также как и интервал взять и использовать.
  • Ответ от сервера может быть в разы больше, чем указанный буфер в размере 1024.
  • Если соединение закрывается, то идет спам в консоль (стоит дописать в условие циклов есть ли соединение или нет).
  • Ping/Pong - не уверен, что всегда 2 и 3, стоит проверить и сделать проверку ответа от сервера.
  • Полученные JSON значения десериализировать в классы. Но там правда каша, сервер присылает object[], что не есть хорошо, ибо придется писать конвертор.
  • Метод ConnectAsync выполняет не свои обязанности, стоит вынести инициализацию и прочее в другие методы.
  • В некоторые асинхронные методы не закинул CancellationToken (например в Task.Delay.
  • Ну и другие мелочи... Оставлю все это для вас)
→ Ссылка