Вводная

Привет. У меня есть несколько групп в ВК, в которые нужно периодически публиковать посты. В целом мое желание можно описать фразой «А когда мне это делать, если я все время не хочу». Мотивация в моем случае это лень.

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

Требования

Контент, который я публикую это тематические фото и изображения с короткими комментариями. На данный момент все разложено по папкам. Альбомы и картинки имеют свои порядковые номера.

Что важно отразить в алгоритме:

  • публикацию постов через временной интервал;

  • разместить недельный план публикаций с комментариями к постам;

  • возможность опубликовать пост без очереди;

  • публиковать на выбор одну или несколько изображений;

  • хранить историю опубликованных фото и комментариев;

  • автопостинг для нескольких групп.

Логика

Все пункты требований я постарался вместить в три основных этапа:

логика работы
логика работы
  1. Проверка временного интервала публикаций. Проверяем текущую дату и время с временем последней опубликованной записью. Получаем интервал и сравниваем его с целевым.

  2. Определить очередь публикации. Сравниваем количество публикаций по плану в день и по факту из истории публикаций.

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

Работая над таблицами задался вопросом: «А что должен делать скрипт, когда все фото будут опубликованы?». Решил, что нужно предусмотреть отдельную таблицу в БД, которая будет отображать количества фото в альбоме.

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

Схема работы приложения

Общая схема
Общая схема

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

Итоговая логика работы скрипта у меня получилась следующая:

Схема работы скрипта
Схема работы скрипта

Наполнение и таблицы

Очередь для публикаций будет выглядеть следующим образом:

- день недели;

- номер альбома с картинками (отсортированы по нарастающей);

- количество фото для публикации;

- признак: фото одиночное или тематически объединены в несколько по умолчанию.

Пример недельного плана публикаций
Пример недельного плана публикаций

Иерархия размещения альбомов с картинками:

Пример организации изображений
Пример организации изображений

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

  • history_post -  история опубликованных постов

  • line_post – план недельной публикации

  • out_of_line_post – поставить публикацию вне плана

  • total_photos – количество фото в альбоме

  • comment_photo – комментарии к картинкам из альбомов

Таблицы со столбцами
Таблицы со столбцами

Более детальное описание столбцов оставлю ниже.

history_post

История опубликованных постов.

Столбец

Тип

Описание

id_push

integer

PRIMARY KEY, порядковый номер публикации в таблице

id_albom

smallint

Номер альбом с картинками

id_pic

smallint

Номер (название) картинки

comment

text

Комментарий

line

boolean

Пост из плана публикаций

time_push

timestamp with time zone

Время публикации

id_group

iinteger

ID группы

union_photos

boolean

Фото опубликовано одно или несколько

line_post

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

Столбец

Тип

Описание

id_day

smallint

День недели

id_albom

smallint

Номер альбом с картинками

count_photo

smallint

Количество фото для публикаций.

union_photos

boolean

Фото одно или несколько

start_date

timestamp with time zone

Дата начала

finish_date

timestamp with time zone

Дата завершения

id_row

smallint

PRIMARY KEY, порядковый номер в таблице

id_group

integer

ID группы

out_of_line_post

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

Столбец

Тип

Описание

id_albom

smallint

Номер альбом с картинками

id_photo

smallint

Номер (название) картинки

comment

text

Комментарий

data

timestamp with time zone

Дата когда необходима публикация

status

boolean

False – неопубликованно, True - опубликовано  

id_row

smallint

PRIMARY KEY, порядковый номер в таблице

id_group

integer

ID группы

union_photos

boolean

Фото одно или несколько

total_photos

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

Столбец

Тип

Описание

id_albom

smallint

Номер альбом с картинками

count_photo

smallint

Количество фото в альбоме.

date

timestamp with time zone

Дата

id_row

smallint

PRIMARY KEY, порядковый номер в таблице

id_group

integer

ID группы

union_photos

boolean

Фото одно или несколько

comment_photo

Храним комментарии к изображениям.

Столбец

Тип

Описание

