October 21, 2019

......

План статьи

  1. Введение
  2. Термины, которые будут использоваться в статье
  3. Подготовительная часть

  4. Создания скрипта без API
    Декомпиляция сетевого трафика
    Создание схемы скрипта и его реализация
    Тестирование и объяснение отдельных функций


  5. Создание скрипта с использованием API
    Ознакомление с API и библиотек для него
    Создание схемы и реализация скрипта
    Тестирование и объяснение отдельных функций

  6. Заключение

Введение

Еще с 1941 года Американское правительство обширно использовало OSINT. Уже в 1947 году Шерман Кент, аналитик ЦРУ, сообщил, что около 80% информации, которая используется политиками для принятия решений, берется из открытых источников. (

источник

, с. 428, конец 2 параграфа)

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

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

Реализовать скрипт можно двумя способами: используя API (более простой и удобный путь) и написание без API, используя только запросы (долгий и трудоемкий процесс).

Каждый перечисленный способ имеет свои недостатки и преимущества, но обо всем по порядку.

Термины

В этом пункте вы можете ознакомиться со всеми непонятными терминами, которые используются в статье.

  • API (application programming interface) – это набор различных функции, протоколов, определители подпрограмм, для создания приложений. Крайне удобный и незаменимый инструмент при работе с различными онлайн-сервисами и софтами.
  • URL encoding - кодирует ссылку при передачи данных по протоколу HTTP. Закодированная ссылка выглядит так: «google.com%2F%3F%7B%221%22%3A%5B%221%22%2C2%5D%7D», декодированная так: «google.com/?{"1":["1",2]}»
  • JSON Fromater (JSON Beautifier) – программа, которая преобразует однострочный код, в удобный и читаемый вид.

Подготовительная часть

Для написания скриптов я буду использовать Python, так как он удобен при работе с запросами, обработке ответов от серверов, и быстро справляется с массивами данных. Если конкретнее, то я буду использовать Python версии 3.7.3.

Создания скрипта без

API Декомпиляция сетевого трафика

Скрипт мы будем писать под Web версию Инстаграма, и наш скрипт не будет требовать ни единой информации от юзера, кроме нужного никнейма для скана.

Начать стоит с того, что, не имея под руками ключей для использования API, выкручиваться нужно только декомпиляцией трафика. Проанализировав GET запросы, я заметил, что для подзагрузки фотографий, запрос отправляется на

https://www.instagram.com/graphql/query/

:

Рисунок 1: разбор GET запроса

Перейдя по ссылке, я понял, что при валидном запросе, сервер выдаст ответ в формате json:

Рисунок 2: сервер ответил на запрос “Instagram.com/graphql/query ” ошибкой

Использовав один из ранних GET запросов:

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

я получил такой ответ:

Рисунок 3: ответ сервера на GET запрос о подзагрузке доп. фото

Где под номером подразумевается

node

, дерево, которое вмещает в себе всю информацию о посте (будто видео или фото).

Прогнав 2 подобных запроса через URL Decoder, я получил следующее:

Код:

https://www.instagram.com/graphql/query/?query_hash=f2405b236d85e8296cf30347c9f08c2a&variables={"id":"24025320","first":12,"after":"QVFBakZHYzBMN1pNeld5SWhJTUpNbkZzSGhoVWdUMk5WYjBQV0E1WmFyNmNqTDM2bmlmS3lhdWNzN21mOVJVR3RnUDQzRGxOVG83VHRPakpvSm5WbUhMYw=="}

https://www.instagram.com/graphql/query/?query_hash=f2405b236d85e8296cf30347c9f08c2a&variables={"id":"24025320","first":12,"after":"QVFEV2NTam5oekdON2hjRlluLXZHLXhORnpRczdNQmV5LTB4ZTFjM2E4NnZIM25QRHdQOGg2Q2Zrc1BtOXp3VjVxdHNMR2FoWGVMemJMS2VlWHlJX0dLcA=="}

Из этого стало понятно, что параметр

id

- вероятнее всего обозначает

id

