Python для создания идеального поста во «ВКонтакте»

Несколько месяцев назад работал над проектом, одной из главных задач которого был максимальный охват постов во «ВКонтакте». Для выведения формулы идеального поста написал скрипт на Python, который парсил записи группы и группировал данные по часу, количеству символов и типу поста.

В закладки
Аудио

Сразу скажу, скрипт написан далеко не идеально, я маркетолог, а не программист.

Пример работы

Для примера работы скрипта взял группу Life News. На момент написания статьи было собрано 92 207 постов, последний пост датирован 1 апреля 2017 года. 17 минут затрачено на скачивание, и 5 секунд — на обработку данных.

Выводы:

  • Лучшее время для размещения записей — с 19:00 до 22:00. Этот интервал показал максимальные количество просмотров сумму активностей на пост. Интересно, что больше всего комментариев зафиксировано в дневное время, с 11:00 до 14:00, но количество комментариев на пост больше в вечернее время, с 20:00 до 22:00.
  • 91% записей в сообществе короткие, до 200 символов.
  • Материалы с видео набирают в два раза больше активностей, но при этом среднее количество просмотров на пост одинаковое.
  • Посты без встроенной ссылки собирают больше активностей на пост, и ER в два раза выше.

Файл с исходными данными.

Создание токена

Для работы с API «ВКонтакта» необходимо сгенерировать сервисный токен. На эту тему написано много инструкций, поэтому буду краток.

1. Перейдите на страницу создания приложения и создайте приложение «Standalone-приложение»:

2. После создания приложения перейдите в настройки и сохраните сервисный ключ доступа.

Установка дистрибутива Anaconda

Anaconda — это дистрибутив для работы с Python, в него входит много библиотек и инструментов. Нам для реализации данного скрипта понадобится Jupiter Notebook и библиотека Pandas.

  1. Запустите установщик и просто следуйте инструкциям.
  2. После успешной установки перейдите в папку, где будет располагаться ваш проект, и в строке адреса введите CMD и нажмите Enter.
  3. У вас откроется командная строка, введите в неё команду Jupyter Notebook и нажмите Enter.
  4. Откроется страница Jupyter Notebook в браузере, после чего создайте новый файл Python 3.

Работа со скриптом

Собирать данные можно как из своих групп, так и групп конкурентов. Количество постов для анализа устанавливается вручную. Собираются все стандартные данные: количество символов в тексте, сам текст, количество просмотров, комментариев, лайков и вложений.

Ввод исходных данных и подключение библиотек

import requests import pandas as pd from datetime import datetime import time id_group = 'id_group' token = 'token' count_posts = 10000

Первый этап разделен на два промежуточных. В первом мы импортируем необходимые для работы библиотеки. Request — для получения данных из «ВКонтакте», Pandas — для работы с данными, DateTime — для работы с датами, библиотека Time — для создания интервалов между запросами.

На втором этапе необходимо ввести исходные данные. В поле id_group — домен группы, token — токен, полученный на предыдущем этапе, count_posts — количество постов для анализа. Для того чтобы спарсить все записи, просто введите 1 000 000, алгоритм сам возьмёт максимальное количество постов.

После ввода кода в Jupiter Notebook нажимаем Ctrl+Enter и переходим на следующую строку для ввода второй части кода.

Подключение к API и загрузка данных

Для подключения к API мы использовали метод wall.get, более подробное описание методов API в официальной справке.

Вторая часть кода с помощью get-запроса инициирует обращение к API. Не буду подробно останавливаться на get-запросе, просто скажу, что он состоит из двух параметров: URL — ссылка для запроса, и Params — параметр запроса. В словаре params, в зависимости от ваших нужд, можно поменять переменную filter на owner — посты только от владельца, all — все посты, others — гостевые посты.

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

Ответ получаем в JSON-формате и записываем его в переменную r. Из-за ограничений API за одно обращение мы можем забрать только 100 постов. Поэтому за каждую итерацию цикла 100 постов добавляются в массив данных data_posts. Также для обхода ограничения по количеству запросов в секунду я добавил time.sleep(0.5), что позволяет сделать паузу в полсекунды между запросами.

Обработка и запись данных в Data Frame

