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

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

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

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

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

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

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

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

Результатом нашего анализа станут следующие факты:
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 формате:

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

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

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

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

s.pagination},queryId:"f2405b236d85e8296cf30347c9f08c2a"

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

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

{"id":"25025320","first":12,"after":"AQAzEauY26BEUyDxOz9NhBP2gjLbTTD3OD1ajDxZIHvldwFwboiBnIcglaL6Kb_yDssRABBoUDdIls5V8unGC86hC2qk_IeLFUcH2QPTrY3f4A"}

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

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

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

data: {
    user: {
        edge_owner_to_timeline_media: {
            count: 5014,
            page_info: {
                has_next_page: true,
                end_cursor: "AQCCoEpYvQtj0-NgbaQUg9g4ffOJf8drV2RieFJw1RA3E9lDoc8euxXjeuwlUEtXB6CRS9Zs2ZGJcNKseKF9f6b0cN0VC3ck8rnTfOw5q8nlJw"
            }
        }
    }
}

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

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

---
config:
    agent: Firefox
    debug: 2
do:
# Загружаем начальную страницу
- walk:
    to: https://www.instagram.com/instagram/
    do:
    # Ищем элементы script которые подгружают JS
    - find:
        path: script[type="text/javascript"]
        do:
        # Парсим значение атрибута src
        - parse:
            attr: src
        # Проверяем, нужный ли это Javascript, нам нужен тот, у которого в URL есть строка ProfilePageContainer.js
        - if:
            match: ProfilePageContainer\.js
            do:
            # Переходим по URL скрипта
            - walk:
                to: value
                do:

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

<html>
  <head></head>
    <body>
      <body_safe>
          <script>
              ... JS код будет здесь
                </script>
      </body_safe>
    </body>
</html>

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

---
config:
    agent: Firefox
    debug: 2
do:
# Загружаем начальную страницу
- walk:
    to: https://www.instagram.com/instagram/
    do:
    # Ищем элементы script которые подгружают JS
    - find:
        path: script[type="text/javascript"]
        do:
        # Парсим значение атрибута src
        - parse:
            attr: src
        # Проверяем, нужный ли это Javascript, нам нужен тот, у которого в URL есть строка ProfilePageContainer.js
        - if:
            match: ProfilePageContainer\.js
            do:
            # Переходим по URL скрипта
            - walk:
                to: value
                do:
                # Ищем элемент, содержащий искомый JS
                - find:
                    path: script
                    do:
                    # Парсим контент элемента, используя фильтр с регулярным выражением
                    - parse:
                        filter: profilePosts\.byUserId\.get[^,]+,queryId\:\&\s*quot\;([^&]+)\&\s*quot\;
                    # Сохраняем полученное значение в переменной
                    - variable_set: queryid

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

Set variable queryid to register value: 42323d64886122307be10013ad2dcc44

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

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

---
config:
    agent: Firefox
    debug: 2
do:
# Загружаем начальную страницу
- walk:
    to: https://www.instagram.com/instagram/
    do:
    # Ищем элементы script которые подгружают JS
    - find:
        path: script[type="text/javascript"]
        do:
        # Парсим значение атрибута src
        - parse:
            attr: src
        # Проверяем, нужный ли это Javascript, нам нужен тот, у которого в URL есть строка ProfilePageContainer.js
        - if:
            match: ProfilePageContainer\.js
            do:
            # Переходим по URL скрипта
            - walk:
                to: value
                do:
                # Ищем элемент, содержащий искомый JS
                - find:
                    path: script
                    do:
                    # Парсим контент элемента, используя фильтр с регулярным выражением
                    - parse:
                        filter: profilePosts\.byUserId\.get[^,]+,queryId\:\&\s*quot\;([^&]+)\&\s*quot\;
                    # Сохраняем полученное значение в переменной
                    - variable_set: queryid
    # находим элемент script, который содержит текст window._sharedData
    - find:
        path: script:contains("window._sharedData")
        do:
        - parse
        - space_dedupe
        - trim
        # извлекаем JSON
        - filter: 
            args: window\._sharedData\s+\=\s+(.+)\s*;\s*$
        # конвертим JSON в XML
        - normalize:
            routine: json2xml
        # превращаем XML строку в DOM блок
        - to_block
        - find: 
            path: body_safe 
            do: 
        # Находим элемент в котором хранится id канала
        - find:
            path: entry_data > profilepage > graphql > user > id
            do:
            # Парсим содержимое элемента
            - parse
            # Сохраняем полученное значение в переменной
            - variable_set: chid

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

