Простым языком об использовании PHP с cURL на одном примере сайта с JavaScript-защитой.

1. Парсер для получения контента по ссылкам

Задача парсера тривиальная - агрегатор новостей: сбор контента с новостных сайтов.
На входе: файл с URL-ссылками на статьи для сбора.

Веб-интерфейс для администратора: php-страница с кнопкой для запуска скрипта парсера.
Скрипт: использование cURL - с минимальным набором опций.

function cURL_get_content($url){
	$ch = curl_init();
	curl_setopt($ch, CURLOPT_URL, $url);
	curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
	curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
	curl_setopt($ch, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']);
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

	$urlContent = curl_exec($ch);
	curl_close($ch);
	return $urlContent;
}

Кейс:

1) Пользователь:

  • заходит на страницу запуска скрипта,

  • нажимает кнопку запуска.

    2) Скрипт:

  • обходит (в цикле) массив ссылок на сайты,

  • собирает контент статей,

  • формирует веб-страницы по заданному шаблону.

Проблема: наткнувшись на интересную статью с сайта о недвижимости (Домофонд), попытался добавить ее в файл ссылок для сбора контента.

Контент со страницы не собрался: 403 Forbidden.

Ссылка: https://www.domofond.ru/statya/kolichestvo_dolgostroev_uvelichilos_v_vosemnadtsati_regionah_rf_za_2021_god/102082

Спойлер: если сразу интересно найденное решение, то можно переходить к заключению, пропустив бесплодные попытки поиска.

2. Попытка решения в лоб за счет "имитации" браузера доп. заголовками в запросе

В поисках сначала натолкнулся на статьи о том, что некоторые сайты ставят защиту от ботов (чем и является парсер для сбора контента). Например, при использовании cURL надо "оживлять" запрос заголовками, что я и делал, но безуспешно - проиллюстрирую одной из попыток.

function cURL_get_content($url){
$url_1 = "https://www.domofond.ru/";
$url_2 = $url;

$headers = array(
	'GET ' . $url_2 . ' HTTP/1.1',
	"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", 
    "Accept-Encoding: gzip, deflate", 
    "Accept-Language: ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7", 
    "Host: domofond.ru", 
	"Referer: domofond.ru", 
    "Upgrade-Insecure-Requests: 1", 
	'User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36',
	'Cookie: dfuid=5e0dd723-40e6-46cf-a2f1-1db784e320e1; _ga=GA1.2.140590976.1641893876; rrpvid=42725831313485; rcuid=6166ad1a101fb8000139a8a9; _ym_uid=1641893879540019262; _ym_d=1641893879; __gads=ID=00752b7382f51452:T=1641893879:S=ALNI_ManEsFfeviPUR29VUUnBx_DcgsAxQ; _gid=GA1.2.589118605.1642400326; _ym_visorc=w; _ym_isad=2; cto_bundle=ellcGV9VT0c4Q0Vrd2E3dEhtVmhJNk1Ic20xaHNnNXhuSiUyRiUyQiUyRnZYMlFEV0tnNExTWmhQUjJJVzdUOHdkNmlKdnh1aEZtUWMzQ0dxV25Nb3hrYktOeUpEMjZLZ0xLTFElMkZDekxiSkh6elEyOFM0UHVZZ2xHTklpQ2RJOTRkb1Q3QUNTODRlR0hnU0Z1MEFkeDBtNTc1MVozdjVndyUzRCUzRA'
);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
...
}

Пример запроса с заголовками:
GET /statya/kolichestvo_dolgostroev_uvelichilos_v_vosemnadtsati_regionah_rf_za_2021_god/102082 HTTP/1.1 Host: www.domofond.ru User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36 Accept: ... +куки +смотрел/добавлял заголовки, отправленные в браузере

2.1. Заголовок с Host

Хотя при добавлении хост-заголовка все же возникла одна из мыслей.

Если значение заголовка менялось с "Host: www.domofond.ru" на "Host: domofond.ru" (без www.), то появлялась ошибка: 421 Misdirected Request.

Поэтому в браузере я вручную открывал страницу с/без www - естественно, визуально для меня ничего не менялось.

Перенаправление срабатывало нормально, но потом я еще решил изменить и протокол, чтобы проверить, какой будет редирект.

Сменив в браузере ссылку с https:// на http:// (с/без www) получал нормальный редирект на главное зеркало (https://www.) с нормальным отображением статьи.

Заменив ссылку на https://146.158.48.9/statya/kolichestvo_dolgostroev_uvelichilos_v_vosemnadtsati_regionah_rf_za_2021_god/102082 - с IP-адресом хоста, получил, что подключение к сайту не защищено (несмотря на протокол).