страницы, для которой нужна подзагрузка. Параметр

first

– обозначает количество до загружаемых фото, но максимальное количество фотографий, которое можно дозагрузить – 50 штук.

В параметре

after

передается строка, которая существует при условии

True

у параметра

has_next_page

:

Рисунок 4: если “has_next_page”:False, то значения end_cursor нету Рисунок 5: если “has_next_page”:True, то значения end_cursor есть

Но остается вопрос: “Где берется самый первый параметр after?”

Покопавшись в .html странице Инстаграма, я заметил, что в строках 277 и 274 данные записаны в похожем стиле json:

Рисунок 6

Скопировав строки и прогнав их через JSON Formater, я получил с одной строки ~2400 строк, а с другой ~250, и не содержит в себе полезной информации.

Информация об аккаунте содержится в подкаталоге

graphql

:

Рисунок 7: сопоставление данных со строки 274 с данными на странице.

Мне показалось странным называть так подкаталог, и поэтому я решил обратиться к поисковикам.

Самой первой страницей, которую выдал Google, был вопрос на

satckoverflow.com

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

?__a=1

. И это сработало!

Вместо того, чтобы копаться в одной строке в структуре страницы, мы можем просто получить json ответ о любой странице:

Instagram.com/instagram/?__a=1

Рисунок 8: Сравнение ответа сервера, и строки 274

Перейдем теперь к параметру

query_hash

. Так как

f2405b236d85e8296cf30347c9f08c2a

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

Я все же решил разобраться, откуда он берется для GET запросов дозагрузки.

Генератор

query_hash

лежит в файле

ProfilePageContainer.js/

. Но для каждой страницы в Инастаграме существует свой файл:

Рисунок 9

В самом файле хеш лежит в параметре

queryId

, но, как правило, в этом файле как минимум 4 таких параметров с разными хешами:

Рисунок 10: 4 разных queryId в .js файле

Но, именно после

profilePosts.byUserId.get

идет нужный

queryId

:

Рисунок 11

И так, имея всю нужную информацию под рукой, можно перейти к следующему пункту.

Создание приблизительной схемы будущего скрипта

Рисунок 12: приблизительная блок-схема

Реализация скрипта

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

  • Requests – создание GET запросов и загрузки фотографий
  • Argparse – парсинг входящих команд при старте скрипта
  • Json – создание словарей из ответов сервера
  • Time – заморозка скрипта при превышении лимита запросов
  • Os – создание папок

Тестирование и объяснение отдельных функции

Первая функция

parser()

принимает введенный никнейм, и отправляет его в фукнцию

main()

Python:

def parser():
    parser = argparse.ArgumentParser(prog='OSINT Inst')

    parser.add_argument("-n", "--nickname", help="Nickname?", dest="nickname")

    args = vars(parser.parse_args())
    main(args["nickname"])

В функции

main()

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

parse_page()

Python:

def main(nickname):
    url = "https://www.instagram.com/"+nickname
    response = requests.get(url + "?__a=1")

    if "Page Not Found" in response.text:
        print("Page Not Found")
        exit()

    todos = json.loads(response.text)
    os.mkdir("Instagram")
    parse_page(todos)
    photoes(todos["graphql"]["user"])

В функции

parse_page()

принимается словарь из различных значений, и создаются переменные для дальнейшей их записи в файл “info.txt” который, как уже было написано раньше, сохраняется в папке Instagram.

Python:

def parse_page(todos):
    print("Getting info about profile...")
    follow_inst = str(todos["graphql"]["user"]["edge_follow"]["count"])
    followed_by_inst = str(todos["graphql"]["user"]["edge_followed_by"]["count"])
    content_inst = str(todos["graphql"]["user"]["edge_owner_to_timeline_media"]["count"])
    biography = todos["graphql"]["user"]["biography"]
    name = todos["graphql"]["user"]["full_name"]
    username = todos["graphql"]["user"]["username"]
    verified = str(todos["graphql"]["user"]["is_verified"])
    fb = str(todos["graphql"]["user"]["connected_fb_page"])
    business = str(todos["graphql"]["user"]["is_business_account"])

    try:
        link = "\nLink: "+ todos["graphql"]["user"]["external_link"]
    except:
        link = ''


    file = open("Instagram/info.txt", "w", encoding="utf-8")
    info = "Name: "+name+"\nUsername: "+username+"\nBiography: "+biography+"\n\nVerified: "+verified+"\nFollow: "+follow_inst+"\nFollowed: "+\
           followed_by_inst+"\nNumber of posts: "+content_inst+link+"\nBusiness account: "+business+\
           '\nConnected to facebook: '+fb

    file.write(info)
    file.close()