Для работы с данными нам понадобится библиотека Pandas. Pandas — одна из самых популярных библиотек для анализа и обработки данных. Работа с библиотекой строится через объект Data Frame, внешне он напоминает таблицу, но это не совсем так. Для начала работы с DF нам необходимо преобразовать и обработать данные из JSON-формата.

stats = [] for record in data_posts: title = record['text'].split('\n')[0] if len(title) > 80: title = title[:80] len_title = len(record['text']) len_title = len_title // 100 *100 date = datetime.fromtimestamp(record['date']).strftime('%Y-%m-%d') hour = datetime.fromtimestamp(record['date']).strftime('%H') attachment = {'photo' :0, 'audio' :0, 'video': 0 , 'link': 0, 'poll': 0} if 'attachments' in record: for attach in record['attachments']: if attach['type'] in attachment: attachment[attach['type']] = attachment[attach['type']] + 1 if 'views' in record: views = record['views']['count'] else: views = 0

Для начала создадим пустой массив, где будут храниться данные stats = []. Как вы помните, мы записывали все данные поступающие из «ВКонтакте» в массив data_posts. Теперь наша задача вытащить из него только нужное. Для обхода всего массива мы создаём цикл For, такая конструкция for record in data_posts: позволит работать с одним постом за один обход цикла.

За название поста я брал первую строчку и ограничил длину 80 символами. Это подходило исключительно для моего проекта, вы переделайте алгоритм под ваш проект. Для того чтобы записать в переменную title текст, достаточно title = record['text']. Далее я замерил длину текста и разделил на 100, взяв только целое число для удобной группировки.

Переменные Date и Hour — это дата в формате «гггг-мм-дд» и час выхода поста соответственно. Следующий блок кода — это поиск и подсчёт количества вложений. Все они перечислены в словаре attachments.

Последнее условие проверяет, есть ли в данных о посте количество просмотров. «ВКонтакте» относительно недавно ввела количество просмотров в записи, и поэтому при получении постов, опубликованных раньше 2017 года, скрипт выдавал ошибку, так как ключ Views отсутствовал. По этой причине я записывал в такие посты 0;

total_actions = record['comments']['count'] + record['likes']['count'] + record['reposts']['count'] stats.append([title, len_title, attachment['photo'], attachment['audio'], attachment['video'], attachment['link'], attachment['poll'], views , record['comments']['count'], record['likes']['count'], record['reposts']['count'], total_actions, date, hour])

В этой части кода мы подсчитываем сумму активностей и добавляем в наш массив данные, которые нам интересны.

columns = ["name_post", 'len_text', 'photo', 'audio', 'video', 'link', 'poll', "views", "comments", "likes", "share", 'total_action', "date", "hour"] df = pd.DataFrame(data=stats, columns=columns)

В последнем фрагменте третьей части мы записываем наши данные в Data Frame. Сначала необходимо задать название столбцов, я задал их через массив columns. После чего мы создаём объект Data Frame и в качестве аргументов передаём в него массив данных stats и название заголовков columns. Готово, объект создан, и теперь с ним можно работать.

Вычисление показателей и запись в Excel

Получившийся Data Frame группируем по часу, типу постов и длине текстов и вычисляем показатели вроде средней активности на пост, просмотров на пост и ER.

df_hour = df.drop(['len_text', 'photo', 'audio', 'video', 'link', 'poll'], axis=1) df_group_by_hour = df_hour.groupby('hour').sum() df_group_by_hour['count_post'] = df_hour.groupby('hour')['name_post'].count() df_group_by_hour['mean_action'] = df_group_by_hour['total_action'] /df_group_by_hour['count_post'] df_group_by_hour['views_on_post'] = df_group_by_hour['views'] / df_group_by_hour['count_post'] df_group_by_hour['er'] = df_group_by_hour['total_action'] / df_group_by_hour['views'] * 100 df_group_by_hour = df_group_by_hour.sort_values(by="er", ascending=False) df_type = df.drop(['date', 'hour'], axis=1) df_group_by_len_title = df_type.groupby('len_text').sum() df_group_by_len_title['count_posts'] = df_type.groupby('len_text')['name_post'].count() df_group_by_len_title['mean_action'] = df_group_by_len_title['total_action'] / df_group_by_len_title['count_posts'] df_group_by_len_title['views_on_post'] = df_group_by_len_title['views'] / df_group_by_len_title['count_posts'] df_group_by_len_title['er'] = df_group_by_len_title['total_action'] / df_group_by_len_title['views'] * 100 df_group_by_len_title = df_group_by_len_title.sort_values(by='views', ascending=False) df_group_by_len_title = df_group_by_len_title.style.format("{:.2f}")