<body_safe>
    <activity_counts></activity_counts>
    <config>
        <csrf_token>qNVodzmebd0ZnAEOYxFCPpMV1XWGEaDz</csrf_token>
        <viewer></viewer>
    </config>
    <country_code>US</country_code>
    <display_properties_server_guess>
        <orientation></orientation>
        <pixel_ratio>1.5</pixel_ratio>
        <viewport_height>480</viewport_height>
        <viewport_width>360</viewport_width>
    </display_properties_server_guess>
    <entry_data>
        <profilepage>
            <logging_page_id>profilePage_25025320</logging_page_id>
            <graphql>
                <user>
                    <biography>Discovering — and telling — stories from around the world. Curated by Instagram’s community
                        team.</biography>
                    <blocked_by_viewer>false</blocked_by_viewer>
                    <connected_fb_page></connected_fb_page>
                    <country_block>false</country_block>
                    <external_url>http://blog.instagram.com/</external_url>
                    <external_url_linkshimmed>http://l.instagram.com/?u=http%3A%2F%2Fblog.instagram.com%2F&e=ATM_VrrL-_PjBU0WJ0OT_xPSlo-70w2PtE177ZsbPuLY9tmVs8JmIXfYgban04z423i2IL8M</external_url_linkshimmed>
                    <followed_by>
                            <count>230937095</count>
                    </followed_by>
                    <followed_by_viewer>false</followed_by_viewer>
                    <follows>
                            <count>197</count>
                    </follows>
                    <follows_viewer>false</follows_viewer>
                    <full_name>Instagram</full_name>
                    <has_blocked_viewer>false</has_blocked_viewer>
                    <has_requested_viewer>false</has_requested_viewer>
                    <id>25025320</id>
                    <is_private>false</is_private>
                    <is_verified>true</is_verified>
                    <edge_owner_to_timeline_media>
                        <count>5014</count>
                        <edges>
                            <node>
                                <safe___typename>GraphVideo</safe___typename>
                                <comments_disabled>false</comments_disabled>
                                <dimensions>
                                        <height>607</height>
                                        <width>1080</width>
                                </dimensions>
                                <display_url>https://scontent-iad3-1.cdninstagram.com/vp/9cdd0906e30590eed4ad793888595629/5A5F5679/t51.2885-15/s1080x1080/e15/fr/26158234_2061044554178629_8867446855789707264_n.jpg</display_url>
                                <edge_media_preview_like>
                                        <count>573448</count>
                                </edge_media_preview_like>
                                <edge_media_to_caption>
                                        <edges>
                                                <node>
                                                        <text>Video by @yanndixon Spontaneous by nature,
                                                                a flock of starlings swarm as one
                                                                at sunset in England. #WHPspontaneous</text>
                                                </node>
                                        </edges>
                                </edge_media_to_caption>
                                <edge_media_to_comment>
                                        <count>4709</count>
                                </edge_media_to_comment>
                                <id>1688175842423510712</id>
                                <is_video>true</is_video>
                                <owner>
                                        <id>25025320</id>
                                </owner>
                                <shortcode>Bdtmvv-DJa4</shortcode>
                                <taken_at_timestamp>1515466361</taken_at_timestamp>
                                <thumbnail_resources>
                                        <config_height>150</config_height>
                                        <config_width>150</config_width>
                                        <src>https://scontent-iad3-1.cdninstagram.com/vp/1ec5640a0a97e98127a1a04f1be62b6b/5A5F436E/t51.2885-15/s150x150/e15/c236.0.607.607/26158234_2061044554178629_8867446855789707264_n.jpg</src>
                                </thumbnail_resources>
                                <thumbnail_resources>
                                        <config_height>240</config_height>
                                        <config_width>240</config_width>
                                        <src>https://scontent-iad3-1.cdninstagram.com/vp/8c972cdacf536ea7bc6764279f3801b3/5A5EF038/t51.2885-15/s240x240/e15/c236.0.607.607/26158234_2061044554178629_8867446855789707264_n.jpg</src>
                                </thumbnail_resources>
                                <thumbnail_resources>
                                        <config_height>320</config_height>
                                        <config_width>320</config_width>
                                        <src>https://scontent-iad3-1.cdninstagram.com/vp/a74e8d0f933bffe75b28af3092f12769/5A5EFC3E/t51.2885-15/s320x320/e15/c236.0.607.607/26158234_2061044554178629_8867446855789707264_n.jpg</src>
                                </thumbnail_resources>
                                <thumbnail_resources>
                                        <config_height>480</config_height>
                                        <config_width>480</config_width>
                                        <src>https://scontent-iad3-1.cdninstagram.com/vp/59790fbcf0a358521f5eb81ec48de4a6/5A5F4F4D/t51.2885-15/s480x480/e15/c236.0.607.607/26158234_2061044554178629_8867446855789707264_n.jpg</src>
                                </thumbnail_resources>
                                <thumbnail_resources>
                                        <config_height>640</config_height>
                                        <config_width>640</config_width>
                                        <src>https://scontent-iad3-1.cdninstagram.com/vp/556243558c189f5dfff4081ecfdf06cc/5A5F43E1/t51.2885-15/e15/c236.0.607.607/26158234_2061044554178629_8867446855789707264_n.jpg</src>
                                </thumbnail_resources>
                                <thumbnail_src>https://scontent-iad3-1.cdninstagram.com/vp/556243558c189f5dfff4081ecfdf06cc/5A5F43E1/t51.2885-15/e15/c236.0.607.607/26158234_2061044554178629_8867446855789707264_n.jpg</thumbnail_src>
                                <video_view_count>2516274</video_view_count>
                            </node>
                        </edges>
                        ...
                        <page_info>
                                <end_cursor>AQAchf_lNcgUmnCZ0JTwqV_p3J0f-N21HeHzR2xplwxalNZDXg9tNmrBCzkegX1lN53ROI_HVoUZBPtdxZLuDyvUsYdNoLRb2-z6HMtJoTXRYQ</end_cursor>
                                <has_next_page>true</has_next_page>
                        </page_info>
                    </edge_owner_to_timeline_media>
                </user>
            </graphql>
        </profilepage>
    </entry_data>
    <rollout_hash>45ca3dc3d5fd</rollout_hash>
    <show_app_install>true</show_app_install>
    <zero_data></zero_data>