Как только благополучно заканчивается функция

parse_page()

, в функция

main()

запускает следующею, а именно

photoes()

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

ФОТО

Стоит начать с того, что при загрузке страницы в Инстаграме, загружаются уже 12 фотографий, и это можно заметить в ответе на запрос «Instagram.com/{nickname}/?__a=1», а именно в подзаголовке

edges

:

Рисунок 13: уже загруженные 12 фотографий

Так же в подзаголовке

page_info

есть нужный нам параметр

end_cursor

.

Поэтому, переменная

media

вмещает в себе загруженные посты,

checker

содержит в себе параметр

has_next_page

а

idprofile_inst

Id

аккаунта в Инстаграме.

Получив данные, функция photoes(), обратывает их, и дальше вызывает функцию

Get_req_photo()

Python:

def photoes(json_media):
    media = json_media["edge_owner_to_timeline_media"]["edges"]
    checker = json_media["edge_owner_to_timeline_media"]["page_info"]["has_next_page"]
    idprofile_inst = json_media["id"]

    get_req_photo('', media, checker, json_media, idprofile_inst)

Функция

get_req_photo()

создает запросы о дозагрузке фотографии, если

has_next_page

имеет значeние

True

, то есть существует еще страница с фотографиями и её нужно дозагрузить.

Вначале своей работы, переменная

media

сразу же добавляется в переменную

media_to_parse.

Эта переменная в конце работы скрипта будет содержать все посты, которые потом будут обрабатываться.

Python:

def get_req_photo(media_to_parse, media, checker, json_media, idprofile_inst):
    media_to_parse += str(media)

    if checker:
        end_cursor = json_media["edge_owner_to_timeline_media"]["page_info"]["end_cursor"]

        data_for_get_req = 'https://www.instagram.com/graphql/query/?query_hash=f2405b236d85e8296cf30347c9f08c2a' \
                           '&variables=%7B%22id%22%3A%22' + idprofile_inst + \
                           '%22%2C%22first%22%3A50%2C%22after%22%3A%22' + end_cursor.split("==")[0] + '%3D%3D%22%7D'

        gett = requests.get(data_for_get_req)
        json_media = json.loads(gett.text)
        checker = json_media["data"]["user"]["edge_owner_to_timeline_media"]["page_info"]["has_next_page"]
        media = json_media["data"]["user"]["edge_owner_to_timeline_media"]["edges"]

        get_req_photo(media_to_parse, media, checker, json_media["data"]["user"], idprofile_inst)
    else:
        print("Got ", len(str(media_to_parse).split("'shortcode': '")), " post(s)")
        for i in range(1, len(str(media_to_parse).split("'shortcode': '"))):
            link_to_page = str(media_to_parse).split("'shortcode': '")[i].split("',")[0]
            req = requests.get("https://instagram.com/p/" + link_to_page + "/?__a=1")
            json_page = json.loads(req.text)
            parse_media(json_page["graphql"]["shortcode_media"], i)
        print("Done")

И так, если параметр

has_next_page

имеет значение

True

, то скрипт будет создавать запросы и добавлять посты до тех пор, пока параметр

has_next_page

не будет иметь значения

False

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

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

parse_media()

В Инстаграме существует 3 вида постов: фотография, видео, коллекция. Из-за этого в функции

parse_media()

существует отдельно загрузка для каждого вида постов. Я решил загружать только фотографии, из-за того, что видео будут существенно тормозить работу скрипта. Было решено создавать файл с названием “video.txt”, где будет указываться ссылка на пост с видео.

