Предисловие


Сейчас многие используют инстаграм (далее инста): кто-то там собирает альбомы, кто-то продает, кто-то покупает, а я там ленюсь. Мне всегда было интересно как там поживают мои друзья, одноклассники, коллеги и инста в этом помогала. Захотел узнать, что там нового — зашел, полистал ленту, увидел все, что интересовало ушел… НО! Мне почему-то всегда нужно было лайкнуть каждый пост (не могу обьяснить зачем, но такие вот дела). И вот представьте, неделю туда не заходил, сидишь, лайкаешь недельный пул, а когда у тебя 200+ подписок — это вообще ад.

Активные действия


В итоге, как и любому нормальному человеку, мне стало лень лайкать все подряд и я забил. Вроде бы все стало хорошо, я перестал тратить кучу времени на бесполезные лайки, но меня съедала совесть. Я понимал, что подписчикам плохо без моего царского лайка, они грустят и бла бла бла… В общем, было решено, что нужно написать что-то простое и легкое, которое сможет решить проблему негодования, а может и помочь еще кому-то. От знакомых много слышал о python и о том, как клево тестить приложения с помощью selenium или использовать его в качестве некого crawler'a. Было решено использовать python и selenium в связке с phantom js, все это было для меня ново, т.к. до этого я с данными технологиями вообще не был знаком.

Почему Selenium и phantom?


Тут все очень просто. Клиентская часть instagram написана на react, следовательно, какие-либо данные можно дернуть там только после того, как страница будет срендерена. Т.к. selenium как раз и служит для автоматизации действий в браузере, а phantom js помогает делать это все без какого-либо отображения, было решено их использовать. Забегая наперед скажу, что от phantom js я решил отказаться в силу того, что он достаточно медленный, а у chrome появилась опция headless, что и позволило использовать его в качестве «безголового» браузера.

Почему python?


Я много слышал и читал о том, что этот язык отлично подходит для работы с big data, отсюда я сделал вывод, что в нем удобно работать вообще с какими-либо данными (парсить, сортировать, сравнивать, форматировать и т.д.), также я где-то читал, что к нему удобно и быстро писать свой мини-библиотеки (а это то, что нужно для бота, чтоб сделал его как можно универсальнее). Взвесив все, решил остановиться на python3 (до этого часть проекта уже была написана с возможностью запуска на python2 и python3).


Разработка библиотеки для бота