И записываем в Excel-файл.

with pd.ExcelWriter('data_vk_{}.xlsx'.format(id_group)) as writer: df.to_excel(writer, index = False , sheet_name='Исходный DataFrame') df_group_by_hour.to_excel(writer, index = True, sheet_name='Группировка по часу') df_group_by_len_title.to_excel(writer, index = True, sheet_name='Группировка по кол-ву символов') for atach in ['photo','audio', 'video','link','poll']: df_group_by_temp = df_type.groupby(atach).sum() df_group_by_temp = df_group_by_temp.loc[:,["views", "comments", "likes", "share", 'total_action']] df_group_by_temp['count_posts'] = df_type.groupby(atach)['name_post'].count() df_group_by_temp['mean_action'] = df_group_by_temp['total_action'] / df_group_by_temp['count_posts'] df_group_by_temp['views_on_post'] = df_group_by_temp['views'] / df_group_by_temp['count_posts'] df_group_by_temp['er'] = df_group_by_temp['total_action'] / df_group_by_temp['views'] * 100 df_group_by_temp = df_group_by_temp.sort_values(by='er', ascending=False) df_group_by_temp = df_group_by_temp.style.format("{:.2f}") sheet_name = 'Группировка по ' + atach df_group_by_temp.to_excel(writer, index = True, sheet_name=sheet_name)

Файл записывается туда же, где находится файл Jypter Notebook. Он содержит исходные необработанные данные, сгруппированные данные по часу, сгруппированные данные по количеству символов и группировку по вложениям.

Готовый Jupyter Notebook.

Надеюсь, данный гайд поможет вам найти формулу вашего идеального поста.

Материал опубликован пользователем. Нажмите кнопку «Написать», чтобы поделиться мнением или рассказать о своём проекте.

Написать
{ "author_name": "Марков Вячеслав", "author_type": "self", "tags": [], "comments": 17, "likes": 40, "favorites": 140, "is_advertisement": false, "subsite_label": "marketing", "id": 69765, "is_wide": false, "is_ugc": true, "date": "Wed, 29 May 2019 22:43:29 +0300" }
Cквозная аналитика
от сына маминой подруги
Будь лучшим в своем деле
Попробовать бесплатно
{ "id": 69765, "author_id": 259329, "diff_limit": 1000, "urls": {"diff":"\/comments\/69765\/get","add":"\/comments\/69765\/add","edit":"\/comments\/edit","remove":"\/admin\/comments\/remove","pin":"\/admin\/comments\/pin","get4edit":"\/comments\/get4edit","complain":"\/comments\/complain","load_more":"\/comments\/loading\/69765"}, "attach_limit": 2, "max_comment_text_length": 5000, "subsite_id": 199113, "last_count_and_date": null }
17 комментариев

Популярные

По порядку

Написать комментарий...
16

я маркетолог, а не программист

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

Все полученные факты будут верны тольк для группы Life News, но никак не помогут в других местах. Но и это не все

Лучшее время для постинга с 19:00 до 22:00

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

Не учитывать это нельзя, нельзя только на время смотреть

Посты без встроенной ссылки собирают больше активностей на пост и ER в 2 раза выше.

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

И потом в статье все прямо красиво про обработку, душа радуется. Надеюсь, вам не будет обидно из-за написанного выше

Ответить
1

Семён, спасибо за обратную связь. Насколько я понял, вы подумали, что я выводы написал про весь ВК, но это не так. Я показал как можно работать с каждым конкретным сообществом и все выводы делать под свои группы. А что по поводу ER действительно слабый вывод, стыдно за него)

Ответить
1

Да, вообще так и подумал

Для анализа одного конкретного сообщества там все по делу, парсинг красивый

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

Ответить
–3

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

Ответить
1

Давайте просто доделаем, запилим в онлайн сервис и будем продавать?

Ответить
0

Уже делаю) Но пока проблемы с клиент серверной архитектурой и отлаженным выполнением задач.