Так же, скрипт загружает фотографии самого высокого качества. Ссылки на фотографии лежат в параметре

display_resources

Рисунок 14: подкаталог с ссылками на фото различного качества

Функция создает глобальную папку Instagram, . Из информации, берется: количество лайков, текст поста, локация, отмеченные профили (теги), комментарии и ссылка на пост; для видео: количество просмотров; для фотографии: captions.

Captions – это догадка искусственного интеллекта, что может содержать фотография. К примеру:

Рисунок 15: фото, и описание, созданное ИИ

Так же, в корневую папку, будут сохраняться еще отдельные файлы:

  • All_comments.txt – все комментарии со всех постов
  • All_tagged.txt – все «затеганные» аккаунты в постах жертвы
  • All_locations.txt – все локации, которые помечены в постах жертвы

Python:

def parse_media(json_page, m):
    view_count = ''
    os.mkdir("Instagram/post " + str(m))
    captions = '\nCaption(s): '
    print("Downloading post " + str(m))

    if json_page["__typename"] == "GraphVideo":
        view_count = str("\nView count = " + str(json_page["video_view_count"]))
        out = open("Instagram/post " + str(m) + "/video.txt", "w")
        out.write(str("https://instagram.com/p/"+json_page["shortcode"]))
        out.close()

    if json_page["__typename"] == "GraphImage":
        photo = str(json_page["display_resources"][-1]["src"])
        p = requests.get(photo)
        out = open("Instagram/post " + str(m) + "/img" + str(m) + ".jpg", "wb")
        out.write(p.content)
        out.close()
        captions = "\n"+json_page["accessibility_caption"]

    if json_page["__typename"] == "GraphSidecar":
        for i in range(len(json_page["edge_sidecar_to_children"]["edges"])):
            photoes = json_page["edge_sidecar_to_children"]["edges"][i]["node"]["display_resources"][-1]["src"]
            p = requests.get(photoes)
            out = open("Instagram/post "+str(m)+"/img"+str(i)+".jpg", "wb")
            out.write(p.content)
            out.close()
            try:
                captions += "\nphoto №"+str(i)+"-"+ str(json_page["edge_sidecar_to_children"]["edges"][i]
                                                  ["node"]["accessibility_caption"])+"\n"
            except:
                view_count = str("\nView count = " + str(json_page["edge_sidecar_to_children"]["edges"][i]
                                                  ["node"]["video_view_count"]))
                out = open("Instagram/post " + str(m) + "/video.txt", "w")
                out.write(str("https://instagram.com/p/" + json_page["shortcode"]))
                out.close()

    likes = "\nlikes: " + str(json_page["edge_media_preview_like"]["count"])+"\n"
    link = "Link - "+"instagram.com/p/"+json_page["shortcode"]+"\n"
    location = json_page["location"]

    if location is not None:
        with open("Instagram/all_locations.txt", "a", encoding="utf-8") as file:
            file.write("post "+str(m)+"\n"+str(location)+"\n")
    else:
        location = ''
        pass


    if json_page["edge_media_to_tagged_user"]["edges"] == []:
        tagged_user = ''
    else:
        tagged = str(json_page["edge_media_to_tagged_user"]).split("node")
        tagged_user = '\nTagged user(s): '
        for i in range(1, len(tagged)):
             tagged_user += "@"+str(json_page["edge_media_to_tagged_user"]).split("username': '")[i].split("'}")[0]+\
                    " ("+str(json_page["edge_media_to_tagged_user"]).split("full_name': '")[i].split("', ")[0]+")\n"
        with open("Instagram/all_tagged.txt", "a", encoding="utf-8") as file:
            file.write(str("post " + str(m) + ":" + tagged_user + "\n"))


    file = open("Instagram/post "+str(m)+"/info.txt", "w", encoding="utf-8")

    try:
        text = "text: '"+str(json_page["edge_media_to_caption"]["edges"][0]["node"]["text"])+"'\n"
    except:
        text = "text: None\n"

    all_information = str(text+likes+tagged_user+"\nLocation: "+str(location)+captions+view_count+"\n"+link)
    file.write(all_information)
    file.close()

    try:
        checker_for_comment = json_page["edge_media_to_parent_comment"]["count"]
    except:
        checker_for_comment = json_page["edge_media_to_comment"]["count"]

    if checker_for_comment != 0:
        comm(json_page, m)
    else:
        pass

