1

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

Но ничего из этого не помогло.

xkrystalll
  • 55
  • 2
  • 9
  • Причин может быть множество. А решения могут быть разными. Может быть и слип. Почему бы не попробовать а потом приходить с вопросом? – Andrew Stop_RU_war_in_UA Sep 17 '21 at 14:13
  • В том то и дело, что я и слипы попробовал и всё что пришло в голову. Но ничего не помогло, поэтому и написал. – xkrystalll Sep 17 '21 at 14:38
  • 1
    Ну тогда я не вижу в вопросе перечисления что попробовал и какой результат каждого из путей. – Andrew Stop_RU_war_in_UA Sep 17 '21 at 14:41
  • Представьте, что вы подход к совершенно незнакомому человеку на улице, вот прям к первому встречному и говорите "Мне необходимо спарсить сайт, но получаю только загрузочный экран, как быть?". Какова будет его реакция с таким набором данных? Скорей всего он просто пошлет вас в "мягкой форме". Вот, собственно, а как мы должны на это реагировать? Сделайте минимальный пример, покажите сайт, тогда можно будет поговорить, а сейчас, ну не реально это нам решить. – EvgeniyZ Sep 17 '21 at 14:59
  • В любом браузере зайдите на этот сайт и откройте исходный код страницы. Кода там очень мало, данных нет. Потому что они подгружаются javascript'ом. Значит, вам нужно организовать загрузку самостоятельно. – Alexander Petrov Sep 17 '21 at 15:15
  • https://ru.stackoverflow.com/q/420354/184217 - посмотрите варианты здесь. Обратите внимание на CefSharp и Selenium. – Alexander Petrov Sep 17 '21 at 15:15
  • С оф сайта AngleSharp ссылка ведёт сюда: AngleSharp and JavaScript - разбирайтесь, как использовать JS – Alexander Petrov Sep 17 '21 at 15:19
  • Alexander, мне не обязательно использовать AngleSharp, мне нужно любым способом спарсить чат на сайте. – xkrystalll Sep 17 '21 at 15:22
  • Зачем вам вообще тут AngleSharp, Selenium и прочее, зачем вообще HTML. Научитесь анализировать тот ресурс, с которым вы работаете. На сайте используются соккеты, а значит и все данные там (например, чат), достаточно подключиться... – EvgeniyZ Sep 17 '21 at 15:42
  • EvgeniyZ, не подскажите, пожалуйста, как подключится? Я не совсем понимаю просто. Извините, что дёргаю так. – xkrystalll Sep 17 '21 at 16:08
  • Если хотите обратиться через ник, то не забывайте про @ (например @xkrystalll). Про клиент соккетов я давненько писать этот ответ. – EvgeniyZ Sep 17 '21 at 16:11
  • @EvgeniyZ, при подключении и использовании метода ReceiveAsync(), получается код 400. – xkrystalll Sep 17 '21 at 16:23
  • 2
    @xkrystalll Посмотрите внимательней на оригинал, а именно на заголовки (можете даже почитать это, только там про простые запросы, а не соккеты, но суть аналогична) и найдите тот, который требует сервер. Как найдете, установите его (ws.Options.SetRequestHeader() метод). Я вам даже подсказку дам: Origin. – EvgeniyZ Sep 17 '21 at 16:38
  • @EvgeniyZ, спасибо за помощь, почти докопался до решения. Но после 30 секунд соединение обрывается. Данные всё же приходят, session id и код инициализации сайта (40) – xkrystalll Sep 17 '21 at 19:54
  • https://ru.stackoverflow.com/q/1178026/373567 вдруг пригодится. – aepot Sep 17 '21 at 22:33

1 Answers1

5

Для вашей задачи не нужны парсеры, ведь если мы заглянем в "Средства разработчика" (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 &quot;Closed&quot;;
      

      }

  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(&quot;pingInterval&quot;)]
      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(&quot;pingInterval&quot;)]
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(&quot;Origin&quot;, &quot;https://www.rustreaper.com&quot;);
}

public async Task ConnectAsync(CancellationToken cancellationToken = default)
{
    await client.ConnectAsync(uri, cancellationToken);
    var response = await ReadAsync(cancellationToken);
    session = JsonSerializer.Deserialize&lt;Session&gt;(response.Substring(1));
    Console.WriteLine($&quot;Сессия {session.Id}, Ping/Pong раз в {session.Timeout}сек.&quot;);

    await JoinToChat(new[] { &quot;english&quot;, &quot;russian&quot; }, 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&lt;byte&gt; arraySegment = new(Encoding.UTF8.GetBytes(message));
        return client.SendAsync(arraySegment, WebSocketMessageType.Text, true, cancellationToken);
    }

    return Task.CompletedTask;
}

public async Task&lt;string&gt; ReadAsync(CancellationToken cancellationToken = default)
{
    if (client is { State: WebSocketState.Open })
    {
        ArraySegment&lt;byte&gt; bytesReceived = new ArraySegment&lt;byte&gt;(new byte[1024]);
        WebSocketReceiveResult result = await client.ReceiveAsync(bytesReceived, cancellationToken);
        return Encoding.UTF8.GetString(bytesReceived.Array, 0, result.Count);
    }

    return &quot;Closed&quot;;
}

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(&quot;2&quot;, cancellationToken);
    }
}

private async Task JoinToChat(string[] languages, CancellationToken cancellationToken = default)
{
    var data = JsonSerializer.Serialize(new object[] { &quot;join&quot;, languages });
    await SendAsync(&quot;40/chat&quot;, cancellationToken);
    await Task.Delay(1000);
    await SendAsync($&quot;42/chat,0{data}&quot;);
}

public void Dispose() =&gt; 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.
  • Ну и другие мелочи... Оставлю все это для вас)
EvgeniyZ
  • 15,694