Весь процесс описывать глупо, поэтому остановимся на самых интересных моментах:

  • Авторизация

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

    Оказалось, что у instagram с этим все просто (а вот mail ru доставил мне дикую головную боль):
    import pickle
    import time
    import tempfile
    import os
    import selenium.common.exceptions as excp
    
    
    def auth_with_cookies(browser, logger, login, cookie_path=tempfile.gettempdir()):
        """
        Authenticate to instagram.com with cookies
        :param browser: WebDriver
        :param logger:
        :param login:
        :param cookie_path:
        :return:
        """
        logger.save_screen_shot(browser, 'login.png')
        try:
            logger.log('Trying to auth with cookies.')
            cookies = pickle.load(open(os.path.join(cookie_path, login + '.pkl'), "rb"))
            for cookie in cookies:
                browser.add_cookie(cookie)
            browser.refresh()
            if check_if_user_authenticated(browser):
                logger.log("Successful authorization with cookies.")
                return True
        except:
            pass
    
        logger.log("Unsuccessful authorization with cookies.")
        return False
    
    
    def auth_with_credentials(browser, logger, login, password, cookie_path=tempfile.gettempdir()):
        logger.log('Trying to auth with credentials.')
        login_field = browser.find_element_by_name("username")
        login_field.clear()
        logger.log("--->AuthWithCreds: filling username.")
        login_field.send_keys(login)
        password_field = browser.find_element_by_name("password")
        password_field.clear()
        logger.log("--->AuthWithCreds: filling password.")
        password_field.send_keys(password)
        submit = browser.find_element_by_css_selector("form button")
        logger.log("--->AuthWithCreds: submitting login form.")
        submit.submit()
        time.sleep(3)
        logger.log("--->AuthWithCreds: saving cookies.")
        pickle.dump([browser.get_cookie('sessionid')], open(os.path.join(cookie_path, login + '.pkl'), "wb"))
        if check_if_user_authenticated(browser):
            logger.log("Successful authorization with credentials.")
            return True
        logger.log("Unsuccessful authorization with credentials.")
        return False
    
    
    def check_if_user_authenticated(browser):
        try:
            browser.find_element_by_css_selector(".coreSpriteDesktopNavProfile")
            return True
        except excp.NoSuchElementException:
            return False

    При неудачной авторизации куками, авторизируемся логином/паролем, сохраняет куку и используем ее в дальнейшем, стандартная схема.

    #TODO: никак не дойдут руки до проверки возраста куки
  • Лайкинг ленты новостей

    Т.к. в первую очередь я писал это для себя, мне было интересно, чтоб у меня всегда была отлайкана новостная лента. Изначально все было просто, листается сверху до последнего обработанного поста, веб-элементы постов заносятся в массив, включается задняя и лайкается все на обратном пути, проложенном через веб-элементы постов, которые лежат в ранее созданном массиве. Я был счастлив, что все работает именно так, как мне того хотелось, но где-то через два месяца «луна была козероге» и мой бот тупо перестал работать. Проверял все как мог, на разных веб-драйверах, визуально ничего не изменилось, но при этом ничего и не работает. В общем, убил я на поиски проблемы около трех дней. Оказалось все очень просто: раньше когда бот проходил по проскролленым постам, он брал их объекты из массива, скроллил к посту (имитируя действия человека) находил там кнопку «лайк», нажимал ее и шел дальше; теперь же инстаграм решил хранить в html-разметке только ~ 9 постов из которых в структуре 5ый — активный у пользователя, предыдущие 4 и следующие 4, а все остальные из html просто удалялись. Пришлось решать вопрос собиранием тех постов, которые нужно лайкнуть в массив по их ссылке, потом при скроллинге вверх (тупо вверх) искать текущий пост в раннее собранном массиве и при наличии его там — лайкать.

    Та еще наркомания..
    for post in progress:
                real_time_posts = br.find_elements_by_tag_name('article')
                post_link = post.get('pl')
                filtered_posts = [p for p in real_time_posts if self._get_feed_post_link(p) == post_link]
                if filtered_posts.__len__():
                    real_post = filtered_posts.pop()
                    # scroll to real post in markup
                    heart = real_post.find_element_by_css_selector('div:nth-child(3) section a:first-child')
                    self.browser.execute_script("return arguments[0].scrollIntoView(false);", heart)
                    # getting need to process elements
                    author = real_post.find_element_by_css_selector('div:first-child .notranslate').text
                    heart_classes = heart.find_element_by_css_selector('span').get_attribute('class')
                    # check restrictions
                    is_not_liked = 'coreSpriteHeartOpen' in heart_classes
                    is_mine = author == login
                    need_to_exclude = author in exclude
    
                    if is_mine or not is_not_liked:
                        self.post_skipped += 1
                        pass
                    elif need_to_exclude:
                        self.post_skipped_excluded += 1
                        pass
                    else:
                        # like this post
                        time.sleep(.3)
                        heart.click()
                        time.sleep(.7)
                        self.db.likes_increment()
                        self.post_liked += 1
                        log = '---> liked @{} post {}'.format(author, post_link)
                        self.logger.log_to_file(log)

    ПОБЕДА!
  • Лимиты действий

    Чтоб не привлекать много внимания, нужно ставить боту какие-то ограничения. Чтоб придерживаться этих ограничений, нужно куда-то сохранять счетчики произведенных действий. Для хранилища всякой внутренней информации было выбрано sqlite — быстро, удобно, локально. Прям в библиотеке я написал небольшой модуль для работы в бд, туда же добавил миграции — для последующих релизов. Сохраняется в бд каждый лайк/фоллоу с часом, в который он сделан, потом считаются лайки/фолловы за сутки/текущий час, исходя из этих данных решается можно ли еще кого-то лайкать или фолловить. Лимиты пока жестко прописаны в библиотеке, нужно будет сделать их конфигурируемыми.
  • Ответвление в процессе разработки

    Пока писалась библиотека для бота, в голове засел вопрос о циферках. Стало интересно сколько у пользователя лайков, просмотров, комментов в разрезе поста или сумарно. Для удовлетворения интереса был написан небольшой класс библиотеки, который через приватное api инстаграмма собирал всю доступную (без авторизации) статистику и выдавал ее пользователю:

    Скрытый текст
    +-- https://instagram.com/al_kricha/ --------------------------+
    |   counter                    |             value             |
    +------------------------------+-------------------------------+
    |   followed                   |              402              |
    |   posts                      |              397              |
    |   comments                   |             1602              |
    |   likes                      |             20429             |
    |   following                  |              211              |
    |   video views                |             6138              |
    |                                                              |
    +--------- https://github.com/aLkRicha/insta_browser ----------+
    +--------------------------------------------------------------+
    |                       top liked posts                        |
    +--------------------------------------------------------------+
    |       https://instagram.com/p/BVIUvMkj1RV/ - 139 likes       |
    |       https://instagram.com/p/BTzJ38-DkUT/ - 132 likes       |
    |       https://instagram.com/p/BI8rgr-gXKg/ - 129 likes       |
    |       https://instagram.com/p/BW-I6o6DBjm/ - 119 likes       |
    |       https://instagram.com/p/BM4_XSoFhck/ - 118 likes       |
    |       https://instagram.com/p/BJVm3KIA-Vj/ - 117 likes       |
    |       https://instagram.com/p/BIhuQaCgRxI/ - 113 likes       |
    |       https://instagram.com/p/BM6XgB2l_r7/ - 112 likes       |
    |       https://instagram.com/p/BMHiRNUlHvh/ - 112 likes       |
    |       https://instagram.com/p/BLmMEwjlElP/ - 111 likes       |
    +--------------------------------------------------------------+

    Имея такие данные мы с другом (txwkx) решили визуализировать их и создали instameter.me — небольшой сервис, где можно посмотреть «резюме» любого открытого instagram-аккаунта.

    Пример
  • Что умеет бот?

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

    • Лайкает ленту новостей до последнего не лайкнутого.
    • Лайкает тег на указанное кол-во постов
    • Лайкает локацию на указанное кол-во постов
    • Автофолловит людей из постов локации/тега, при включении настройки, но не всех подряд, а только тех, которые потенциально могут стать подписчиками
    • Собирает статистику по пользователю
    • Хранит статистику по часам о совершенным действиям

  • Что хотелось бы сделать в будущем?

    • Написание ± осмысленных комментариев
    • Отписываться от ненужных аккаунтов
    • Лайкать несколько постов только что зафолловенного человека
    • Переписать алгоритм прохождения новостной ленты
    • Сравнивать несколько аккаунтов