Комментарии

Парсинг комментариев происходит подобным образом.

При загрузке фотографии по ссылке

Instagram.com/p/xxxxxxxxxxx

, загружаются так же и часть комментариев (как это было с профилем и постами):

Рисунок 16: загруженные комментарии фотографии

И так, существует функция

comm()

которая вызывается каждый раз из функции

parse_media()

. В функции создаются первые переменные, которые в дальнейшем используются в

get_req_com()

:

Python:

def comm(json_page, m):
    list_of_comments = ''
    try:
        try:
            comments = json_page["edge_media_to_parent_comment"]["edges"]
            page_info = json_page["edge_media_to_parent_comment"]["page_info"]
        except KeyError:
            comments = json_page["edge_media_to_comment"]["edges"]
            page_info = json_page["edge_media_to_comment"]["page_info"]
    except Exception as e:
        print("\n\n", json_page,"\nError:", e)
        exit()
    get_req_com(json_page["shortcode"], comments, page_info, list_of_comments, m)

В функции

get_req_com()

все загруженные комментарии с постом сохраняются в переменную

list_of_comm

. Только в подкаталоге page_info содержится информация о существовании следующих комментариев. Если параметр

has_next_page

равен

True

, то соз��ается запрос о подзагрузке 49 комментариев (да, почему-то лимит постов – 50, но лимит комментариев - 49), потом создаются свежие переменные, и опять отправляются в ту же функцию.

Загрузив информацию о посте, и скачав фотографию, новый пост не начнет обрабатываться, пока

has_next_page

не будет равен

False:

Рисунок 17

После того, как

has_next_page

достиг значения

False

, все собранные комментарии с поста отправляются в функцию

parse_comm()

, где каждый коммент парсится, и собирается в единую переменную

list

, и после этого, функция возвращает отредактированный список комментариев в переменную

comments

.

Comments

записывается в файл comments.txt определенного поста и в файл all_comments.txt .

Так же при загрузке комментариев иногда проскальзывает ошибка ‘rate limiting’. Эта ошибка связанна с большим количеством запросов на подзагрузку дополнительных комментариев. И решением является «простой» кода 8 минут.

Как только работа скрипта закончена, мы получаем такой результат:

Рисунок 18: демонстрация сохраненных данных

Создание скрипта с использованием API

Этот скрипт мы будет писать под Twitter.

Получение доступа к api является самым сложным пунктом. И так, что бы начать пользоваться API Twitter’a, мы должны:

  • Создать аккаунт в Twitter
  • Перейти в https://developer.twitter.com/en/apps и создать приложение
  • После этого, вам нужно придумать название вашего приложения, описать его до 100 символов и придумать ссылку.
  • Далее, вам нужно будет выбрать, в какой области находится ваше приложение:

Рисунок 19: выбор категории будущего приложения

  • Так же, нужно будет заполнить ответить на подобные вопросы: «Для каких именно целей вы будете использовать Твиттер», «Какие лица, кроме вас, будут так же получать информацию?», «Будете ли вы распространять полученную информацию о пользователях Твиттера?» и все в этом духе. Заполнив все бланки, и нажав Next, заявление автоматически отправляется на рассмотрение в команду Твиттера:

Рисунок 20: заявление на выдачу API отправлено

Получив на свою почту подобное письмо:

Рисунок 21: письмо от команды Твиттера на одобрение приложения

Можно приступать к получению ключей.

Переходим в Apps:

Рисунок 22

Затем в Details:

Рисунок 23

Во вкладке Keys and tokens, будут лежать нужные нам ключи:

Рисунок 24

