Проблемы с gzinflate()

Discussion in 'PHP' started by mamontenok, 19 Apr 2012.

  1. mamontenok

    mamontenok Banned

    Joined:
    17 Jul 2010
    Messages:
    12
    Likes Received:
    1
    Reputations:
    5
    Уже 100500 раз использовал функцию gzinflate() для разжатия страницы полученной от сервера. Всё по стандартной схеме - разжимаем контент, начиная с 11 символа после двойного переноса
    PHP:
    $b=substr($p,strpos($p,"\r\n\r\n")+14);
    $b=gzinflate($b);
    Всё было неплохо, пока функция вдруг не стала выдавать ошибку "data error". Что особенно странно - это происходит не сразу, а через несколько запросов, т.е. сначала нормально разжимает, а потом вдруг начинает ошибки выдавать. Проверял переменные - в $p содержится свежая страница. Вот что это может быть? Пока не могу понять
     
  2. GRRRL Power

    GRRRL Power Elder - Старейшина

    Joined:
    13 Jul 2010
    Messages:
    823
    Likes Received:
    185
    Reputations:
    84
    Я когда-то давно отыскал вот такую функцию:

    PHP:
      function gzBody($gzData)
      {
        if(
    substr($gzData,0,3)=="\x1f\x8b\x08")
        {
          
    $i=10;
          
    $flg=ord(substr($gzData,3,1));
          if(
    $flg>0)
          {
            if(
    $flg&4)
            {
              list(
    $xlen)=unpack('v',substr($gzData,$i,2));
              
    $i=$i+2+$xlen;
            }

            if(
    $flg&8$i=strpos($gzData,"\0",$i)+1;
            if(
    $flg&16$i=strpos($gzData,"\0",$i)+1;
            if(
    $flg&2$i=$i+2;
          }

          return 
    gzinflate(substr($gzData,$i,-8));
        }
        else
          return 
    false;
      }
    Она, по всей видимости, больше соответствует стандартам. Хотя по-хорошему, конечно, прочитать стандарт и сделать полностью так, как там написано.
     
    1 person likes this.
  3. mamontenok

    mamontenok Banned

    Joined:
    17 Jul 2010
    Messages:
    12
    Likes Received:
    1
    Reputations:
    5
    Большое спасибо. А можно ссылку получить на русский вариант, или какую-нибудь статью более менее понятную.
    http://www.faqs.org/rfcs/rfc1952.html
    это же он?
     
    #3 mamontenok, 19 Apr 2012
    Last edited: 19 Apr 2012
  4. GRRRL Power

    GRRRL Power Elder - Старейшина

    Joined:
    13 Jul 2010
    Messages:
    823
    Likes Received:
    185
    Reputations:
    84
  5. mamontenok

    mamontenok Banned

    Joined:
    17 Jul 2010
    Messages:
    12
    Likes Received:
    1
    Reputations:
    5
    GRRRL Power, тебе огромное спасибо. Честно говоря, ни функция (я её потом нашёл на просторах, даже несколько вариантов), ни ссылки особо не помогли. Функция упорно не хотела работать, сколько с ней не бился. Но зато ты направил меня немного.

    Оказывается, страницы, на которых gzinflate() выдавала ошибку приходили с заголовком
    PHP:
    Transfer-Encodingchunked
    . А это означает, что данные будут приходить по частям. Каждой части предшествует маркер, содержащий длину "чунка" в шестнадцатеричном представлении, и перенос строки (\r\n). Таким образом, прежде чем разжимать страницу строчкой
    PHP:
    //---в $page уже содержится страница без заголовков и двойного переноса
    $page=gzinflate(substr($page,10));
    необходимо отсечь маркер и перенос строки
    PHP:
    $page=substr($page,strpos($page,"\r\n")+2);
    $page=gzinflate(substr($page,10));
    При работе с более менее серьёзными сайтами это должно работать, так как они придерживаются наиболее распространённых стандартов. Проблемы могут возникнуть с менее "продвинутыми", или специфичными сайтами. Вот там уже может понадобится более серьёзная функция для обработки разжимаемых данных.

    Ещё могут возникнуть проблемы из-за плохого интернет-соединения - даже без Transfer-Encoding: chunked ответ может приходить частями и это нужно учитывать при работе через сокеты. В этом случае при каждом дополучении перед разжатием нужно собирать воедино уже полученный неразжатый контент и свежий. Для контроля целостности можно использовать заголовок Content-Length, но не все серверы и не всегда его шлют.

    Теоретически, даже с Transfer-Encoding: chunked сами чунки могут приходить не полностью, поэтому может понадобится сбор каждого маркера и проверка длины пришедшего чунка. Но это уже слишком плохое соединение и медленный интернет. В таких условиях лучше не работать.
     
  6. mamontenok

    mamontenok Banned

    Joined:
    17 Jul 2010
    Messages:
    12
    Likes Received:
    1
    Reputations:
    5
    На самом деле чанки могут приходить не целиком даже с не самым медленным интернетом (чанк может быть довольно большим). Проверять длину чанков мне показалось слишком затратно, да и целостность страницы для меня не является необходимым пунктом - главное, чтобы в уже полученном ответе содержались нужные данные.

    Поэтому решил выложить сюда унифицированную функцию, которая вычитывает и обрабатывает страницу в зависимости от заголовков и других нюансов, а также пояснения к ней, так как мне не удалось разобраться с функциями, найденными в интернете, без мануала к ним. Авось кому понадобится

    Способ вычитки предполагает, что из-за интернет соединения ответ может прийти по частям и при этом после каждой части не будет feof. Поэтому после пяти подряд пустых вычиток вылетаем из цикла вычитки и после функции page_combine() можно выполнить какие-то другие действия, или определить стоит ли ещё ждать ответа, или полученных данных уже достаточно и можно двигаться дальше

    Использование этой функции налагает некоторые обязательства - необходимо следить чтобы каждый раз перед обработкой нового ответа очищались переменные $page и $body, а также обнулялся счётчик дополучений страницы перед обработкой нового ответа и, соответственно, увеличивался, когда страница получена не полностью и нужно довычитать. И перед началом новой сессии очищался массив $session.
    PHP:
    function page_combine($s,$c){
            
    //------$s - дескриптор на сокет, из которого нужно вычитывать
            //------$c - счётчик количества попыток дополучить страницу
        
    global $page;//-------переменная с разжатой страницей
        
    global $body;//-------переменная с неразжатым телом ответа
        
    global $session;//-------куки
        //------вычитываем ответ
        
    for($i=0,$p="";!feof($s);$i++){
            
    $a=fgets($s[$k],2048);
                    
    $p.=$a;
            if(!empty(
    $a))$i=0;
            else{
                if(
    $i==4):break;endif;
                }
            }
            
    //------$h - заголовки ответа, $b - тело ответа
            //------разделяем ответ на заголовки и тело
        
    if($c==0){
            
    $h=substr($p,0,strpos($p,"\r\n\r\n"));
            
    $b=substr($p,strpos($p,"\r\n\r\n")+4);
            }
        else{
            
    $h=substr($page,0,strpos($page,"\r\n\r\n"));
            
    $b=$p;
            }
        
    //-------вот здесь самый смак - разжатие страницы в зависимости от условий
        
    if(strpos($h,"Content-Encoding: gzip")!==false){
            if(
    strpos($h,"Transfer-Encoding: chunked")!==false){
                if(
    $c==0)$b=substr($b,strpos($b,"\r\n")+12);
                else{
                    
    $a=ord(substr($b,0,1));
                    
    //------символы маркера длины чанка имеют определённый диапазон ASCII кодов
                    //------поэтому определяем с чего начинается свежедовычитанный ответ
                    //------если с маркера, то отсекаем его, а если нет, то ничего не трогаем
                    
    if($a>96&&$a<123||$a>64&&$a<91||$a>47&&$a<58):$b=substr($b,strpos($b,"\r\n")+2);endif;
                    }
                }
            else{
                if(
    ==0):$b=substr($b,10);endif;
                }
            
    $body.=$b;//-----соединяем воедино неразжатое тело ответа, это важно
            
    if(strlen($b)>0):$b=gzinflate($body);endif;
            }else{}
        
    //-----далее дополнительная примочка - перевод в нужную кодировку. Мои исполняемые фалы сохранены в utf-8
        
    if(strpos($h,"charset=windows-1251")!==false):$b=iconv("cp1251","utf-8",$b);endif;
        
    //-----собираем страницу
        
    if(==0)$page=$h."\r\n\r\n".$b;
        else 
    $page.=$b;
        
    //-----теперь в глобальной переменной $page содержится целый, разжатый и перекодированный ответ,
        //-----с которым уже можно работать
        //-----и как бонус - обработка кукисов.
        //-----чтобы отправить куки, в запрос нужно засунуть строчку $request.="Cookie: ".implode("; ",$session)."\r\n";
        //-----куки будут собираться только при первом получении страницы, 
        //-----а также можно работать с каждой отдельной кукой,
        //-----каждая из которых является элементом ассоциативного массива $session с ключом равным названию куки
        //-----и значением в формате<имя куки=значение куки>
        
    if($c==0){
            
    $arr=explode("\r\n",$h);
            foreach(
    $arr as $v){
                
    $b1=strpos($v,"Set-Cookie");
                if(
    $b1!==false){
                    
    $v=explode(";",substr($v,12));
                    
    $v=explode("=",$v[0]);
                    
    $session[$v[0]]=$v[0]."=".$v[1];
                    if(
    $v[1]=="deleted"||$v[1]=="DELETED")unset($session[$v[0]]);//--если сервер просит удалить куку
                    
    }else{}
                }
            }else{}
        }
    После вызова этой функции у вас будет разжатая страница и все куки
     
    #6 mamontenok, 20 Apr 2012
    Last edited: 20 Apr 2012
    1 person likes this.
  7. GRRRL Power

    GRRRL Power Elder - Старейшина

    Joined:
    13 Jul 2010
    Messages:
    823
    Likes Received:
    185
    Reputations:
    84
    Про чанки мне известно, но не думал, что здесь дело в этом. Чанки приходят, только если запрашиваешь страницу с HTTP/1.1. Если использовать HTTP/1.0, то она не бьется.
     
  8. mamontenok

    mamontenok Banned

    Joined:
    17 Jul 2010
    Messages:
    12
    Likes Received:
    1
    Reputations:
    5
    Так я тоже не думал, что дело в чанках. Насчёт запрашивания в HTTP/1.0 не уверен. Не буду точно утверждать, но вроде бы видел как на запрос в 1.0 сервер всё равно отвечал в 1.1. Но по правилам, конечно, сервер должен отвечать в 1.0
     
  9. Flisk

    Flisk Member

    Joined:
    4 Aug 2010
    Messages:
    147
    Likes Received:
    8
    Reputations:
    -2
    Извиняюсь что влезаю, но вот описание на русском, если кому надо (и не знает инглиш).
    http://rfc2.ru/2068.rfc
     
    1 person likes this.
  10. mamontenok

    mamontenok Banned

    Joined:
    17 Jul 2010
    Messages:
    12
    Likes Received:
    1
    Reputations:
    5
    Flisk, отличную ссылку подкинул, благодарю