</body_safe>

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

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

---
config:
    agent: Firefox
    debug: 2
do:
# Загружаем начальную страницу
- walk:
    to: https://www.instagram.com/instagram/
    do:
    # Ищем элементы script которые подгружают JS
    - find:
        path: script[type="text/javascript"]
        do:
        # Парсим значение атрибута src
        - parse:
            attr: src
        # Проверяем, нужный ли это Javascript, нам нужен тот, у которого в URL есть строка ProfilePageContainer.js
        - if:
            match: ProfilePageContainer\.js
            do:
            # Переходим по URL скрипта
            - walk:
                to: value
                do:
                # Ищем элемент, содержащий искомый JS
                - find:
                    path: script
                    do:
                    # Парсим контент элемента, используя фильтр с регулярным выражением
                    - parse:
                        filter: profilePosts\.byUserId\.get[^,]+,queryId\:\&\s*quot\;([^&]+)\&\s*quot\;
                    # Сохраняем полученное значение в переменной
                    - variable_set: queryid
    # находим элемент script, который содержит текст window._sharedData
    - find:
        path: script:contains("window._sharedData")
        do:
        - parse
        - space_dedupe
        - trim
        # извлекаем JSON
        - filter: 
            args: window\._sharedData\s+\=\s+(.+)\s*;\s*$
        # конвертим JSON в XML
        - normalize:
            routine: json2xml
        # превращаем XML строку в DOM блок
        - to_block
        - find: 
            path: body_safe 
            do: 
        # Находим элемент в котором хранится id канала
        - find:
            path: entry_data > profilepage > graphql > user > id
            do:
            # Парсим содержимое элемента
            - parse
            # Сохраняем полученное значение в переменной
            - variable_set: chid
        # Находим элемент в котором хранится rhx_gis
        - find:
            path: rhx_gis
            do:
            # Парсим содержимое элемента
            - parse
            # Сохраняем полученное значение в переменной
            - variable_set: rhxgis
        # Находим элементы записей и итерируем по ним
        - find:
            path: entry_data > profilepage > graphql > user > edge_owner_to_timeline_media > edges > node
            do:
            # Создаем новый объект с именем item
            - object_new: item
            # Находим элемент с URL изображения
            - find:
                path: display_url
                do:
                # Парсим содержимое элемента
                - parse
                # Записываем значение в поле объекта item
                - object_field_set:
                    object: item
                    field: url
            # Находим элемент с описанием записи
            - find:
                path: edge_media_to_caption > edges > node > text
                do:
                # Парсим содержимое элемента
                - parse
                # Записываем значение в поле объекта item
                - object_field_set:
                    object: item
                    field: caption
            # Находим элемент с флагом видео это или нет
            - find:
                path: is_video
                do:
                # Парсим содержимое элемента
                - parse
                # Записываем значение в поле объекта item
                - object_field_set:
                    object: item
                    field: video
            # Находим элемент с количеством комментариев
            - find:
                path: edge_media_to_comment > count
                do:
                # Парсим содержимое элемента
                - parse
                # Записываем значение в поле объекта item
                - object_field_set:
                    object: item
                    field: comments
            # Находим элемент с количеством лайков
            - find:
                path: edge_media_preview_like > count
                do:
                # Парсим содержимое элемента
                - parse
                # Записываем значение в поле объекта item
                - object_field_set:
                    object: item
                    field: likes
            # записываем объект в базу
            - object_save:
                name: item
        # Находим элемент, в котором хранятся данные для подгрузки
        - find:
            path: entry_data > profilepage > graphql > user > edge_owner_to_timeline_media > page_info
            do:
            # Находим элемент, в котором хранятся данные о наличии следующей страницы
            - find:
                path: has_next_page
                do:
                # Парсим содержимое элемента
                - parse
                # Сохраняем значение в переменную
                - variable_set: hnp
            # Читаем содержимое переменной в регистр
            - variable_get: hnp
            # Проверяем равно ли значение 'true'
            - if:
                match: 'true'
                do:
                # Если да, то находим элемент с курсором
                - find:
                    path: end_cursor
                    do:
                    # Парсим содержимое элемента
                    - parse
                    # Сохраняем значение в переменную
                    - variable_set: cursor
                    # URL-энкодим параметр
                    - eval:
                        routine: js
                        body: '(function () {return encodeURIComponent("")})();'
                    # Сохраняем значение в переменную
                    - variable_set: cursor_encoded
                    # Формируем пул линков и добавляем в него URL на первую подгрузку
                    - link_add:
                        url: https://www.instagram.com/graphql/query/?query_hash=&variables=%7B%22id%22%3A%22%22%2C%22first%22%3A12%2C%22after%22%3A%22%22%7D
                    # Формируем подпись и записываем ее в переменную signature
                    - register_set: ':{"id":"","first":12,"after":""}'
                    - normalize:
                        routine: md5
                    - variable_set: signature
    # Устанавливаем счетчик подгрузок в 0
    - counter_set:
        name: pages
        value: 0
    # Итерируем по пулу и загружаем текущий линк и используем подпись в заголовках запроса
    - walk:
        to: links
        headers:
            x-instagram-gis: 
            x-requested-with: XMLHttpRequest
        do:

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