id_albom

smallint

Номер альбом с картинками

id_photo

smallint

Количество фото для публикаций.

date

timestamp with time zone

Дата

id_row

smallint

PRIMARY KEY, порядковый номер в таблице

id_group

integer

ID группы

union_photos

boolean

Фото одно или несколько

Разработка

Файлы и их назначение.

сonfig.yaml – файл со всеми настройками. БД, Grafana, VK.
config_table.json – справочник с названиями таблиц и столбцов в БД
main.py – запуск приложения
utils.py – основная логика
sql_query.py – запросы к базе
manager_DB.py – подключение к базе и запись строк в БД
GetPic.py – определение картинок для публикации
loadVkConnent.py – публикация контента в VK
logi.py – тексты с логами

Не буду подробно разбирать каждый файл, остановлюсь только на utils.py, все файлы будут на GitLab

Создаем класс ManagerPost, который наследует все остальные классы, а именно:

• manager_DB – подключение к базе и запись строк в БД
• GP - определение картинок для публикации
• SqlMan - запросы к базе
• VkMan - публикация контента в VK
• Log – логи

Создаем функции:

CheckTime.

По действиям:

• Выгружаем кол-во постов, которые необходимо опубликовать;
• Определяем текущие время и время публикации последнего поста;
• Через If определяем время с учетом количества постов в день и какой интервал между нами. Если True продолжаем скрипт, если нет выходим из программы.

Если необходимо справочник TimeIntervalDict можно изменить под ваши потребности.

class ManagerPost(manager_DB,GP,SqlMan,VkMan,Log):
    "класс для управленеия постановкой постов"
    
    def CheckTime (self):
        """определяем сколько разница по времени должна быть
        исходя из количества планируемых постов
        возвращает True если необходимое время прошло"""
        
        CountPost = str(pd.read_sql(SqlMan.CountPostDay(self), self.conn)['count'][0])
        TimeIntervalDict = {'2': 13, 
                    '3': 6,
                    '4': 5,
                    }
        
        Today = datetime.now()
       
        TimePost = pd.read_sql(SqlMan.GetTimePostFromLine(self), self.conn)['time_push'][0]
       
        if (Today - TimePost) > timedelta(hours=TimeIntervalDict[CountPost]):
            Log.TimeCorrect(self)
            return 
        else: 
            Log.TimeLess(self,Today,TimePost,timedelta(hours=TimeIntervalDict[CountPost]))
            sys.exit()

GetInfoPost

• Получаем пост, который должен быть опубликован. Если DataFrama пустой, то отправляем лог, о том, что все посты уже опубликованы и завершаем скрипт
• По номеру альбома получаем все ранее опубликованные фото и общие количество картинок в альбоме
• Складываем количество опубликованных фото + кол-во которое нужно опубликовать в текущей сессии. Если значение получается больше (True), чем фото, то в альбоме запускается ветка переопределения количества фото в альбоме.
На выход выплевывается информация о посте который нужно опубликовать и список опубликованных картинок.

def GetInfoPost(self):
      """Проверка количество уже опубликованных фото.
      Если все фото почти опубликованы, обнуляем в БД запись с новым 
      количеством фото"""

      InfoPost = pd.read_sql(SqlMan.GetInfoPostSQL(self), self.conn)

      if InfoPost.shape[0] == 0:
          Log.AllAlbomPush(self)
          sys.exit()
      IdAlbom = InfoPost['id_albom'][0]
      IdPhotoPush = pd.read_sql(SqlMan.GetIdPic(self,  IdAlbom), self.conn)
      ListPhotoPush = IdPhotoPush['id_pic'].to_list()
      
      BoolCheck = (len(IdPhotoPush) + InfoPost['count_photo'][0]) >= InfoPost['total_photo'][0]
      
      # переопределяем количество фото в таблице
      if  BoolCheck == True:
          Date = datetime.now(timezone.utc)
 
          if InfoPost['union_photos'][0] == False:
              AlbomDir = Path(self.WithoutUnion, str(IdAlbom))
              CountPhotoFromAlbom = len([name for name in os.listdir(AlbomDir) if os.path.isfile(os.path.join(AlbomDir, name))])
          else:
              AlbomDir = Path(self.WithUnion, str(IdAlbom))
              CountPhotoFromAlbom = len(set([filename.split('-')[0] for filename in os.listdir(AlbomDir)]))

              
          LoadConnent = [[IdAlbom, CountPhotoFromAlbom, 'TIMESTAMP WITH TIME ZONE ', self.IdGroupQuery ,InfoPost['union_photos'][0]]]

          mananger_DB.UploadRowDB(self,self.ConfigTable['total_photos'],LoadConnent, Date) 

          Log.NewCountPhoto(self,IdAlbom,CountPhotoFromAlbom)
          ListPhotoPush = []

      return InfoPost, ListPhotoPush