Ответить
0

Есть ссылка на гитхаб?

Ответить
0

Могу хелпануть

Ответить
0

Прикольный кейс) Вот ещё инструкция как работать с API VK через сервис RockStat: https://digitalgod.be/guides/vk_ads_api

Ответить
0

С удовольствием читал данный кейс. Более того воплотил его в жизнь на их платформе. Одно огорчает, что новые статьи пока не добавляются.

Ответить
0

А не пробовал просто подойти по другому к постам? Ну разбить их на тех кто с картинками, кто с видео?

Ответить
0

Тот случай, когда Jupyter используется не для хипстерского ML, а чтобы написать парсер (:

Ответить
0

Залейте на google colab свой ноутбук и не мучайте людей установкой анаконды )

Ответить
0

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

Ответить
0

Спасибо за статью, очень интересно.
Можно для тех, кто в душе блондинка, подробнее описать момент создания приложения и получения данных? выходит ошибка про неверный параметр request - видимо приложение не работает как надо.

Ответить
0

Скиньте ссылку на ваш notebook

Ответить
0
{ "page_type": "article" }

Прямой эфир

[ { "id": 1, "label": "100%×150_Branding_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox_method": "createAdaptive", "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "ezfl" } } }, { "id": 2, "label": "1200х400", "provider": "adfox", "adaptive": [ "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "ezfn" } } }, { "id": 3, "label": "240х200 _ТГБ_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "fizc" } } }, { "id": 4, "label": "240х200_mobile", "provider": "adfox", "adaptive": [ "phone" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "flbq" } } }, { "id": 5, "label": "300x500_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "ezfk" } } }, { "id": 6, "label": "1180х250_Interpool_баннер над комментариями_Desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "h", "ps": "bugf", "p2": "ffyh" } } }, { "id": 7, "label": "Article Footer 100%_desktop_mobile", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "fjxb" } } }, { "id": 8, "label": "Fullscreen Desktop", "provider": "adfox", "adaptive": [ "desktop", "tablet" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "fjoh" } } }, { "id": 9, "label": "Fullscreen Mobile", "provider": "adfox", "adaptive": [ "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "fjog" } } }, { "id": 10, "disable": true, "label": "Native Partner Desktop", "provider": "adfox", "adaptive": [ "desktop", "tablet" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fmyb" } } }, { "id": 11, "disable": true, "label": "Native Partner Mobile", "provider": "adfox", "adaptive": [ "phone" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fmyc" } } }, { "id": 12, "label": "Кнопка в шапке", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "p1": "bscsh", "p2": "fdhx" } } }, { "id": 13, "label": "DM InPage Video PartnerCode", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox_method": "createAdaptive", "adfox": { "ownerId": 228129, "params": { "pp": "h", "ps": "bugf", "p2": "flvn" } } }, { "id": 14, "label": "Yandex context video banner", "provider": "yandex", "yandex": { "block_id": "VI-223676-0", "render_to": "inpage_VI-223676-0-1104503429", "adfox_url": "//ads.adfox.ru/228129/getCode?pp=h&ps=bugf&p2=fpjw&puid1=&puid2=&puid3=&puid4=&puid8=&puid9=&puid10=&puid21=&puid22=&puid31=&puid32=&puid33=&fmt=1&dl={REFERER}&pr=" } }, { "id": 15, "label": "Плашка на главной", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox": { "ownerId": 228129, "params": { "p1": "byudx", "p2": "ftjf" } } }, { "id": 16, "label": "Кнопка в шапке мобайл", "provider": "adfox", "adaptive": [ "tablet", "phone" ], "adfox": { "ownerId": 228129, "params": { "p1": "byzqf", "p2": "ftwx" } } }, { "id": 17, "label": "Stratum Desktop", "provider": "adfox", "adaptive": [ "desktop" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "fzvb" } } }, { "id": 18, "label": "Stratum Mobile", "provider": "adfox", "adaptive": [ "tablet", "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "fzvc" } } }, { "id": 19, "label": "Тизер на главной", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "p1": "cbltd", "p2": "gazs" } } } ]
Компания отказалась от email
в пользу общения при помощи мемов
Подписаться на push-уведомления
{ "page_type": "default" }