<html>

<head></head>

<body>
<body_safe>
    <activity_counts></activity_counts>
    <config>
        <csrf_token>qNVodzmebd0ZnAEOYxFCPpMV1XWGEaDz</csrf_token>
        <viewer></viewer>
    </config>
    <country_code>US</country_code>
    <display_properties_server_guess>
        <orientation></orientation>
        <pixel_ratio>1.5</pixel_ratio>
        <viewport_height>480</viewport_height>
        <viewport_width>360</viewport_width>
    </display_properties_server_guess>
    <entry_data>
        <profilepage>
            <logging_page_id>profilePage_25025320</logging_page_id>
            <graphql>
                <user>
                    <biography>Discovering — and telling — stories from around the world. Curated by Instagram’s community
                        team.</biography>
                    <blocked_by_viewer>false</blocked_by_viewer>
                    <connected_fb_page></connected_fb_page>
                    <country_block>false</country_block>
                    <external_url>http://blog.instagram.com/</external_url>
                    <external_url_linkshimmed>http://l.instagram.com/?u=http%3A%2F%2Fblog.instagram.com%2F&e=ATM_VrrL-_PjBU0WJ0OT_xPSlo-70w2PtE177ZsbPuLY9tmVs8JmIXfYgban04z423i2IL8M</external_url_linkshimmed>
                    <followed_by>
                            <count>230937095</count>
                    </followed_by>
                    <followed_by_viewer>false</followed_by_viewer>
                    <follows>
                            <count>197</count>
                    </follows>
                    <follows_viewer>false</follows_viewer>
                    <full_name>Instagram</full_name>
                    <has_blocked_viewer>false</has_blocked_viewer>
                    <has_requested_viewer>false</has_requested_viewer>
                    <id>25025320</id>
                    <is_private>false</is_private>
                    <is_verified>true</is_verified>
                    <edge_owner_to_timeline_media>
                        <count>5014</count>
                        <edges>
                            <node>
                                <safe___typename>GraphVideo</safe___typename>
                                <comments_disabled>false</comments_disabled>
                                <dimensions>
                                        <height>607</height>
                                        <width>1080</width>
                                </dimensions>
                                <display_url>https://scontent-iad3-1.cdninstagram.com/vp/9cdd0906e30590eed4ad793888595629/5A5F5679/t51.2885-15/s1080x1080/e15/fr/26158234_2061044554178629_8867446855789707264_n.jpg</display_url>
                                <edge_media_preview_like>
                                        <count>573448</count>
                                </edge_media_preview_like>
                                <edge_media_to_caption>
                                        <edges>
                                                <node>
                                                        <text>Video by @yanndixon Spontaneous by nature,
                                                                a flock of starlings swarm as one
                                                                at sunset in England. #WHPspontaneous</text>
                                                </node>
                                        </edges>
                                </edge_media_to_caption>
                                <edge_media_to_comment>
                                        <count>4709</count>
                                </edge_media_to_comment>
                                <id>1688175842423510712</id>
                                <is_video>true</is_video>
                                <owner>
                                        <id>25025320</id>
                                </owner>
                                <shortcode>Bdtmvv-DJa4</shortcode>
                                <taken_at_timestamp>1515466361</taken_at_timestamp>
                                <thumbnail_resources>
                                        <config_height>150</config_height>
                                        <config_width>150</config_width>
                                        <src>https://scontent-iad3-1.cdninstagram.com/vp/1ec5640a0a97e98127a1a04f1be62b6b/5A5F436E/t51.2885-15/s150x150/e15/c236.0.607.607/26158234_2061044554178629_8867446855789707264_n.jpg</src>
                                </thumbnail_resources>
                                <thumbnail_resources>
                                        <config_height>240</config_height>
                                        <config_width>240</config_width>
                                        <src>https://scontent-iad3-1.cdninstagram.com/vp/8c972cdacf536ea7bc6764279f3801b3/5A5EF038/t51.2885-15/s240x240/e15/c236.0.607.607/26158234_2061044554178629_8867446855789707264_n.jpg</src>
                                </thumbnail_resources>
                                <thumbnail_resources>
                                        <config_height>320</config_height>
                                        <config_width>320</config_width>
                                        <src>https://scontent-iad3-1.cdninstagram.com/vp/a74e8d0f933bffe75b28af3092f12769/5A5EFC3E/t51.2885-15/s320x320/e15/c236.0.607.607/26158234_2061044554178629_8867446855789707264_n.jpg</src>
                                </thumbnail_resources>
                                <thumbnail_resources>
                                        <config_height>480</config_height>
                                        <config_width>480</config_width>
                                        <src>https://scontent-iad3-1.cdninstagram.com/vp/59790fbcf0a358521f5eb81ec48de4a6/5A5F4F4D/t51.2885-15/s480x480/e15/c236.0.607.607/26158234_2061044554178629_8867446855789707264_n.jpg</src>
                                </thumbnail_resources>
                                <thumbnail_resources>
                                        <config_height>640</config_height>
                                        <config_width>640</config_width>
                                        <src>https://scontent-iad3-1.cdninstagram.com/vp/556243558c189f5dfff4081ecfdf06cc/5A5F43E1/t51.2885-15/e15/c236.0.607.607/26158234_2061044554178629_8867446855789707264_n.jpg</src>
                                </thumbnail_resources>
                                <thumbnail_src>https://scontent-iad3-1.cdninstagram.com/vp/556243558c189f5dfff4081ecfdf06cc/5A5F43E1/t51.2885-15/e15/c236.0.607.607/26158234_2061044554178629_8867446855789707264_n.jpg</thumbnail_src>
                                <video_view_count>2516274</video_view_count>
                            </node>
                        </edges>
                        ...
                        <page_info>
                                <end_cursor>AQAchf_lNcgUmnCZ0JTwqV_p3J0f-N21HeHzR2xplwxalNZDXg9tNmrBCzkegX1lN53ROI_HVoUZBPtdxZLuDyvUsYdNoLRb2-z6HMtJoTXRYQ</end_cursor>
                                <has_next_page>true</has_next_page>
                        </page_info>
                    </edge_owner_to_timeline_media>
                </user>
            </graphql>
        </profilepage>
    </entry_data>
    <rollout_hash>45ca3dc3d5fd</rollout_hash>
    <show_app_install>true</show_app_install>
    <zero_data></zero_data>
