334 lines
14 KiB
Python
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()
|