Как парсить страницы сайтов с автоподгрузкой на примере Instagram

Как парсить страницы сайтов с автоподгрузкой на примере Instagram

Статья обновлена 29 июня 2018 в связи с изменениями структуры JS необходимой для извлечения query_hash. Механика автоподгрузки на страницах сайтов осуществляется с помощью Javascript. Поэтому, для того, чтобы определить на какой URL нам нужно обращаться и какие параметры использовать, нам нужно либо досконально изучить JS код который работает на странице, либо, и что предпочтительней, изучить запросы, которые делает браузер при прокрутке страницы вниз. Изучить запросы мы можем с помощью Инструментов для разработчика, которые встроены во все современные браузеры. В нашей статье мы будем использовать Google Chrome, но вы можете использовать любой другой браузер, приняв во внимание, что инструменты разработчика могут выглядеть по разному в разных браузерах.

Изучать нашу задачу мы будем на примере Instagram, а именно, используя официальный канал Instagram. Откроем эту страницу в браузере, и запустим Chrome Dev Tools — инструменты для разработчика, которые встроены в Google Chrome. Для этого кликнем правой кнопкой мыши в любом месте страницы и выберем опцию «Просмотреть код» или нажмите «Ctrl+Shift+I»:

Учимся писать парсеры на примере Instagram: открываем инструменты для разработчика

У нас откроется окно инструментов, где мы перейдем во вкладку Network и в фильтрах выберем показ только XHR запросов. Мы это делаем для того, чтобы отфильтровать ненужные нам запросы. После этого перезагрузим страницу в браузере с помощью кнопки Reload в интерфейсе браузера или клавиши «F5» на клавиатуре.

Учимся писать парсеры на примере Instagram: изучаем XHR запросы

Давайте теперь прокрутим страницу вниз несколько раз с помощью колесика мышки, что вызовет подгрузку контента. Каждый раз, когда при прокручивании мы будем достигать нижней части страницы, JS будет делать XHR запрос на сервер, получать данные и добавлять их на страницу. В результате, у нас в списке окажется несколько запросов, которые выглядят почти одинаково. Скорее всего они нам и нужны.

Учимся писать парсеры на примере Instagram: находим нужные XHR запросы

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

Учимся писать парсеры на примере Instagram: проверяем содержимое XHR запросов

Убедившись, что это нужные нам запросы, рассмотрим один из них более внимательно. Для этого перейдем во вкладку Headers. Там мы можем найти информацию о том, на какой именно URL производится запрос, какой тип запроса (POST или GET) используется, а также какие параметры передаются с запросом.

Учимся писать парсеры на примере Instagram: изучаем XHR запрос

Параметры запроса лучше изучать в секции Query String Parameters, прокрутив рабочее окно в панели инструментов вниз до конца:

Учимся писать парсеры на примере Instagram: параметры XHR запроса

Результатом нашего анализа станут следующие факты:
URL запроса: https://www.instagram.com/graphql/query/
Тип запроса: GET
Передаваемые параметры: query_hash и variables

Очевидно, что в query_hash передается статичный id, который генерируется, скорее всего, когда вы заходите на страницу. В variables же передаются некие параметры в JSON формате, влияющие на выборку данных.

Давайте проведем небольшой эксперимент, возьмем URL с параметрами, который использовался для загрузки данных:

https://www.instagram.com/graphql/query/?query_hash=df16f80848b2de5a3ca9495d781f98df&variables=%7B%22id%22%3A%2225025320%22%2C%22first%22%3A12%2C%22after%22%3A%22AQDsbvCEthjsp_O_8UO9vPTHKy6Qea2H_RRxe7v46B2XKXhSYVTv8FLSDk0BxmXqLw_T1R9aB8DB51Kp2hp80mP51bKdG9Ahy4eKWT9h3QplzA%22%7D