</body_safe>
</body>
</html>

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

---
config:
    agent: Firefox
    debug: 2
do:
# Загружаем начальную страницу
- walk:
    to: https://www.instagram.com/instagram/
    do:
    # Ищем элементы script которые подгружают JS
    - find:
        path: script[type="text/javascript"]
        do:
        # Парсим значение атрибута src
        - parse:
            attr: src
        # Проверяем, нужный ли это Javascript, нам нужен тот, у которого в URL есть строка ProfilePageContainer.js
        - if:
            match: ProfilePageContainer\.js
            do:
            # Переходим по URL скрипта
            - walk:
                to: value
                do:
                # Ищем элемент, содержащий искомый JS
                - find:
                    path: script
                    do:
                    # Парсим контент элемента, используя фильтр с регулярным выражением
                    - parse:
                        filter: profilePosts\.byUserId\.get[^,]+,queryId\:\&\s*quot\;([^&]+)\&\s*quot\;
                    # Сохраняем полученное значение в переменной
                    - variable_set: queryid
    # находим элемент script, который содержит текст window._sharedData
    - find:
        path: script:contains("window._sharedData")
        do:
        - parse
        - space_dedupe
        - trim
        # извлекаем JSON
        - filter: 
            args: window\._sharedData\s+\=\s+(.+)\s*;\s*$
        # конвертим JSON в XML
        - normalize:
            routine: json2xml
        # превращаем XML строку в DOM блок
        - to_block
        - find: 
            path: body_safe 
            do: 
        # Находим элемент в котором хранится id канала
        - find:
            path: entry_data > profilepage > graphql > user > id
            do:
            # Парсим содержимое элемента
            - parse
            # Сохраняем полученное значение в переменной
            - variable_set: chid
        # Находим элемент в котором хранится rhx_gis
        - find:
            path: rhx_gis
            do:
            # Парсим содержимое элемента
            - parse
            # Сохраняем полученное значение в переменной
            - variable_set: rhxgis
        # Находим элементы записей и итерируем по ним
        - find:
            path: entry_data > profilepage > graphql > user > edge_owner_to_timeline_media > edges > node
            do:
            # Создаем новый объект с именем item
            - object_new: item
            # Находим элемент с URL изображения
            - find:
                path: display_url
                do:
                # Парсим содержимое элемента
                - parse
                # Записываем значение в поле объекта item
                - object_field_set:
                    object: item
                    field: url
            # Находим элемент с описанием записи
            - find:
                path: edge_media_to_caption > edges > node > text
                do:
                # Парсим содержимое элемента
                - parse
                # Записываем значение в поле объекта item
                - object_field_set:
                    object: item
                    field: caption
            # Находим элемент с флагом видео это или нет
            - find:
                path: is_video
                do:
                # Парсим содержимое элемента
                - parse
                # Записываем значение в поле объекта item
                - object_field_set:
                    object: item
                    field: video
            # Находим элемент с количеством комментариев
            - find:
                path: edge_media_to_comment > count
                do:
                # Парсим содержимое элемента
                - parse
                # Записываем значение в поле объекта item
                - object_field_set:
                    object: item
                    field: comments
            # Находим элемент с количеством лайков
            - find:
                path: edge_media_preview_like > count
                do:
                # Парсим содержимое элемента
                - parse
                # Записываем значение в поле объекта item
                - object_field_set:
                    object: item
                    field: likes
            # записываем объект в базу
            - object_save:
                name: item
        # Находим элемент, в котором хранятся данные для подгрузки
        - find:
            path: entry_data > profilepage > graphql > user > edge_owner_to_timeline_media > page_info
            do:
            # Находим элемент, в котором хранятся данные о наличии следующей страницы
            - find:
                path: has_next_page
                do:
                # Парсим содержимое элемента
                - parse
                # Сохраняем значение в переменную
                - variable_set: hnp
            # Читаем содержимое переменной в регистр
            - variable_get: hnp
            # Проверяем равно ли значение 'true'
            - if:
                match: 'true'
                do:
                # Если да, то находим элемент с курсором
                - find:
                    path: end_cursor
                    do:
                    # Парсим содержимое элемента
                    - parse
                    # Сохраняем значение в переменную
                    - variable_set: cursor
                    # URL-энкодим параметр
                    - eval:
                        routine: js
                        body: '(function () {return encodeURIComponent("")})();'
                    # Сохраняем значение в переменную
                    - variable_set: cursor_encoded
                    # Формируем пул линков и добавляем в него URL на первую подгрузку
                    - link_add:
                        url: https://www.instagram.com/graphql/query/?query_hash=&variables=%7B%22id%22%3A%22%22%2C%22first%22%3A12%2C%22after%22%3A%22%22%7D
                    # Формируем подпись и записываем ее в переменную signature
                    - register_set: ':{"id":"","first":12,"after":""}'
                    - normalize:
                        routine: md5
                    - variable_set: signature
    # Устанавливаем счетчик подгрузок в 0
    - counter_set:
        name: pages
        value: 0
    # Итерируем по пулу и загружаем текущий линк и используем подпись в заголовках запроса
    - walk:
        to: links
        headers:
            x-instagram-gis: 
            x-requested-with: XMLHttpRequest
        do:
        - sleep: 3
        # Находим элемент, в котором хранятся данные для подгрузки
        - find:
            path: edge_owner_to_timeline_media > page_info
            do:
            # Находим элемент, в котором хранятся данные о наличии следующей страницы
            - find:
                path: has_next_page
                do:
                # Парсим содержимое элемента
                - parse
                # Сохраняем значение в переменную
                - variable_set: hnp
            # Читаем содержимое переменной в регистр
            - variable_get: hnp
            # Проверяем равно ли значение 'true'
            - if:
                match: 'true'
                do:
                # Если да, то проверяем счетчик подгрузок, больше ли он 10
                - counter_get: pages
                - if:
                    type: int
                    gt: 10
                    else:
                    # Если нет, то находим элемент с курсором
                    - find:
                        path: end_cursor
                        do:
                        # Парсим содержимое элемента
                        - parse
                        # Сохраняем значение в переменную
                        - variable_set: cursor
                        # URL-энкодим параметр
                        - eval:
                            routine: js
                            body: '(function () {return encodeURIComponent("")})();'
                        # Сохраняем значение в переменную
                        - variable_set: cursor_encoded
                        # Формируем пул линков и добавляем в него URL следующей подгрузки
                        - link_add:
                            url: https://www.instagram.com/graphql/query/?query_hash=&variables=%7B%22id%22%3A%22%22%2C%22first%22%3A12%2C%22after%22%3A%22%22%7D
                        # Формируем подпись и записываем ее в переменную signature
                        - register_set: ':{"id":"","first":12,"after":""}'
                        - normalize:
                            routine: md5
                        - variable_set: signature
        # Находим элементы записей и итерируем по ним
        - find:
            path: edge_owner_to_timeline_media > edges > node
            do:
            # Создаем новый объект с именем item
            - object_new: item
            # Находим элемент с URL изображения
            - find:
                path: display_url
                do:
                # Парсим содержимое элемента
                - parse
                # Записываем значение в поле объекта item
                - object_field_set:
                    object: item
                    field: url
            # Находим элемент с описанием записи
            - find:
                path: edge_media_to_caption > edges > node > text
                do:
                # Парсим содержимое элемента
                - parse
                # Записываем значение в поле объекта item
                - object_field_set:
                    object: item
                    field: caption
            # Находим элемент с флагом видео это или нет
            - find:
                path: is_video
                do:
                # Парсим содержимое элемента
                - parse
                # Записываем значение в поле объекта item
                - object_field_set:
                    object: item
                    field: video
            # Находим элемент с количеством комментариев
            - find:
                path: edge_media_to_comment > count
                do:
                # Парсим содержимое элемента
                - parse
                # Записываем значение в поле объекта item
                - object_field_set:
                    object: item
                    field: comments
            # Находим элемент с количеством лайков
            - find:
                path: edge_media_preview_like > count
                do:
                # Парсим содержимое элемента
                - parse
                # Записываем значение в поле объекта item
                - object_field_set:
                    object: item
                    field: likes
            # записываем объект в базу
            - object_save:
                name: item
        # Увеличим счетчик подгрузок на 1
        - counter_increment:
            name: pages
            by: 1

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