При этом вместо статьи отобразилась та же ошибка: 421 Misdirected Request.

3. Изучение веб-страницы, "проблемной" для парсера

Продолжив поиски по теме защиты на сайтах от ботов, столкнулся с выполнением JavaScript-кода на страницах для этой цели.

Сразу скажу, что в браузере отключил выполнение JavaScript-сценариев, но страница продолжила отображаться нормально.

Хотя при изучении кода проблемной страницы смутил следующий скрипт, включающий в виде JSON-блока контент статьи.

<script>window.__INITIAL_DATA__ = {...,"body":"<h2>В восемнадцати регионах России с начала 2021 года увеличилось число проблемных объектов, жилье в которых ждут обманутые дольщики. Об этом сообщает ТАСС со ссылкой на Фонд защиты прав граждан — участников долевого строительства.</h2><figure class=\"image\"><img src=\"https://st04.domofond.ru/image/1/1.8e4sara2-QaYKU3NkRHj4GaUXwecy78XUsJf.n2qDfSv5Xan97G3s9xyoTicCp2winXLLHxcVGotZqQg\" srcset=\"https://st29.domofond.ru/image/1/1.UJZ1xba2WH7Bxv-hwvxU-1U7_n_FZF58H2f-.mAwXzCu3zagIajLE0KwxA4pTSi_Rcg215CdmhiCxQdo 80w, https://st04.domofond.ru/image/1/1.8e4sara2-QaYKU3NkRHj4GaUXwecy78XUsJf.n2qDfSv5Xan97G3s9xyoTicCp2winXLLHxcVGotZqQg 1200w\" sizes=\"100vw\" width=\"1200\"><figcaption>markus thoenen/Fotolia</figcaption></figure><p>&nbsp;</p><p>В частности, новые долгострои находятся в Ленинградской области — 33 объекта, Ивановской области&nbsp;— 9 объектов, Красноярском крае — 9 объектов, Ульяновской области — 8 объектов. По данным фонда, общее число новых проблемных домов составило 90 объектов.</p><p>Однако за тот же период в 39 субъектах РФ количество долгостроев сократилось на 368 домов. В число данных регионов вошли Московская, Ростовская, Липецкая области, Башкирия, Краснодарский край и другие.</p><p>Добавим, что в настоящее время в стране зафиксировано 2,6 тыс. проблемных объектов, которые находятся в 72 регионах России.</p><p><strong>Не пропустите:</strong></p><p><a href=\"https://www.domofond.ru/statya/fond_dolschikov_zaymetsya_tolko_vyplatoy_denezhnyh_kompensatsiy/101990\" target=\"_blank\"><strong>Фонд дольщиков займется только выплатой денежных компенсаций</strong></a></p><p><a href=\"https://www.domofond.ru/statya/pri_perenose_sdachi_doma_dengi_s_eskrou_schetov_ne_budut_vozvraschatsya_dolschikam/102037\" target=\"_blank\"><strong>При переносе сдачи дома деньги с эскроу-счетов не будут возвращаться дольщикам</strong></a></p><p><a href=\"https://www.domofond.ru/statya/mozhno_li_vzyskat_neustoyku_s_zastroyschika_za_zaderzhku_sdachi_kvartiry/100823\" target=\"_blank\"><strong>Можно ли взыскать неустойку с застройщика за задержку сдачи квартиры?</strong></a></p><p><a href=\"https://www.domofond.ru/statya/kak_vzyskat_kompensatsiyu_s_zastroyschika_zaderzhavshego_sdachu_doma/6766\" target=\"_blank\"><strong>Как взыскать компенсацию с застройщика, задержавшего сдачу дома?</strong></a></p>","author":{"id":144,"name":"Анна Филонова","profile":"<p><strong>Редактор Domofond.ru</strong></p>"},...}
</script>

4. ПО для сбора контента с веб-сайтов

В ходе исследований попадались статьи на "безголовые" браузеры, среды наподобие селениума и т.п.

Предлагалась имитация действий пользователя, установка тайм-аутов при обращении к сайту для снижения нагрузки и т.д.

Но для решения задач по парсингу одной-двух ссылок, а не многопоточного скачивания всего сайта это не подходило.

Глубоко в этом направлении не разбирался и не копал.

5. Возврат к мысли про ссылку с IP-адресом сервера

Пришлось вернуться к начальному вопросу: как получить контент веб-страницы, если по ссылке с именем хоста не получилось.

