Если для вашей работы вам было нужно собирать данные с корпоративных профилей Инстаграм, вы наверняка использовали для этого мобильное приложение, поскольку в веб версии корпоративные данные отсутствовали. В частности, было невозможно определить корпоративный это профиль или персональный. Теперь появилась возможность обрабатывать их автоматически парсером, использующим мобильное API. Мы нашли его на просторах интернета, один из наших пользователей написал его и поделился с общественностью на одном из известных маркетинговых интернет ресурсах. Давайте разберем, как этот парсер работает.
Для использования данного парсера требуется указать логин для вашего аккаунта в инcтаграм, пароль для него, а также список аккаунтов, о которых вы хотите собрать корпоративную информацию. Примите во внимание, что Инстаграм может заблокировать ваш аккаунт инстаграм если используя этот парсер вы нарушите условия использования сервиса, поэтому используйте его на свой страх и риск. Публикуя код этого парсера мы хотим помочь нашим пользователям овладеть мета языком для парсинга, поэтому мы постоянно рассматриваем различные кейсы из реальной жизни и разбираем код парсеров по полочкам. Вот собственно код парсера:
---
config:
agent: Firefox
debug: 2
do:
- variable_set:
field: username
value: ИМЯ ВАШЕГО АККАУНТА В ИНСТАГРАМ
- variable_set:
field: password
value: ПАРОЛЬ ДЛЯ ВАШЕГО АККАУНТА В ИНСТАГРАМ
- variable_set:
field: accounts
value: СПИСОК АККАУНТОВ ИНСТАГРАМ, РАЗДЕЛЕННЫХ ЗАПЯТОЙ, ДЛЯ КОТОРЫХ НУЖНО ИЗВЛЕЧЬ БИЗНЕС ДАННЫЕ
- walk:
to: https://www.instagram.com/
do:
- find:
path: body
do:
- parse:
filter: window\._sharedData\s+\=\s+([^;]+);
- normalize:
routine: json2xml
- to_block
- find:
path: config>csrf_token
do:
- parse
- variable_set: token
- walk:
to:
post: https://www.instagram.com/accounts/login/ajax/
headers:
x-csrftoken: <%token%>
x-instagram-ajax: 1
x-requested-with: XMLHttpRequest
data:
username: <%username%>
password: <%password%>
do:
- find:
path: status
do:
- parse
- if:
match: "fail"
do:
- cannot_login_probably_checkpoint_is_required
- exit
- find:
path: authenticated
do:
- parse
- if:
match: "true"
else:
- wrong_login_or_password
- exit
- cookie_get: mid
- variable_set: mid
- cookie_get: rur
- variable_set: rur
- cookie_get: ds_user_id
- variable_set: dsuserid
- cookie_get: sessionid
- variable_set: sessionid
- variable_get: accounts
- to_block
- split:
context: text
delimiter: ','
- find:
path: div.splitted
do:
- parse
- space_dedupe
- trim
- variable_set: account
- walk:
to: https://www.instagram.com/<%account%>/
do:
- find:
path: script:contains("window._sharedData")
do:
- parse
- space_dedupe
- trim
- filter:
args:
- window\._sharedData\s+\=\s+(.+)\s*;\s*$
- normalize:
routine: json2xml
- to_block
- find:
path: body_safe
do:
- find:
path: entry_data > profilepage > graphql > user > id
do:
- parse
- variable_set: id
- walk:
to: https://i.instagram.com/api/v1/users/<%id%>/info/
headers:
X-IG-App-ID: 567067343352427
X-IG-Capabilities: 3brDAw==
X-IG-Connection-Type: WIFI
X-IG-Connection-Speed: 3400
X-IG-Bandwidth-Speed-KBPS: -1.000
X-IG-Bandwidth-TotalBytes-B: 0
X-IG-Bandwidth-TotalTime-MS: 0
Cookie: mid=<%mid%>; csrftoken=<%token%>; rur=<%rur%>; ds_user_id=<%dsuserid%>; sessionid=<%sessionid%>; ig_or=;
X-FB-HTTP-Engine: Liger
Accept: '*/*'
Accept-Language: en-US
do:
- find:
path: body_safe > user
do:
- object_new: item
- find:
path: address_street
do:
- parse
- space_dedupe
- trim
- object_field_set:
object: item
field: address_street
- find:
path: category
do:
- parse
- space_dedupe
- trim
- object_field_set:
object: item
field: category
- find:
path: city_name
do:
- parse
- space_dedupe
- trim
- object_field_set:
object: item
field: city_name
- find:
path: contact_phone_number
do:
- parse
- space_dedupe
- trim
- object_field_set:
object: item
field: contact_phone_number
- find:
path: external_url
do:
- parse
- space_dedupe
- trim
- object_field_set:
object: item
field: external_url
- find:
path: full_name
do:
- parse
- space_dedupe
- trim
- object_field_set:
object: item
field: full_name
- find:
path: is_business
do:
- parse
- space_dedupe
- trim
- object_field_set:
object: item
field: is_business
- find:
path: latitude
do:
- parse
- space_dedupe
- trim
- object_field_set:
object: item
field: latitude
- find:
path: longitude
do:
- parse
- space_dedupe
- trim
- object_field_set:
object: item
field: longitude
- find:
path: pk
do:
- parse
- space_dedupe
- trim
- object_field_set:
object: item
field: id
- find:
path: public_email
do:
- parse
- space_dedupe
- trim
- object_field_set:
object: item
field: public_email
- find:
path: public_phone_country_code
do:
- parse
- space_dedupe
- trim
- object_field_set:
object: item
field: public_phone_country_code
- find:
path: public_phone_number
do:
- parse
- space_dedupe
- trim
- object_field_set:
object: item
field: public_phone_number
- find:
path: username
do:
- parse
- space_dedupe
- trim
- object_field_set:
object: item
field: username
- find:
path: zip
do:
- parse
- space_dedupe
- trim
- object_field_set:
object: item
field: zip
- object_save:
name: item
- sleep: 5
Как вы уже наверное знаете, секция config предназначена для предустановки парсера, в данном случае для установки уровня режима отладки (который в принципе нужен только для разработки и его можно было бы опустить) и имени браузера от лица которого парсер будет отправлять запросы на сервер. Технически здесь мог бы быть Chrome или Safari, но автор решил что должен быть Firefox. К слову сказать, иногда сервера могут отдавать разные данные, в зависимости от выставленного имени браузера. Также, иногда может потребоваться использовать не пресет, а полную строку User-Agent, их можно найти тут.
config:
agent: Firefox
debug: 2
Основной логический блок парсера находится в секции do. В самом начале происходит инициализация переменных вашим логином, паролем для аккаунта инстаграм и списком аккаунтов:
- variable_set:
field: username
value: ИМЯ ВАШЕГО АККАУНТА В ИНСТАГРАМ
- variable_set:
field: password
value: ПАРОЛЬ ДЛЯ ВАШЕГО АККАУНТА В ИНСТАГРАМ
- variable_set:
field: accounts
value: СПИСОК АККАУНТОВ ИНСТАГРАМ, РАЗДЕЛЕННЫХ ЗАПЯТОЙ, ДЛЯ КОТОРЫХ НУЖНО ИЗВЛЕЧЬ БИЗНЕС ДАННЫЕ
Далее парсер загружает домашнюю страницу Инстаграм и проходит в в тег body
- walk:
to: https://www.instagram.com/
do:
- find:
path: body
do:
Парсит весь текст и извлекает объект Javascript, транслирует его в XML и превращает в DOM блок, переходя в его контекст.
- parse:
filter: window\._sharedData\s+\=\s+([^;]+);
- normalize:
routine: json2xml
- to_block
Сейчас в нашем контексте находится извлеченный объект Javascript (JSON) как DOM и мы можем ходить по его элементам, как если бы это была обычная HTML страница. Поэтому мы находим ноду config а в ней ноду csrf_token, парсим содержимое и извлекаем токен, который нам нужен для логина в инстаграм и записываем его в переменную token. После чего логинимся в Инстаграм, используя токен, логин и пароль, которые мы храним в переменных:
- find:
path: config>csrf_token
do:
- parse
- variable_set: token
- walk:
to:
post: https://www.instagram.com/accounts/login/ajax/
headers:
x-csrftoken: <%token%>
x-instagram-ajax: 1
x-requested-with: XMLHttpRequest
data:
username: <%username%>
password: <%password%>
Далее парсер проверяет, авторизовал ли нас Инстаграм
- find:
path: status
do:
- parse
- if:
match: "fail"
do:
- cannot_login_probably_checkpoint_is_required
- exit
И если нет, выводится ошибка и парсер заканчивает работу. Если вы видите в логе эту ошибку, попробуйте залогиниться в аккаунт через браузер и решить вручную челлендж. После этого вы сможете войти в аккаунт парсером. Если же авторизация прошла успешно, парсер продолжит работу и перенесет необходимые куки в переменные для возможности использования их в запросах:
- find:
path: authenticated
do:
- parse
- if:
match: "true"
else:
- wrong_login_or_password
- exit
- cookie_get: mid
- variable_set: mid
- cookie_get: rur
- variable_set: rur
- cookie_get: ds_user_id
- variable_set: dsuserid
- cookie_get: sessionid
- variable_set: sessionid
Затем парсер считывает переменную со списком аккаунтов, которые нужно забрать, в регистр и переводит текст в регистре в блок. Это делается для того, чтобы использовать команду split, так как команда работает с содержимимым блока, а не регистра. После разбивки, парсер итерирует по каждому аккаунту и выполняет команды в блоке do:
- split:
context: text
delimiter: ','
- find:
path: div.splitted
do:
Все что происходит далее, применяется к каждому переданному в списке аккаунту. Парсер парсит содержимое блока, в котором находится имя аккаунта, очищает его от лишних пробелов и записывает в переменную для использования в запросах.
- parse
- space_dedupe
- trim
- variable_set: account
Парсер забирает страницу канала для того, чтобы извлечь ID канала, поскольку именно ID используется для запроса мобильного API. ID сохраняется в переменной.
- walk:
to: https://www.instagram.com/<%account%>/
do:
- find:
path: script:contains("window._sharedData")
do:
- parse
- space_dedupe
- trim
- filter:
args:
- window\._sharedData\s+\=\s+(.+)\s*;\s*$
- normalize:
routine: json2xml
- to_block
- find:
path: body_safe
do:
- find:
path: entry_data > profilepage > graphql > user > id
do:
- parse
- variable_set: id
После чего идет запрос на мобильный API. Как мы видим, парсер маскируется под мобильное приложения, используя определенные заголовки запроса.
- walk:
to: https://i.instagram.com/api/v1/users/<%id%>/info/
headers:
X-IG-App-ID: 567067343352427
X-IG-Capabilities: 3brDAw==
X-IG-Connection-Type: WIFI
X-IG-Connection-Speed: 3400
X-IG-Bandwidth-Speed-KBPS: -1.000
X-IG-Bandwidth-TotalBytes-B: 0
X-IG-Bandwidth-TotalTime-MS: 0
Cookie: mid=<%mid%>; csrftoken=<%token%>; rur=<%rur%>; ds_user_id=<%dsuserid%>; sessionid=<%sessionid%>; ig_or=;
X-FB-HTTP-Engine: Liger
Accept: '*/*'
Accept-Language: en-US
do:
Мобильный API возвращает ответ в JSON. Диггернаут автоматически конвертирует его в XML и даем возможность работать вам с DOM структурой, используя стандартную команду find. Так что весь дальнейший код просто забирает данные используя определенные CSS селекторы и сохраняет их в структуру данных.
- object_new: item
- find:
path: address_street
do:
- parse
- space_dedupe
- trim
- object_field_set:
object: item
field: address_street
- find:
path: category
do:
- parse
- space_dedupe
- trim
- object_field_set:
object: item
field: category
- find:
path: city_name
do:
- parse
- space_dedupe
- trim
- object_field_set:
object: item
field: city_name
- find:
path: contact_phone_number
do:
- parse
- space_dedupe
- trim
- object_field_set:
object: item
field: contact_phone_number
- find:
path: external_url
do:
- parse
- space_dedupe
- trim
- object_field_set:
object: item
field: external_url
- find:
path: full_name
do:
- parse
- space_dedupe
- trim
- object_field_set:
object: item
field: full_name
- find:
path: is_business
do:
- parse
- space_dedupe
- trim
- object_field_set:
object: item
field: is_business
- find:
path: latitude
do:
- parse
- space_dedupe
- trim
- object_field_set:
object: item
field: latitude
- find:
path: longitude
do:
- parse
- space_dedupe
- trim
- object_field_set:
object: item
field: longitude
- find:
path: pk
do:
- parse
- space_dedupe
- trim
- object_field_set:
object: item
field: id
- find:
path: public_email
do:
- parse
- space_dedupe
- trim
- object_field_set:
object: item
field: public_email
- find:
path: public_phone_country_code
do:
- parse
- space_dedupe
- trim
- object_field_set:
object: item
field: public_phone_country_code
- find:
path: public_phone_number
do:
- parse
- space_dedupe
- trim
- object_field_set:
object: item
field: public_phone_number
- find:
path: username
do:
- parse
- space_dedupe
- trim
- object_field_set:
object: item
field: username
- find:
path: zip
do:
- parse
- space_dedupe
- trim
- object_field_set:
object: item
field: zip
- object_save:
name: item
В целом логика работы парсера проста, единственный сложный момент, это процесс маскировки под мобильное приложение. Пример полученных данных приведен ниже:
{
item : {
category : "Product/Service",
username : "adidas",
is_business : "true",
contact_phone_number : "",
zip : "91074",
public_phone_number : "",
longitude : "10.9094251",
latitude : "49.5831932",
public_phone_country_code : "",
full_name : "adidas",
city_name : "Herzogenaurach",
address_street : "Adi-Dassler-Str. 1",
id : "20269764",
public_email : "",
external_url : "http://a.did.as/BuiltToDefy"
}
}