Приступаем к созданию схемы

Создание приблизительной схемы будущего скрипта

Рисунок 25: приблизительная блок-схема

Реализация скрипта

В этом скрипте будем использовать дополнительную библиотеку TwitterAPI

Устанавливается она, как и все доп. библиотеки, следующей командой:

Pip install TwitterAPI

Из основных библиотек, будут использоваться следующее:

  • Os – для создания папок
  • Requests – для загрузки фотографий
  • Json – для создания словарей из ответов сервера

Тестирование и объяснение отдельных функций

Так как мы уже работаем с API, в этом скрипте будет использовать ключи, которые находятся во вкладке Apps>Details>Keys and tokens. Вставляем их в скрипт, и создаем переменную api:

Python:

cons_key = "XXXXXXXXXXXXXXXXXXXXXXXXX"
cons_sec_key = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
acc_tok = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
acc_tok_sec = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

api = TwitterAPI(cons_key, cons_sec_key, acc_tok, acc_tok_sec)

Функция

main()

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

info_about_acc()

Python:

def main(nick):
    reqst = api.request("users/lookup", {"screen_name": nick})
    jsn = json.loads(reqst.text)
    try:
        if jsn["errors"][0]["code"] == 17:
            print("This page doesn't exist")
        elif '""errors":' in reqst.text:
            print("ERROR\nResponse data:", reqst.txt)
        exit()
    except:
        os.mkdir("Twitter")
        count, count_for_tweets = info_about_acc(jsn[0])
    print("Downloading list of followers...")
    followers(nick, "-1", [])
    friends(nick, "-1", [])
    getting_tweets(nick, [], 0, count_for_tweets, "")
    getting_favorites(nick, count, 0, [], '')

Функция

info_about_acc()

выглядит большой только из-за своих переменных, и некоторых исключений. Основные цели функции – запись данных, взятых из словаря в файл «Info.txt» и сохранение фотографий.

Python:

def info_about_acc(jsn):
    print("Getting information...")
    id = str(jsn["id"])
    name = jsn["name"]
    nickname = jsn["screen_name"]
    followers = str(jsn["followers_count"])
    friends = str(jsn["friends_count"])
    data_of_creation = jsn["created_at"]
    count_of_liked_tweets = jsn["favourites_count"]
    verified = jsn["verified"]
    profile_pic = jsn["profile_image_url_https"]
    statuses_count = jsn["statuses_count"]

    lang = jsn["lang"]
    if lang is not None:
        lang = "\nLang -" + lang
    else:
        lang = ''

    location = jsn["location"]
    if location != '':
        location = "\nLocation -" + location

    description = jsn["description"]
    if description != '':
        description = "\n\tDescription -" + description

    print("Downloading photos...")
    background_pic = jsn["profile_background_image_url"]
    if background_pic is not None:
        photo_back = requests.get(background_pic)
        out_back = open("Twitter/Profile background photo.jpg", "wb")
        out_back.write(photo_back.content)
        out_back.close()
        background_pic = '\nBackground picture - ' + background_pic
    else:
        background_pic = ''

    if not jsn["profile_use_background_image"]:
        banner_pic = jsn['profile_banner_url']
        photo = requests.get(banner_pic)
        out = open("Twitter/Profile banner.jpg", "wb")
        out.write(photo.content)
        out.close()

    photo = requests.get(profile_pic.split("_normal")[0]+profile_pic.split("_normal")[1])
    out = open("Twitter/Profile photo.jpg", "wb")
    out.write(photo.content)
    out.close()

    print("Writing information to the file info.txt...")
    info = "ID - " + id + "\nName - " + name + "\nNickname - " + nickname + "\nFollowers - " + followers + "\nFriends - " + friends+\
    "\nData of creation - " + data_of_creation + "\nCount of liked twiters - " + str(count_of_liked_tweets) + "\nVerified - "+\
    str(verified) + "\nStatuses count - " + str(statuses_count) + lang + location + description + background_pic + \
           "\nProfile pic - " + profile_pic

    file = open("Twitter/Info.txt", "w")
    file.write(info)

    file.close()

    return count_of_liked_tweets, statuses_count

