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