GetComment

• По номеру альбома и картинки получаем таблицу с комментариями из БД, если запись отсутствует оставляем NULL;
• Получаем историю ранее опубликованных комментариев к фото;
• Далее объединяем два df и выбираем комментарий, который публиковался реже всех.

def GetCommment(self, InfoPost, BoxNewPic):
    "определяем комменатрий для публикации"

    try:
        CommentDf = pd.DataFrame(\
                pd.read_sql(SqlMan.GetIdCom(self,InfoPost, BoxNewPic, True), self.conn)['comment'][0].split(';'),\
                columns=['comment'])
    except IndexError:
        comment = 'NULL'
        return  comment
    
    # вытаскиваем все комментарии
    CommentHistory = pd.read_sql(SqlMan.GetIdCom(self,InfoPost, BoxNewPic, False), self.conn)


    # считаем комментарии
    CommentBox = CommentDf.merge(CommentHistory, on='comment', how='left').fillna(0).sort_values(by='count').reset_index(drop=True)
    Log.CommentPhoto(self, CommentBox)

    return  CommentBox['comment'][0]

CommitPost

Эта сборка алгоритма. Получилась избыточная функция и на мой взгляд ее надо переписать и разбить на 2-3 более мелких. Но раз работает, решил не трогать. Прокомментирую ее по частям.
• проверяем время публикации;
• определяем если ли у нас публикации вне плана;
• если True, проверяем совпадения дат. Вытаскиваем коммент, определяем фото и отправляем на публикацию;
• если False, определяем фото и комментарий из плана публикаций;

def CommitPost(self):
        
    # проверяем новые посты и время
    ManagerPost.CheckTime(self)

    InfoPost = pd.read_sql(SqlMan.GetNewLinePost(self), self.conn)
    today = datetime.now()
    
    # сценарий если новая фото
    if InfoPost[InfoPost['date']==today.date()].shape[0] != 0:

        Line = False
        if InfoPost['comment'][0] != None:
            Comment = f"{InfoPost['comment'][0]}"
        else:
            Comment = 'NULL'
        
        if InfoPost['union_photos'][0] == True:
            NewIdPic, BoxNewPic, DirAlbom = GP.GetPicUnionTrue(self,InfoPost,[], True)
        else:
            BoxNewPic, DirAlbom = GP.GetPicUnionFalse(self,InfoPost,[],True)
        
        self.cursor.execute(SqlMan.ChahgeStatusNewPost(self))
        self.conn.commit()

    else:
        # сценарий если в рамках линии
        Log.NewPostNotExist(self)
        InfoPost, IdPhotoPush = ManagerPost.GetInfoPost(self)
        Line = True

        if InfoPost['union_photos'][0] == True:
            NewIdPic, BoxNewPic, DirAlbom = GP.GetPicUnionTrue(self,InfoPost,IdPhotoPush)
        else:
            BoxNewPic, DirAlbom = GP.GetPicUnionFalse(self,InfoPost,IdPhotoPush)
        
        # получаем номер фото и коммент
        Log.CommentPhoto(self, BoxNewPic)
        Comment = ManagerPost.GetCommment(self,InfoPost, BoxNewPic[0])