После этого, функция возвращает

count_of_liked_tweets

(количество лайкнуты) в переменную

count

в функции

main()

.

Далее, запускается функция

followers()

Эта функция работает с подписчиками аккаунта. Создается запрос на загрузку данных о подписчиках, далее загруженная информация преобразуется в словарь, добавляется в переменную, и если нужна еще подзагрузка (об этом говорит параметр

next_cursor

):

Рисунок 26: Next_cursor

То создает еще:

Python:

if jsn["next_cursor"] != 0:
    followers(nickname, jsn["next_cursor"], media_to_parse)

Как и в прошлом скрипте, передавая уже новые значения (

media

,

cursor

)

Python:

def followers(nickname, cursor, media_to_parse):
    r = api.request("followers/list", {"cursor": cursor, "screen_name": nickname, "skip_status": "true",
                                       "include_user_entities": "false", "count": "200"})
    jsn = json.loads(r.text)
    print(jsn)
    media_to_parse += jsn["users"]

    if jsn["next_cursor"] != 0:
        followers(nickname, jsn["next_cursor"], media_to_parse)
    else:
        print("Got ", len(str(media_to_parse).split("'id': "))-1, " follower(s)")
        print("Downloading list of followers...")
        for i in range(len(str(media_to_parse).split("'id':"))-1):
            parsing_followers_friends(media_to_parse[i], "Followers")
        print("Done\n")

Функция

friends()

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

Так же как и во

followers()

, сначала происходит создание запроса, создание словаря, добавление в переменную.

Python:

def friends(nickname, cursor, media_to_parse):
    r = api.request("friends/list", {"cursor": cursor, "screen_name": nickname, "skip_status": "true",
                                       "include_user_entities": "false", "count": "200"})
    jsn = json.loads(r.text)
    media_to_parse += jsn["users"]

    if jsn["next_cursor"] != 0:
        friends(nickname, jsn["next_cursor"], media_to_parse)
    else:
        print("Got ", str(len(str(media_to_parse).split("'id': "))-1), " friend(s)")
        print("Downloading list of friends...")
        for i in range(len(str(media_to_parse).split("'id':"))-1):
            parsing_followers_friends(media_to_parse[i], "Friends")
        print("Done\n")

И если больше друзей нету, то есть

next_cursor равняется

‘’, то как в функции

followers()

, так и во

friends()

переменная media, которая имеет в себе весь список друзей/подписчиков, отправляется в функцию

parsing_followers_friends()

, где, в зависимости от

friends_or_followers

, функция решает куда сохранять файл.В функции

followers()

, в функцию отсылается строка “Followers”:

Python:

parsing_followers_friends(media_to_parse[i], [B]"Followers"[/B])

А в функции

friends()

– строка “Friends”:

Python:

parsing_followers_friends(media_to_parse[i], [B]"Friends"[/B])

После обработки последнего друга, функция

friends()

окончена, и стартует функция

getting_tweets()

Данная функция собирает все твиты и ретвиты пользователя в одну переменную, и в дальнейшем отправляется в функцию

parsing_followers_friends()

:

Python:

def getting_tweets(nickname, media, count, statuses, max_id):
    if max_id == "":
        r = api.request("statuses/user_timeline", {"screen_name": nickname, "count": "200"})
    else:
        r = api.request("statuses/user_timeline", {"screen_name": nickname, "count": "200", "max_id": max_id})
    jsn = json.loads(r.text)
    media += jsn
    count += 200

    if statuses > count:
        del media[-1]
        getting_tweets(nickname, media, count, statuses, jsn[-1]["id_str"])

    else:
        print("Got "+str(len(media))+" tweet(s)")
        print("Downloading tweets")
        for i in range(len(media)):
            parsing_tweets(media[i])
        print("Done")