Решил, в качестве эксперимента, скормить парсеру ссылку https://146.158.48.9/statya/kolichestvo_dolgostroev_uvelichilos_v_vosemnadtsati_regionah_rf_za_2021_god/102082 - с IP-адресом хоста.

В ответ получил содержимое страницы.

6. Выводы и решение

В чем проблема - до конца непонятно. Закроет ли собственник сайта (Домофонд) эту дыру и сможет ли это вообще сделать - неясно.

Но в статье приведено достаточно простое решение и оно работает, а на просторах интернета с такой легкой подсказкой я не сталкивался.

В решении дополнил скрипт методом gethostbyname, чтобы получать IPv4-адрес, соответствующий переданному имени хоста.

Парсер для получения контента по ссылке на входе имеет тот же набор URL-ссылок, но теперь обращается по IP-адресам серверов, что не мешает его работе в сборе данных.

Комментарии (32)


  1. TheRikipm
    23.01.2022 15:41
    +5

    Но для решения задач по парсингу одной-двух ссылок, а не многопоточного скачивания всего сайта это не подходило.

    А зачем скачивать весь сайт если можно скачать только контент этих одной-двух ссылок?

    В вашем случае самым "правильным" решением будет как раз селениум с безголовым браузером. По крайней мере домофонду защититься от него будет куда сложнее чем от вашего итогового решения.


    1. dchizhikov Автор
      23.01.2022 16:44
      -3

      Не хотелось лишнего ПО.


      1. amarao
        23.01.2022 23:26
        +1

        Так что вы написали ещё ПО. Л - логика.


        1. dchizhikov Автор
          24.01.2022 09:30

          Скрипт уже был и прекрасно работает.

          Потребовалось лишь минимально модернизировать.


          1. amarao
            24.01.2022 14:29

            Но вы же не хотели лишнего ПО, но вместо этого написали ещё ПО!


            1. dchizhikov Автор
              24.01.2022 14:34

              Скрипт не лишнее ПО, а работающий под задачу функционал.


              1. amarao
                24.01.2022 16:08
                +1

                А всё остальное ПО, которое решало поставленную задачу классом лучше, лишнее?

                Поздравляю, у вас NIH-синдром. Гуглябельно.


                1. dchizhikov Автор
                  24.01.2022 16:27

                  Микроскопом тоже можно гвозди заколачивать)))


                  1. amarao
                    24.01.2022 18:04
                    +2

                    Да. Но вместо этого это изобретаете микроскоп из трубочки для туалетной бумаги и двух полиэтиленовых пакетиков с водой.

                    А потом всё равно им забиваете гвозди.


                    1. dchizhikov Автор
                      24.01.2022 18:15

                      я-то как раз с молотком просто, юморист))


                      1. amarao
                        24.01.2022 18:26
                        +1

                        Нет, вы не с инструментом пришли, а со средством разработки, и написали ещё одно ПО. Лишнее.


                      1. dchizhikov Автор
                        24.01.2022 18:42
                        -1

                        Нет - см. выше))


  1. random1st
    23.01.2022 16:09
    +3

    Скопировал CURL сгенерированный браузером, импортировал в Postman, поотключал все хидеры, защиту никакую не обнаружил. Все.

    PS. Если дать себе труда немного подумать, то запрет ботов на новостном сайте равносильно запрету индексации и выдаче его в поисковиках. Более того, поисковики не приветствуют когда содержимое страницы отдаваемое пользователям не совпадает с тем, что забирают они.


    1. dchizhikov Автор
      23.01.2022 16:45
      -4

      Не хотелось лишнего ПО.

      В роботс.тхт действительно стоит много запретов для ботов, но не на новостные статьи.

      По имени и по айпи хоста - отдается одинаковый контент.


      1. random1st
        23.01.2022 16:55
        +1

        robots.txt пассивный механизм, не влияющий на отдачу контента. Что касается отдачи по IP и по имени - вообще касательства к делу не имеет равно как и лишнее ПО. Я пытаюсь сказать что проблема скорее всего на вашей стороне.


        1. dchizhikov Автор
          23.01.2022 17:00
          -2

          Например, какие проблемы? С другими сайтами проблем по сбору нет.

          Хотел дополнить в статье, но напишу в комменте, что с авито - примерно та же ситуация возникла. Решено этим же подходом - успешно.


          1. random1st
            23.01.2022 17:44
            +3

            без понятия. Для того чтобы ответить, нужно для начала проблему воспроизвести. Мне не удалось. Сайт отдает контент всегда. Даже когда я пустил через Apache Benchmark 100 запросов с конкарренси в 10 мне не удалось активировать никаких механизмов защиты. Так что вопрос с кривым кодом на вашей стороне по-прежнему наиболее вероятен.


            1. dchizhikov Автор
              23.01.2022 18:00

              Можете скрин ответа привести - что именно отдает, какой контент?


              1. random1st
                23.01.2022 18:04

                контент страницы естественно. Скрины прикладывать не вижу смысла, еще раз повторюсь - мне отдает все то же самое и в браузере, и curl и postman. Разбирайтесь с проблемой на своей стороне. Бредово рассуждать о защите JS если у вас статически рендерится страница.


                1. dchizhikov Автор
                  23.01.2022 18:42
                  -2

                  какая-то защита там есть по-любому - видимо, от парсинга предложений о жилье. проверю тогда сам в постмане, спасибо.


                1. dchizhikov Автор
                  24.01.2022 14:41

                  постман гетом по ссылке отдает контент - вообще без каких-либо заголовков.


  1. koreychenko
    23.01.2022 16:30
    +2

    Дичь лютейшую прочитал сейчас я.
    Коллега, для ваших целей замечательно зайдёт вот такая штука:
    https://splash.readthedocs.io/en/stable/
    Он очень просто ставится. У него даже docker image готовый есть.
    А дальше вы делаете к нему http запрос хоть тем же курлом с указанием урла сайта, который нужно спарсить - и он возвращает вам html.
    Более того, можно писать свои скрипты парсинга на Lua, если нужно парсить по какому-то сценарию. Например, если сайты с AJAX подгрузкой контента, постраничником и т.п.
    Из минусов:
    - разработчики на него немного подзабили, походу
    - падает на сложных Angular приложениях


    1. dchizhikov Автор
      23.01.2022 16:45
      -5

      Не хотелось лишнего ПО.


    1. cry_san
      24.01.2022 03:23

      Только ради этого комментария стоило зайти на эту статью.

      Спасибо!


  1. zzzzzzzzzzzz
    23.01.2022 16:44
    +3

    В решении дополнил скрипт методом gethostbyname, чтобы получать IPv4-адрес, соответствующий переданному имени хоста.

    Плохая идея, т.к., начиная с HTTP/1.1, на одном IP может висеть несколько сайтов. Соответственно, скачиваться будет что-нибудь не то.


    1. dchizhikov Автор
      23.01.2022 17:06

      В этом случае (домофонд, авито) было как раз наоборот (изначально использовал gethostbynamel - для списка айпи). Думаю, что для крупных сайтов так и будет.

      Но спасибо за коммент.


      1. satoo
        24.01.2022 01:30
        +2

        ????‍♂️ как одним комментом показать непонимание работы http (а также того, что помогает ему: балансировщиков, проксей, cdn и пр)


        1. dchizhikov Автор
          24.01.2022 09:36

          Стояла задача работоспособности скрипта для 1 сайта с защитой - она решена.

          О чем вкратце рассказано в статье.


  1. NickyX3
    24.01.2022 10:39

    Я тут намедни столкнулся с дригой забавной ситуацией.

    cURL из под PHP на двух "одинаковых" версиях PHP на разных машинах при запросе одного и того же урла выдавал на одной машине заголовки как есть, а на другой в нижнем регистре. Так и не понял почему, тупо добавил в regexp case independed.


    1. random1st
      25.01.2022 00:04

      а кто сказал что проблема в версии PHP а не в web-сервере?


      1. NickyX3
        25.01.2022 10:54

        а зачем вебсерверу отдавать заголовки то в нормальном виде, то в нижнем регистре?
        Тем более страница одна и таже, и по факту оно проявлялось именно на разных машинах, которые делают запрос. Я подозреваю, что "проблема" в версии cURL extension/OpenSSL. Ибо хоть версия php7.4 и там и там одна, но одна тачка Debian 10, другая Debian 11


  1. apirk
    25.01.2022 09:10
    +1

    О чём статья вообще? За 5 минут накидал скрипт, никакой защиты на веб-сервере нет.

    $wc = New-Object system.Net.WebClient;
    ($wc.downloadString("https://www.domofond.ru/statya/kolichestvo_dolgostroev_uvelichilos_v_vosemnadtsati_regionah_rf_za_2021_god/102082") -split '\r?\n')[60].Replace('</script>','').Replace('                <script>window.__INITIAL_DATA__ = ', '') | ConvertFrom-Json

    На выходе готовый JSON, дёргайте оттуда любые данные. В чём проблема-то? )