[{
    "item": {
        "caption": "Photo by @williamk\nOut with the old, in with the new. ? #TheWeekOnInstagram",
        "comments": "5073",
        "likes": "571325",
        "url": "https://scontent-sjc3-1.cdninstagram.com/vp/d064d34902bbaba17456da7043307001/5ADF67CC/t51.2885-15/e35/26066810_1561269847323081_4659907128088068096_n.jpg",
        "video": "false"
    }
}
,{
    "item": {
        "caption": "Photo by @thatbloom\n“I waited. I waited a lot,” says Roland Kraemer (@thatbloom), who knelt patiently in the snow to capture this moment. “I was inspired by the fact that you don’t necessarily have to travel far to take good photos. This was taken almost in my backyard.” #TheWeekOnInstagram",
        "comments": "10468",
        "likes": "1235401",
        "url": "https://scontent-sjc3-1.cdninstagram.com/vp/4782daed87f1da6d3f22f6d02e2730fa/5AF16142/t51.2885-15/e35/26152364_141930706473591_386722995680313344_n.jpg",
        "video": "false"
    }
}
,{
    "item": {
        "caption": "Photo by @tiagoovarjao\nLate afternoon light, good friends and the ocean. ? #TheWeekOnInstagram",
        "comments": "4280",
        "likes": "708045",
        "url": "https://scontent-sjc3-1.cdninstagram.com/vp/8f95ddb0d51ff26a11fa19df3d22d51a/5AEDD312/t51.2885-15/e35/26225106_1942276889134646_4232956111503753216_n.jpg",
        "video": "false"
    }
}]

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