Так же стоит объяснить про странную строку del media[-1]. Дело в том, что при подзагрузке твитов/ретвитов, отсутствует такой курсор, как "next_page”, имеющийся в предыдущих функциях, и отследить, загрузились ли все твиты/ретвиты невозможно. Поэтому, пришлось придумывать велосипед, и использовать id последнего твита, как начало для загрузки следующего. Но из-за того, что один и тот же твит будет в конце и в начале, нужно удалять последний твит в общей переменной.

Твиттер также дает возможность просмотреть id тех пользователей, которые ретвитнули твит указанного аккаунта. Но так как, во-первых, лимит запросов составляет всего 300 в 15 минут:

Рисунок 27: Лимит на отправку запросов

а во-вторых на каждый id нужно создавать отдельный запрос, чтобы узнать информацию об аккаунте. Все указанные действия будут происходить медленно, с интервалом в 15 минут (если скрипт за 15 минут будет использовать больше 300 запросов). Из-за указанных особенностей, данная функция использоваться не будет.

По итогу, и собрав все твиты/ретвиты, весь полученный из словарей массив, отправляется в

parsin_tweets()

:

Python:

def parsing_tweets(tweet):
    try:
        author_nickname = tweet["retweeted_status"]["user"]["screen_name"]
        author_name = tweet["retweeted_status"]["user"]["name"]
        text = str(tweet["retweeted_status"]["text"])
        link_to_tweet = "https://twitter.com/"+author_nickname+"/status/"+tweet["id_str"]
        tweets_like = tweet["retweeted_status"]["favorite_count"]
        tweets_retweet = tweet["retweeted_status"]["retweet_count"]
        with open("Twitter/Retweets.txt", "a", encoding="utf-8") as file:
            file.write(author_name+"  (@"+author_nickname+")\n\t'"+text+"'\n\n\t"+str(tweets_retweet)+" Retweet(s)  "+str(tweets_like)+" Like(s)"+"\n\n\tTweet - "+link_to_tweet+"\n\n"+"--"*25+"\n")

    except:
        text = tweet["text"]
        data = tweet["created_at"]
        likes = tweet["favorite_count"]
        retweets = tweet["retweet_count"]
        link_to_tweet = "https://twitter.com/" + tweet["user"]["screen_name"] + "/status/" + tweet["id_str"]
        with open("Twitter/Tweets.txt", "a", encoding="utf-8") as file:
            file.write("'"+text+"'\n\n\t"+data+"\t\t"+str(retweets)+" Retweet(s)  "+str(likes)+
                       " Like(s)"+"\n\n\tTweet - "+link_to_tweet+"\n\n\n"+"--"*25+"\n")

    file.close()

После создания переменных, полученные данные записываются в файл “Retweets.txt” и “Tweets.txt”.

И последняя функция имеет название

getting_fevorites()

.

Она использует

count

(переменная из функции

main()

) для подсчета загруженных твитов.

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

Лайкнутые твиты записывает в “Liked.txt”

После скана, получаем папку с такими файлами:

Рисунок 28: полученные файлы после работы скрипта

Сравнение вариантов реализации

OSINT скриптов

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

В Инстаграме, если пользователь не вошел в аккаунт – он не может смотреть Stories, список подписчиков и список друзей.

Но в то же время, мы никак не привязаны аккаунтом, и мы можем сканировать что угодно, и сколько угодно (до ошибки rate limiting)

В Твиттере мне пришлось открыто лгать про цели и идеи моего приложения. И не факт что вам удастся таким же образом получить эти ключи для пользования API.

Но другая сторона медали – сплошное удобство.

Вы можете сравнить функции в скрипте для Твиттера и для Инстаграма. Использование библиотеки решает многие вопросы, над которыми я долго сидел при написании скрипта для Инстаграма.

Заключение

Подводя итоги, хочу сказать, что каждый из этих скриптов является утилитой для OSINT, вне зависимости от использования API.

Более анонимный способ – первый, декомпиляция трафика и создание собственных запросов

Более простой – второй, взяв токены и ключи, следовать по документации от Твиттера.

Тут уже решать за вами.

https://github.com/haqgg/OsInstagram

https://pastebin.com/aLu3X6Zh