Заключение


Еще много чего нужно сделать, оптимизировать, переписать. Всегда можно использовать эффективно инструмент не по назначению. Лень — это точно двигатель прогресса. Надеюсь, кому-то мой бот поможет или в работе, или в хобби. Репозиторий с pypi-пакетом может помочь начинающему автоматизатору. Репозиторий с примерами может быть полезным для SMM-щиков. Всем спасибо за внимание.

Ссылки


  • insta_browser — моя мини-библиотека, сердце бота
  • insta_bot — examples репозиторий, сам бот (в таком виде я его и использую)
  • instameter — проект для снятия статистики по instagram-аккаунту

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


  1. zCooler
    29.01.2018 19:52

    Вот еще хороший бот на питоне, там можно много для себя почерпнуть в плане работы с инстаграмом


    1. kricha Автор
      29.01.2018 19:57

      да, можно почерпнуть конфигурации, увидел там пару интересных моментов. Спасибо.


    1. Dn4g
      02.02.2018 00:46

      Подскажите, пожалуйста, есть ли подобные боты для Twitter?


  1. vesper-bot
    30.01.2018 09:09

    Скорее уж «поРОБОТила» :)


  1. Scrloll
    30.01.2018 17:14
    +1

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


  1. lamo4ok
    31.01.2018 00:36

    Я ровным счетом не хочу ни в чем обвинять автора, но у меня такое ощущение, что есть доля лукавства в целях написания этого бота, обозначенных в начале поста :-)


    1. kricha Автор
      31.01.2018 00:46

      а можно конкретнее, в каком моменте вы думаете я лукавил?) Отвечу честно.


  1. vdm_mar
    01.02.2018 11:04

    Спасибо за headless, что-то я прощелкал инфу.