---
config:
    agent: Firefox
    debug: 2
do:
# Инициальзируем переменную в которую записываем hashtag
- variable_set:
    field: tag
    value: beard
# Загружаем начальную страницу
- walk:
    to: https://www.instagram.com/explore/tags/
    do:
    # Ищем элементы script которые подгружают JS
    - find:
        path: script[type="text/javascript"]
        do:
        # Парсим значение атрибута src
        - parse:
            attr: src
        # Проверяем, нужный ли это Javascript, нам нужен тот, у которого в URL есть строка Consumer.js
        - if:
            match: Consumer\.js
            do:
            # Переходим по URL скрипта
            - walk:
                to: value
                do:
                # Ищем элемент, содержащий искомый JS
                - find:
                    path: script
                    do:
                    # Парсим контент элемента, используя фильтр с регулярным выражением
                    - parse:
                        filter: T\.pagination\},queryId\:\&\s*quot\;([^&]+)\&\s*quot\;
                    # Сохраняем полученное значение в переменной
                    - variable_set: queryid
    # находим элемент script, который содержит текст window._sharedData
    - find:
        path: script:contains("window._sharedData")
        do:
        - parse
        - space_dedupe
        - trim
        # извлекаем JSON
        - filter: 
            args: window\._sharedData\s+\=\s+(.+)\s*;\s*$
        # конвертим JSON в XML
        - normalize:
            routine: json2xml
        # превращаем XML строку в DOM блок
        - to_block
        - find: 
            path: body_safe 
            do: 
        # Находим элемент в котором хранится rhx_gis
        - find:
            path: rhx_gis
            do:
            # Парсим содержимое элемента
            - parse
            # Сохраняем полученное значение в переменной
            - variable_set: rhxgis
        # Находим элементы записей и итерируем по ним
        - find:
            path: entry_data > tagpage > graphql > hashtag > edge_hashtag_to_media > edges > node
            do:
            # Создаем новый объект с именем item
            - object_new: item
            # Находим элемент с URL изображения
            - find:
                path: display_url
                do:
                # Парсим содержимое элемента
                - parse
                # Записываем значение в поле объекта item
                - object_field_set:
                    object: item
                    field: url
            # Находим элемент с описанием записи
            - find:
                path: edge_media_to_caption > edges > node > text
                do:
                # Парсим содержимое элемента
                - parse
                # Записываем значение в поле объекта item
                - object_field_set:
                    object: item
                    field: caption
            # Находим элемент с флагом видео это или нет
            - find:
                path: is_video
                do:
                # Парсим содержимое элемента
                - parse
                # Записываем значение в поле объекта item
                - object_field_set:
                    object: item
                    field: video
            # Находим элемент с количеством комментариев
            - find:
                path: edge_media_to_comment > count
                do:
                # Парсим содержимое элемента
                - parse
                # Записываем значение в поле объекта item
                - object_field_set:
                    object: item
                    field: comments
            # Находим элемент с количеством лайков
            - find:
                path: edge_media_preview_like > count
                do:
                # Парсим содержимое элемента
                - parse
                # Записываем значение в поле объекта item
                - object_field_set:
                    object: item
                    field: likes
            # записываем объект в базу
            - object_save:
                name: item
        # Находим элемент, в котором хранятся данные для подгрузки
        - find:
            path: entry_data > tagpage > graphql > hashtag > edge_hashtag_to_media > page_info
            do:
            # Находим элемент, в котором хранятся данные о наличии следующей страницы
            - find:
                path: has_next_page
                do:
                # Парсим содержимое элемента
                - parse
                # Сохраняем значение в переменную
                - variable_set: hnp
            # Читаем содержимое переменной в регистр
            - variable_get: hnp
            # Проверяем равно ли значение 'true'
            - if:
                match: 'true'
                do:
                # Если да, то находим элемент с курсором
                - find:
                    path: end_cursor
                    do:
                    # Парсим содержимое элемента
                    - parse
                    # Сохраняем значение в переменную
                    - variable_set: cursor
                    # URL-энкодим параметр
                    - eval:
                        routine: js
                        body: '(function () {return encodeURIComponent("")})();'
                    # Сохраняем значение в переменную
                    - variable_set: cursor_encoded
                    # Формируем пул линков и добавляем в него URL на первую подгрузку
                    - link_add:
                        url: https://www.instagram.com/graphql/query/?query_hash=&variables=%7B%22tag_name%22%3A%22%22%2C%22first%22%3A12%2C%22after%22%3A%22%22%7D
                    # Формируем подпись и записываем ее в переменную signature
                    - register_set: ':{"tag_name":"","first":12,"after":""}'
                    - normalize:
                        routine: md5
                    - variable_set: signature
    # Устанавливаем счетчик подгрузок в 0
    - counter_set:
        name: pages
        value: 0
    # Итерируем по пулу и загружаем текущий линк и используем подпись в заголовках запроса
    - walk:
        to: links
        headers:
            x-instagram-gis: 
            x-requested-with: XMLHttpRequest
        do:
        - sleep: 3
        # Находим элемент, в котором хранятся данные для подгрузки
        - find:
            path: edge_hashtag_to_media > page_info
            do:
            # Находим элемент, в котором хранятся данные о наличии следующей страницы
            - find:
                path: has_next_page
                do:
                # Парсим содержимое элемента
                - parse
                # Сохраняем значение в переменную
                - variable_set: hnp
            # Читаем содержимое переменной в регистр
            - variable_get: hnp
            # Проверяем равно ли значение 'true'
            - if:
                match: 'true'
                do:
                # Если да, то проверяем счетчик подгрузок, больше ли он 10
                - counter_get: pages
                - if:
                    type: int
                    gt: 10
                    else:
                    # Если нет, то находим элемент с курсором
                    - find:
                        path: end_cursor
                        do:
                        # Парсим содержимое элемента
                        - parse
                        # Сохраняем значение в переменную
                        - variable_set: cursor
                        # URL-энкодим параметр
                        - eval:
                            routine: js
                            body: '(function () {return encodeURIComponent("")})();'
                        # Сохраняем значение в переменную
                        - variable_set: cursor_encoded
                        # Формируем пул линков и добавляем в него URL следующей подгрузки
                        - link_add:
                            url: https://www.instagram.com/graphql/query/?query_hash=&variables=%7B%22tag_name%22%3A%22%22%2C%22first%22%3A12%2C%22after%22%3A%22%22%7D
                        # Формируем подпись и записываем ее в переменную signature
                        - register_set: ':{"tag_name":"","first":12,"after":""}'
                        - normalize:
                            routine: md5
                        - variable_set: signature
        # Находим элементы записей и итерируем по ним
        - find:
            path: edge_hashtag_to_media > edges > node
            do:
            # Создаем новый объект с именем item
            - object_new: item
            # Находим элемент с URL изображения
            - find:
                path: display_url
                do:
                # Парсим содержимое элемента
                - parse
                # Записываем значение в поле объекта item
                - object_field_set:
                    object: item
                    field: url
            # Находим элемент с описанием записи
            - find:
                path: edge_media_to_caption > edges > node > text
                do:
                # Парсим содержимое элемента
                - parse
                # Записываем значение в поле объекта item
                - object_field_set:
                    object: item
                    field: caption
            # Находим элемент с флагом видео это или нет
            - find:
                path: is_video
                do:
                # Парсим содержимое элемента
                - parse
                # Записываем значение в поле объекта item
                - object_field_set:
                    object: item
                    field: video
            # Находим элемент с количеством комментариев
            - find:
                path: edge_media_to_comment > count
                do:
                # Парсим содержимое элемента
                - parse
                # Записываем значение в поле объекта item
                - object_field_set:
                    object: item
                    field: comments
            # Находим элемент с количеством лайков
            - find:
                path: edge_media_preview_like > count
                do:
                # Парсим содержимое элемента
                - parse
                # Записываем значение в поле объекта item
                - object_field_set:
                    object: item
                    field: likes
            # записываем объект в базу
            - object_save:
                name: item
        # Увеличим счетчик подгрузок на 1
        - counter_increment:
            name: pages
            by: 1
Михаил Сисин: Со-основатель облачного сервиса по сбору информации и парсингу сайтов Diggernaut. Работает в области сбора и анализа данных, а также разработки систем искусственного интеллекта и машинного обучения  более десяти лет.
Related Post

This website uses cookies.