• определяем время для отложенного постинга в vk;
• далее грузим данные в БД. Развилка по алгоритму в поле InfoPost['union_photos'] (фото одно или несколько тематически связанных);
• закрываем соединения с базой;

# определяем время
minute = randint(5, 59)
Date = datetime(today.year, today.month, today.day, today.hour, minute)

BoxWithPath = []

# отображаем время публикации в БД (history_post)
if InfoPost['union_photos'][0] == False:
    for i in range(len(BoxNewPic)):
        LoadConnent = [
        [InfoPost['id_albom'][0], 
        BoxNewPic[i],
        Comment.strip(),
        Line, 
        'TIMESTAMP WITH TIME ZONE ',
        self.GroupIdForDB,
        InfoPost['union_photos'][0]]
        ]
        manager_DB.UploadRowDB(self, self.ConfigTable['history_post'],LoadConnent, Date)
        Log.PostUploudDB(self)
        BoxWithPath.append(DirAlbom + str('\\') + str(BoxNewPic[i]) + str('.jpg'))
else:
        LoadConnent = [
        [InfoPost['id_albom'][0], 
        NewIdPic,
        Comment.strip(),
        Line, 
        'TIMESTAMP WITH TIME ZONE ',
        self.GroupIdForDB,
        InfoPost['union_photos'][0]]
        ]
    
        manager_DB.UploadRowDB(self, self.ConfigTable['history_post'],LoadConnent, Date)
        Log.PostUploudDB(self)

        for i in range(len(BoxNewPic)):
            BoxWithPath.append(DirAlbom + str('\\') + str(BoxNewPic[i]) + str('.jpg'))

manager_DB.CoonClose(self)
Log.InfoPostLog(self,InfoPost['id_albom'][0],BoxNewPic,Comment,Date)
        

• переводим дату для формата публикации в VK;
• загружаем фото в VK оставляем комментарий и ставим публикацию на таймер;
• отправляем лог с деталями публикации.

unixtime = time.mktime(Date.timetuple())
VkMan.LoadVkСontent(self,BoxWithPath,unixtime,Comment)
Log.PostUploudVK(self)

return 

Развернуть

Чтобы не нагружать читателя еще текстом, о том, как я настроил БД, Grafana Loki, загрузил картинки и запустил код на сервере я напишу в отдельной статье.

Результат

Цель достигнута посты опубликовываются.

История сохраняется.

пример заполнения истории публикаций
пример заполнения истории публикаций

Что можно доработать?

«Умные мысли часто преследовали его, но он был быстрее». Когда все было написано и протестировано, я подумал, что можно было сделать:

  • контент с изображениями хранить в облаке;

В текущем решение, изображение нужно хранить локально. Если вам необходимо добавить/удалить картинки, то нужно обновлять их локально. Как оказалось, на практике это неудобно.

  • функция для загрузки и выгрузки из БД плана публикаций, комментарий, количество фото;

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

  • уведомление в случае ошибки;

Все логи отправляются на Grafana Loki, при тестировании появилась потребность настроить уведомления на почту в случае ошибки.

  • обработать ошибки при загрузках в VK;

За все несколько недель тестирования 1-2 раза отлетал api vk или происходил сбой при загрузке картинок. В таком случае в БД отображается строка, что публикация прошла успешно, а в реальности ошибка. Возможно имеет смысл обработать эту ошибку и удалить последнею строку в БД.

  • предусмотреть несколько расширений для изображений.

На данный момент приложение работает только с расширением jpg для остальных форматов картинок скрипт выдаст ошибку. Можно переписать часть кода, чтобы сделать его универсальным. У меня такой потребности нет, но думаю она не будет лишний.

Заключение

Скрипт работает и цель автопостинга достигнута. Возможно мое решение поможет кому-то высвободить свое время. Наверное скрипт можно использовать для любой социальной сети у которой есть api адаптировав код под свои нужны. Если у вас есть идеи, предложения как можно улучшить скрипт или есть свое решение пишите в комментариях.

Код на GitHub

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