Если бы до последнего апдейта API мы бы взяли и вставили его в адресную строку браузера и нажали Enter, то мы бы увидели как загрузится страница в JSON формате:

Учимся писать парсеры на примере Instagram: фид с подгруженными данными

Однако, теперь просто так API Инстаграма не отдает данные, для этого необходимо рассчитать подпись для запроса и передать ее в заголовке запроса. Этот вопрос более подробно рассматривается ниже. Без корректного заголовка все что мы получим сейчас — это ошибку 403.

Теперь нам нужно понять, откуда берется query_hash. Если мы перейдем во вкладку Elements и попытаемся найти (CTRL+F) наш query_hash df16f80848b2de5a3ca9495d781f98df, то мы узнаем что на самой странице его нет, а значит он подгружается или генерируется где-то в коде Javascript. Поэтому, перейдем опять во вкладку Network и поставим фильтр на JS. Таким образом мы увидим только запросы на JS файлы. Последовательно перебирая запрос за запросом, будем искать наш id в загруженных файлах: просто выбираем запрос, затем открываем в открывшейся панели вкладку Response чтобы увидеть содержимое JS и делаем поиск нашего id (CTRL+F). После нескольких неудачных попыток, мы обнаружим, что наш id находится в следующем JS файле:

https://www.instagram.com/static/bundles/ProfilePageContainer.js/031ac4860b53.js

а фрагмент кода, который обрамляет id, выглядит так:

Соответственно, для получения query_hash нам надо найти на первой странице URL на ProfilePageContainer.js файл, извлечь этот URL, забрать JS файл по этому URL, распарсить место с нужным нам id и записать его в переменную для дальнейшего использования.

Теперь давайте посмотрим, что за переменные передаются в variables:

Если мы проанализируем все XHR запросы с догружаемыми данными, что мы обнаружим, что меняется только параметр after. Поэтому id скорее всего есть id канала, который мы парсим, first — количество записей, которые сервер должен отдать по запросу, а after — очевидно id последней показанной записи.

Нам нужно найти место, из которого мы можем извлечь id канала, для этого первым делом мы поищем текст 25025320 в исходном коде начальной страницы. Перейдем во вкладку Elements и сделаем поиск (CTRL+F) нашего id. Мы обнаружим, что он есть в JSON структуре на самой странице, именно оттуда мы и можем его извлечь:

Учимся писать парсеры на примере Instagram: JSON структура с нужными данными

Вроде все понятно, но где нам брать этот самый after для каждой последующей подгрузки? Все очень просто. Если мы загрузим в браузере следующий URL:

https://www.instagram.com/graphql/query/?query_hash=df16f80848b2de5a3ca9495d781f98df&variables=%7B%22id%22%3A%2225025320%22%2C%22first%22%3A12%2C%22after%22%3A%22AQAzEauY26BEUyDxOz9NhBP2gjLbTTD3OD1ajDxZIHvldwFwboiBnIcglaL6Kb_yDssRABBoUDdIls5V8unGC86hC2qk_IeLFUcH2QPTrY3f4A%22%7D

мы увидим, что там есть следующая структура:

То есть, в нашей логике мы сможем использовать значение поля has_next_page чтобы знать переходить ли на следующую страницу или нет и end_cursor как значение параметра after.

Сейчас мы напишем заготовку нашего парсера, загрузим первую страницу и попытаемся загрузить JS файл с query_id. Создайте диггер в вашем аккаунте Diggernaut и добавьте в него следующую конфигурацию:

Установите диггер в режим Отладка. Теперь нам нужно запустить наш парсер и после того как он отработает посмотреть лог. В конце лога мы увидим как диггернаут работает с JS файлами. Он преобразовывает их в следующую структуру:

А значит селектор для забора всего JS будет script. Давайте допишем функцию парсинга query_id из JS:

Сохраним наш парсер и снова запустим. Подождем когда он закончит работу и посмотрим в лог. В логе мы увидим следующую строчку:

