https://habrahabr.ru/post/347774/- Интернет-маркетинг
- Визуализация данных
- Python
Предисловие
Сейчас многие используют инстаграм (далее инста): кто-то там собирает альбомы, кто-то продает, кто-то покупает, а я там ленюсь. Мне всегда было интересно как там поживают мои друзья, одноклассники, коллеги и инста в этом помогала. Захотел узнать, что там нового — зашел, полистал ленту, увидел все, что интересовало ушел… НО! Мне почему-то всегда нужно было лайкнуть каждый пост (не могу обьяснить зачем, но такие вот дела). И вот представьте, неделю туда не заходил, сидишь, лайкаешь недельный пул, а когда у тебя 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-аккаунту