Files
Zilant2025/vk_load.py
T
gitadmin ea6c5e5a7d
ci/woodpecker/push/woodpecker Pipeline was successful
[ZILANT] обработка пустых постов
2026-08-30 12:33:10 +03:00

334 lines
14 KiB
Python

import mysql.connector
import requests
import json
import os
import time
import random
from datetime import datetime
from mysql.connector import errorcode
from dotenv import load_dotenv
from ensure_db import ensure_database_structure
# Загрузка переменных окружения из .env файла
load_dotenv()
# Получение конфигурации из переменных окружения
VK_GROUP_ID = int(os.getenv('VK_GROUP_ID', -3342146))
VK_ACCESS_TOKEN = os.getenv('VK_ACCESS_TOKEN', '')
VK_API_VERSION = os.getenv('VK_API_VERSION', '5.131')
IS_EVENT = os.getenv('IS_EVENT', 'false').lower() in ('true', '1', 'yes', 'on')
AUTO_PUBLISH = os.getenv('AUTO_PUBLISH', 'false').lower() in ('true', '1', 'yes', 'on')
VK_NO_REPOST_TAG = os.getenv('VK_NO_REPOST_TAG', '') # Тег для отключения автопубликации
# Минимум букв (isalpha), чтобы пост не считался пустым (видео без описания и т.п.)
try:
VK_POST_MIN_LETTERS = int((os.getenv('VK_POST_MIN_LETTERS') or '5').strip().strip('"').strip("'"))
except ValueError:
VK_POST_MIN_LETTERS = 5
if VK_POST_MIN_LETTERS < 0:
VK_POST_MIN_LETTERS = 0
EMPTY_POST_SHORTNAME = "- = Пустой пост = -"
LOG_FILE = os.getenv('LOG_FILE', 'vk_loader.log') # Путь к лог-файлу
LOG_PREFIX = "VK_loader" # Уникальный префикс для идентификации скрипта
# Конфигурация MariaDB из .env
DB_CONFIG = {
'host': os.getenv('MDB_HOST', 'localhost'),
'user': os.getenv('MDB_USER', 'root'),
'password': os.getenv('MDB_PW', ''),
'database': os.getenv('MDBASE', 'vk_posts')
}
def log_message(message, max_retries=5, retry_delay=0.1):
"""
Записывает сообщение в лог-файл с обработкой блокировок
и идентификатором скрипта
"""
if not LOG_FILE:
return
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# Добавляем идентификатор скрипта к сообщению
log_line = f"[{timestamp}] [{LOG_PREFIX}] {message}\n"
for attempt in range(max_retries):
try:
with open(LOG_FILE, 'a', encoding='utf-8') as log:
log.write(log_line)
return True
except (IOError, OSError) as e:
if "locked" in str(e).lower() and attempt < max_retries - 1:
# Случайная задержка для уменьшения коллизий
sleep_time = retry_delay * (1 + random.random() * 0.5)
time.sleep(sleep_time)
else:
# Если не удалось записать после всех попыток
print(f"Ошибка записи в лог: {e}")
print(f"Сообщение для лога: {log_line.strip()}")
return False
def get_vk_posts():
"""Получает последние 10 постов из VK группы"""
url = 'https://api.vk.ru/method/wall.get'
params = {
'owner_id': VK_GROUP_ID,
'count': 20,
'access_token': VK_ACCESS_TOKEN,
'v': VK_API_VERSION,
'extended': 1
}
try:
response = requests.get(url, params=params, timeout=10)
response.raise_for_status()
data = response.json()
if 'error' in data:
error_msg = f"VK API Error: {data['error']['error_msg']}"
log_message(error_msg)
raise Exception(error_msg)
return data['response']['items']
except requests.exceptions.RequestException as e:
error_msg = f"Ошибка подключения к VK API: {e}"
log_message(error_msg)
raise Exception(error_msg)
def count_letters(text):
"""Количество букв в тексте (Unicode isalpha), без цифр и знаков."""
return sum(1 for c in (text or "") if c.isalpha())
def process_post(post):
"""Извлекает необходимые данные из поста VK, включая обработку репостов"""
post_id = post['id']
vk_post_url = f"https://vk.ru/wall{VK_GROUP_ID}_{post_id}"
published_at = datetime.utcfromtimestamp(post['date']).strftime('%Y-%m-%d %H:%M:%S')
# Проверка, является ли пост репостом
is_repost = 'copy_history' in post and len(post.get('copy_history', [])) > 0
# Если это репост, используем данные из оригинального поста
if is_repost:
original_post = post['copy_history'][0] # Берем первый элемент (обычно он один)
# Используем данные из оригинального поста
source_post = original_post
# Сохраняем информацию о том, что это репост, в тексте
repost_prefix = f"[Репост] "
original_owner_id = original_post.get('owner_id', 0)
original_post_id = original_post.get('id', 0)
original_url = f"https://vk.ru/wall{original_owner_id}_{original_post_id}"
repost_info = f"\n\n[Оригинальный пост: {original_url}]"
else:
source_post = post
repost_prefix = ""
repost_info = ""
# Инициализация полей опроса
is_poll = 0
poll_question = None
poll_options = None
poll_multiple = False
poll_end_date = None
# Инициализация поля изображения
image_url = None
# Обработка вложений из оригинального поста (если это репост) или текущего поста
for attachment in source_post.get('attachments', []):
if attachment['type'] == 'poll':
is_poll = 1
poll = attachment['poll']
poll_question = poll['question']
poll_options = json.dumps([answer['text'] for answer in poll['answers']])
poll_multiple = bool(poll.get('multiple', False))
if poll.get('end_date', 0) > 0:
poll_end_date = datetime.utcfromtimestamp(poll['end_date']).strftime('%Y-%m-%d %H:%M:%S')
elif attachment['type'] == 'photo' and not image_url:
# Берем только первое изображение
photo = attachment['photo']
sizes = photo.get('sizes', [])
if sizes:
# Выбираем изображение максимального качества
max_size = max(sizes, key=lambda s: s.get('width', 0) * s.get('height', 0))
image_url = max_size['url']
# Формируем текст поста: если это репост, добавляем префикс и информацию об оригинале
post_text = source_post.get('text', '')
own_text = post.get('text', '')
letter_count = count_letters(post_text)
if is_repost:
letter_count += count_letters(own_text)
# Если у репоста есть свой текст, добавляем его перед текстом оригинала
if own_text.strip():
post_text = f"{own_text}\n\n---\n\n{post_text}"
post_text = repost_prefix + post_text + repost_info
is_empty_text = letter_count < VK_POST_MIN_LETTERS
return {
'vk_post_id': post_id,
'text': post_text,
'image_url': image_url,
'vk_post_url': vk_post_url,
'published_at': published_at,
'is_poll': is_poll,
'poll_question': poll_question,
'poll_options': poll_options,
'poll_multiple': poll_multiple,
'poll_end_date': poll_end_date,
'is_repost': is_repost,
'is_empty_text': is_empty_text,
'letter_count': letter_count,
}
def save_to_database(posts):
"""Сохраняет новые посты в MariaDB с учетом настроек публикации"""
try:
conn = mysql.connector.connect(**DB_CONFIG)
cursor = conn.cursor()
new_posts_count = 0
for post in posts:
try:
# Проверка наличия тега VK_NO_REPOST_TAG в тексте поста
post_text = post.get('text', '')
is_empty_text = post.get('is_empty_text', False)
should_auto_publish = AUTO_PUBLISH
shortname = None
if is_empty_text:
should_auto_publish = False
shortname = EMPTY_POST_SHORTNAME
elif VK_NO_REPOST_TAG and VK_NO_REPOST_TAG in post_text:
# Если тег найден, отключаем автопубликацию независимо от AUTO_PUBLISH
should_auto_publish = False
# Добавляем поля is_event, marked_for_publication, shortname и action_number
cursor.execute('''
INSERT INTO posts (
vk_post_id, text, image_url, vk_post_url, published_at,
is_poll, poll_question, poll_options, poll_multiple, poll_end_date,
is_event, marked_for_publication, shortname, action_number
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE vk_post_id = VALUES(vk_post_id)
''', (
post['vk_post_id'],
post['text'],
post['image_url'],
post['vk_post_url'],
post['published_at'],
post['is_poll'],
post['poll_question'],
post['poll_options'],
int(post['poll_multiple']),
post['poll_end_date'],
IS_EVENT,
int(should_auto_publish),
shortname,
None # action_number
))
if cursor.rowcount > 0:
new_posts_count += 1
# Логируем добавление нового поста с указанием причины отключения автопубликации
auto_publish_reason = 'Да'
if not should_auto_publish:
if is_empty_text:
auto_publish_reason = (
f'Нет (пустой текст: {post.get("letter_count", 0)} букв '
f'< {VK_POST_MIN_LETTERS})'
)
elif VK_NO_REPOST_TAG and VK_NO_REPOST_TAG in post_text:
auto_publish_reason = 'Нет (найден тег ' + VK_NO_REPOST_TAG + ')'
else:
auto_publish_reason = 'Нет'
# Добавляем информацию о репосте в лог
repost_info = ""
if post.get('is_repost', False):
repost_info = ", Репост: Да"
else:
repost_info = ", Репост: Нет"
log_message(f"Добавлен новый пост: ID {post['vk_post_id']}, Дата: {post['published_at']}, Автопубликация: {auto_publish_reason}{repost_info}")
except mysql.connector.Error as err:
error_msg = f"Ошибка при вставке поста {post['vk_post_id']}: {err}"
log_message(error_msg)
continue
conn.commit()
return new_posts_count
except mysql.connector.Error as err:
error_msg = f"Ошибка подключения к базе данных: {err}"
log_message(error_msg)
return 0
finally:
if 'conn' in locals() and conn.is_connected():
cursor.close()
conn.close()
def vk_load_10():
"""
Экспортируемая функция для загрузки 10 последних постов из VK
Возвращает количество добавленных постов
"""
# Стартовая информация
start_msg = (
f"Старт скрипта | База: {DB_CONFIG['database']}@{DB_CONFIG['host']} | "
f"IS_EVENT: {IS_EVENT} | AUTO_PUBLISH: {AUTO_PUBLISH} | "
f"VK_POST_MIN_LETTERS: {VK_POST_MIN_LETTERS}"
)
print(start_msg)
log_message(start_msg)
# Обеспечиваем структуру БД
try:
ensure_database_structure()
except Exception as e:
error_msg = f"Не удалось создать структуру базы данных: {e}"
print(error_msg)
log_message(error_msg)
return 0
# Получаем посты из VK
try:
vk_posts = get_vk_posts()
status_msg = f"Получено {len(vk_posts)} постов из VK"
print(status_msg)
log_message(status_msg)
except Exception as e:
error_msg = f"Ошибка при получении данных из VK: {e}"
print(error_msg)
log_message(error_msg)
return 0
# Обрабатываем и сохраняем посты
processed_posts = [process_post(post) for post in vk_posts]
new_count = save_to_database(processed_posts)
# Фиксируем результат
if new_count > 0:
result_msg = f"Добавлено {new_count} новых постов в базу данных"
else:
result_msg = "Новые посты не обнаружены"
print(result_msg)
log_message(result_msg)
# Завершение работы
end_msg = "Работа скрипта завершена"
print(end_msg)
log_message(end_msg)
return new_count
def main():
"""Основная функция для запуска скрипта из командной строки"""
vk_load_10()
if __name__ == "__main__":
main()