Set variable queryid to register value: 42323d64886122307be10013ad2dcc44

Это значит, что query_hash был успешно извлечен и записан в переменную с именем queryid.

Теперь мы извлечем id канала. Как вы помните, он есть в JSON объекте на самой странице. Поэтому нам нужно взять содержимое определенного элемента script, вытащить оттуда JSON, конвертировать его в XML и забрать нужное нам значение, используя CSS селектор.

Если вы внимательно посмотрите в лог, то увидите, что JSON структура трансформируется в DOM следующим образом:

Это поможет нам построить CSS селекторы для забора первых 12 записей и маркера последней записи, который нужен нам для забора следующих 12 записей. Напишем логику для извлечения данных, а также начем формировать пул (pool) линков со ссылками на фиды (feeds) с подгружаемыми данными. Далее начнем итерацию по пулу линков и посмотрим как преобразует Diggernaut полученный JSON, так, чтобы мы смогли построить корректные CSS селекторы для логики парсера.

Совсем недавно Instagram сделал изменения в публичном API, теперь для авторизация делается не по CSRF токену, а по специальной сигнатуре, которая рассчитывается используя новый параметр rhx_gis, передаваемый в sharedData странице канала и передаваемые в запросе переменные. Алгоритм можно узнать при разборе JS. Этот алгоритм мы используем и будем автоматически подписывать запросы. Для этого нам нужно извлечь rhx_gis параметр.

После запуска в логе мы можем увидеть вот такую структуру, с которой нам нужно работать:

Мы намеренно укоротили исходный код, убрав повторяющиеся элементы. Теперь мы можем описать логику парсинга всех нужных нам полей, а также добавить ограничитель на количество подгрузок, скажем, 10. Также мы добавим паузу, для менее агрессивного парсинга. В результате мы получим финальную версию нашего парсера Instagram.

Теперь мы пожем перевести наш диггер в Активный режим и запустить его. Как результат в вашем наборе данных будут подобные записи.

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

16 комментариев

    • Михаил Сисин

      Спасибо за замечание. Это конечно же упрощает написание парсера. Однако нашей целью является научить человека писать парсеры самостоятельно, а момент парсинга какого-либо id из подгружаемых файлов как сам факт достаточно важен, чтобы исключать его из статьи. Поэтому оставим статью как есть. Для тех же, кто использует парсер в продакшине, можно использовать совет Вячеслава и использовать статический теперь уже query_hash.

  • Макс

    Михаил, подскажите, а данный способ подойдёт для парсинга нет всех записей аккаунта инстаграма, а для конкретной записи, если я укажу ссылку на конкретную страницу (фотографию) в инстаграм?

    • Михаил Сисин

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

    • Михаил Сисин

      Конечно, в базовом парсере, представленном здесь забирается ограниченное число записей в канале. Это число можно увеличить изменив строку 183. Однако мы обнаружили, что Инстаграм изменил что-то на странице. Сегодня в течении дня мы внесем изменения в статью, чтобы она соответствовала действительности.

      Статья обновлена, произошли изменения в системе авторизации публичного API.

  • Георгий

    «возьмем URL с параметрами, который использовался для загрузки данных <…> Вставим его в адресную строку браузера и нажмем Enter. Мы увидим как загрузится страница в JSON формате»
    Не грузится, выдаёт «403 Forbidden». ЧЯДНТ?

    • Михаил Сисин

      Вы все делаете правильно. Дело в том, что после последнего апдейта API Instagram пропала возможность загрузки фидов напрямую в браузер, поскольку текущая реализация авторизации требует передачи цифровой подписи запроса в заголовке запроса. Спасибо за указание на неточность, текст статьи мы поправили.

  • Юлия

    Здравствуйте, очень понравилась статья! Хотелось бы узнать : можно ли так же сделать с определенными хештегами ?

    • Михаил Сисин

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

      • Юлия

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

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *