|
import gradio as gr |
|
import re |
|
from collections import Counter |
|
from datetime import datetime |
|
import emoji |
|
import logging |
|
from typing import Tuple, List, Optional |
|
import statistics |
|
import csv |
|
from io import StringIO |
|
|
|
|
|
logging.basicConfig(level=logging.INFO) |
|
logger = logging.getLogger(__name__) |
|
|
|
def clean_text(text): |
|
"""Очищает текст от лишних пробелов и переносов строк""" |
|
text = re.sub(r'\s+', ' ', text) |
|
return text.strip() |
|
|
|
def count_emojis(text): |
|
"""Подсчитывает количество эмодзи в тексте""" |
|
return len([c for c in text if c in emoji.EMOJI_DATA]) |
|
|
|
def extract_mentions(text): |
|
"""Извлекает упоминания пользователей из текста""" |
|
return re.findall(r'@[\w\.]+', text) |
|
|
|
def get_comment_words(text): |
|
"""Получает список слов из комментария для анализа""" |
|
words = re.findall(r'\w+', text.lower()) |
|
return [w for w in words if len(w) > 2] |
|
|
|
def analyze_sentiment(text): |
|
"""Расширенный анализ тональности по эмодзи и ключевым словам""" |
|
positive_indicators = ['🔥', '❤️', '👍', '😊', '💪', '👏', '🎉', '♥️', '😍', '🙏', |
|
'круто', 'супер', 'класс', 'огонь', 'пушка', 'отлично', 'здорово', |
|
'прекрасно', 'молодец', 'красота', 'спасибо', 'топ', 'лучший', |
|
'amazing', 'wonderful', 'great', 'perfect', 'love', 'beautiful'] |
|
|
|
negative_indicators = ['👎', '😢', '😞', '😠', '😡', '💔', '😕', '😑', |
|
'плохо', 'ужас', 'отстой', 'фу', 'жесть', 'ужасно', |
|
'разочарован', 'печаль', 'грустно', 'bad', 'worst', |
|
'terrible', 'awful', 'sad', 'disappointed'] |
|
|
|
text_lower = text.lower() |
|
|
|
|
|
positive_count = sum(1 for ind in positive_indicators if ind in text_lower) |
|
negative_count = sum(1 for ind in negative_indicators if ind in text_lower) |
|
|
|
|
|
exclamation_count = text.count('!') |
|
if positive_count > negative_count: |
|
positive_count += exclamation_count * 0.5 |
|
elif negative_count > positive_count: |
|
negative_count += exclamation_count * 0.5 |
|
|
|
|
|
if positive_count > negative_count: |
|
return 'positive' |
|
elif negative_count > positive_count: |
|
return 'negative' |
|
return 'neutral' |
|
|
|
def extract_comment_data(comment_text): |
|
"""Извлекает данные из отдельного комментария с поддержкой различных форматов""" |
|
try: |
|
|
|
username_patterns = [ |
|
r"Фото профиля ([^\n]+)", |
|
r"^([^\s]+)\s+", |
|
r"@([^\s]+)\s+", |
|
] |
|
|
|
time_patterns = [ |
|
r"(\d+)\s*(?:ч|нед)\.", |
|
r"(\d+)\s*(?:h|w)", |
|
r"(\d+)\s*(?:час|hour|week)", |
|
] |
|
|
|
likes_patterns = [ |
|
r"(\d+) отметк[аи] \"Нравится\"", |
|
r"Нравится: (\d+)", |
|
r"(\d+) отметка \"Нравится\"", |
|
r"\"Нравится\": (\d+)", |
|
r"likes?: (\d+)", |
|
] |
|
|
|
|
|
username = None |
|
for pattern in username_patterns: |
|
username_match = re.search(pattern, comment_text) |
|
if username_match: |
|
username = username_match.group(1).strip() |
|
break |
|
|
|
if not username: |
|
return None, None, 0, 0 |
|
|
|
|
|
comment = comment_text |
|
|
|
|
|
metadata_patterns = [ |
|
r"Фото профиля [^\n]+\n", |
|
r"\d+\s*(?:ч|нед|h|w|час|hour|week)\.", |
|
r"Нравится:?\s*\d+", |
|
r"\d+ отметк[аи] \"Нравится\"", |
|
r"Ответить", |
|
r"Показать перевод", |
|
r"Скрыть все ответы", |
|
r"Смотреть все ответы \(\d+\)", |
|
username |
|
] |
|
|
|
for pattern in metadata_patterns: |
|
comment = re.sub(pattern, '', comment) |
|
|
|
comment = clean_text(comment) |
|
|
|
|
|
weeks = 0 |
|
for pattern in time_patterns: |
|
time_match = re.search(pattern, comment_text) |
|
if time_match: |
|
time_value = int(time_match.group(1)) |
|
if any(unit in comment_text.lower() for unit in ['нед', 'w', 'week']): |
|
weeks = time_value |
|
else: |
|
weeks = time_value / (24 * 7) |
|
break |
|
|
|
|
|
likes = 0 |
|
for pattern in likes_patterns: |
|
likes_match = re.search(pattern, comment_text) |
|
if likes_match: |
|
likes = int(likes_match.group(1)) |
|
break |
|
|
|
return username, comment.strip(), likes, weeks |
|
|
|
except Exception as e: |
|
logger.error(f"Error extracting comment data: {e}") |
|
return None, None, 0, 0 |
|
|
|
def analyze_post(content_type: str, link_to_post: str, post_likes: int, post_date: str, |
|
description: str, comment_count: int, all_comments: str) -> Tuple[str, str, str, str, str]: |
|
""" |
|
Анализирует пост Instagram и его комментарии |
|
|
|
Args: |
|
content_type: Тип контента (фото/видео) |
|
link_to_post: Ссылка на пост |
|
post_likes: Количество лайков поста |
|
post_date: Дата публикации |
|
description: Описание поста |
|
comment_count: Ожидаемое количество комментариев |
|
all_comments: Текст всех комментариев |
|
|
|
Returns: |
|
Tuple[str, str, str, str, str]: Кортеж с результатами анализа |
|
""" |
|
try: |
|
|
|
comment_patterns = [ |
|
r"(?=Фото профиля)", |
|
r"(?=\n\s*[a-zA-Z0-9._]+\s+[^\n]+\n)", |
|
r"(?=^[a-zA-Z0-9._]+\s+[^\n]+\n)", |
|
r"(?=@[a-zA-Z0-9._]+\s+[^\n]+\n)" |
|
] |
|
|
|
split_pattern = '|'.join(comment_patterns) |
|
comments_blocks = re.split(split_pattern, all_comments) |
|
comments_blocks = [block.strip() for block in comments_blocks if block and block.strip()] |
|
|
|
|
|
usernames = [] |
|
comments = [] |
|
likes = [] |
|
weeks = [] |
|
total_emojis = 0 |
|
mentions = [] |
|
sentiments = [] |
|
comment_lengths = [] |
|
words_per_comment = [] |
|
all_words = [] |
|
user_engagement = {} |
|
|
|
|
|
for block in comments_blocks: |
|
if 'Скрыто алгоритмами Instagram' in block: |
|
continue |
|
|
|
username, comment, like_count, week_number = extract_comment_data(block) |
|
if username and comment: |
|
usernames.append(username) |
|
comments.append(comment) |
|
likes.append(str(like_count)) |
|
weeks.append(week_number) |
|
|
|
|
|
total_emojis += count_emojis(comment) |
|
mentions.extend(extract_mentions(comment)) |
|
sentiment = analyze_sentiment(comment) |
|
sentiments.append(sentiment) |
|
comment_lengths.append(len(comment)) |
|
|
|
words = get_comment_words(comment) |
|
words_per_comment.append(len(words)) |
|
all_words.extend(words) |
|
|
|
|
|
if username not in user_engagement: |
|
user_engagement[username] = { |
|
'comments': 0, |
|
'total_likes': 0, |
|
'emoji_usage': 0, |
|
'avg_length': 0, |
|
'sentiments': [], |
|
'weeks': [] |
|
} |
|
|
|
user_stats = user_engagement[username] |
|
user_stats['comments'] += 1 |
|
user_stats['total_likes'] += like_count |
|
user_stats['emoji_usage'] += count_emojis(comment) |
|
user_stats['avg_length'] += len(comment) |
|
user_stats['sentiments'].append(sentiment) |
|
user_stats['weeks'].append(week_number) |
|
|
|
|
|
total_comments = len(comments) |
|
if total_comments == 0: |
|
return "No comments found", "", "", "", "0" |
|
|
|
|
|
for username in user_engagement: |
|
stats = user_engagement[username] |
|
stats['avg_length'] /= stats['comments'] |
|
stats['engagement_rate'] = stats['total_likes'] / stats['comments'] |
|
stats['sentiment_ratio'] = sum(1 for s in stats['sentiments'] if s == 'positive') / len(stats['sentiments']) |
|
stats['activity_period'] = max(stats['weeks']) - min(stats['weeks']) if stats['weeks'] else 0 |
|
|
|
|
|
avg_comment_length = sum(comment_lengths) / total_comments |
|
sentiment_distribution = Counter(sentiments) |
|
most_active_users = Counter(usernames).most_common(5) |
|
most_mentioned = Counter(mentions).most_common(5) |
|
avg_likes = sum(map(int, likes)) / len(likes) if likes else 0 |
|
|
|
|
|
if weeks: |
|
earliest_week = max(weeks) |
|
latest_week = min(weeks) |
|
week_range = earliest_week - latest_week |
|
|
|
|
|
period_length = week_range / 3 if week_range > 0 else 1 |
|
engagement_periods = { |
|
'early': [], |
|
'middle': [], |
|
'late': [] |
|
} |
|
|
|
for i, week in enumerate(weeks): |
|
if week >= earliest_week - period_length: |
|
engagement_periods['early'].append(i) |
|
elif week >= earliest_week - 2 * period_length: |
|
engagement_periods['middle'].append(i) |
|
else: |
|
engagement_periods['late'].append(i) |
|
|
|
period_stats = { |
|
period: { |
|
'comments': len(indices), |
|
'avg_likes': sum(int(likes[i]) for i in indices) / len(indices) if indices else 0, |
|
'sentiment_ratio': sum(1 for i in indices if sentiments[i] == 'positive') / len(indices) if indices else 0 |
|
} |
|
for period, indices in engagement_periods.items() |
|
} |
|
else: |
|
period_stats = {} |
|
earliest_week = 0 |
|
latest_week = 0 |
|
|
|
|
|
csv_data = { |
|
'metadata': { |
|
'content_type': content_type, |
|
'link': link_to_post, |
|
'post_likes': post_likes, |
|
'post_date': post_date, |
|
'total_comments': total_comments, |
|
'expected_comments': comment_count |
|
}, |
|
'basic_stats': { |
|
'avg_comment_length': round(avg_comment_length, 2), |
|
'median_comment_length': statistics.median(comment_lengths), |
|
'avg_words': round(sum(words_per_comment) / total_comments, 2), |
|
'total_emojis': total_emojis, |
|
'avg_likes': round(avg_likes, 2) |
|
}, |
|
'sentiment_stats': dict(Counter(sentiments)), |
|
'period_analysis': period_stats, |
|
'top_users': dict(most_active_users), |
|
'top_mentioned': dict(most_mentioned) |
|
} |
|
|
|
|
|
output = StringIO() |
|
writer = csv.writer(output) |
|
for section, data in csv_data.items(): |
|
writer.writerow([section]) |
|
for key, value in data.items(): |
|
writer.writerow([key, value]) |
|
writer.writerow([]) |
|
csv_output = output.getvalue() |
|
|
|
|
|
analytics_summary = ( |
|
f"CSV DATA:\n{csv_output}\n\n" |
|
f"ДЕТАЛЬНЫЙ АНАЛИЗ:\n" |
|
f"Контент: {content_type}\n" |
|
f"Ссылка: {link_to_post}\n\n" |
|
f"СТАТИСТИКА:\n" |
|
f"- Всего комментариев: {total_comments} (ожидалось: {comment_count})\n" |
|
f"- Всего лайков на комментариях: {sum(map(int, likes))}\n" |
|
f"- Среднее лайков на комментарий: {avg_likes:.1f}\n" |
|
f"- Период активности: {earliest_week}-{latest_week} недель\n\n" |
|
f"АНАЛИЗ КОНТЕНТА:\n" |
|
f"- Средняя длина комментария: {avg_comment_length:.1f} символов\n" |
|
f"- Медиана длины: {statistics.median(comment_lengths)} символов\n" |
|
f"- Среднее количество слов: {sum(words_per_comment) / total_comments:.1f}\n" |
|
f"- Всего эмодзи: {total_emojis}\n" |
|
f"- Тональность:\n" |
|
f" * Позитивных: {sentiment_distribution['positive']}\n" |
|
f" * Нейтральных: {sentiment_distribution['neutral']}\n" |
|
f" * Негативных: {sentiment_distribution['negative']}\n\n" |
|
f"АНАЛИЗ КОНТЕНТА:\n" |
|
f"- Средняя длина: {avg_comment_length:.1f} символов\n" |
|
f"- Медиана длины: {median_comment_length} символов\n" |
|
f"- Среднее слов: {avg_words_per_comment:.1f}\n" |
|
f"- Эмодзи: {total_emojis}\n" |
|
f"- Тональность:\n" |
|
f" * Позитив: {sentiment_distribution['positive']}\n" |
|
f" * Нейтрально: {sentiment_distribution['neutral']}\n" |
|
f" * Негатив: {sentiment_distribution['negative']}\n\n" |
|
f"ПОПУЛЯРНЫЕ СЛОВА:\n" |
|
+ "\n".join([f"- {word}: {count}" for word, count in common_words]) + "\n\n" |
|
f"АКТИВНЫЕ ПОЛЬЗОВАТЕЛИ:\n" |
|
+ "\n".join([f"- {user}: {count}" for user, count in most_active_users]) + "\n\n" |
|
f"УПОМИНАНИЯ:\n" |
|
+ "\n".join([f"- {user}: {count}" for user, count in most_mentioned if user]) + "\n\n" |
|
f"АНАЛИЗ ПО ПЕРИОДАМ:\n" |
|
+ "\n".join([f"- {period}: {stats['comments']} комментариев, {stats['avg_likes']:.1f} лайков/коммент, " |
|
f"{stats['sentiment_ratio']*100:.1f}% позитивных" |
|
for period, stats in period_stats.items()]) + "\n\n" |
|
f"ЭКСПЕРИМЕНТАЛЬНАЯ АНАЛИТИКА:\n" |
|
f"- Самый активный период: {max(period_stats.items(), key=lambda x: x[1]['comments'])[0]}\n" |
|
f"- Наиболее позитивный период: {max(period_stats.items(), key=lambda x: x[1]['sentiment_ratio'])[0]}\n" |
|
f"- Период с макс. вовлеченностью: {max(period_stats.items(), key=lambda x: x[1]['avg_likes'])[0]}" |
|
) |
|
|
|
return analytics_summary, "\n".join(usernames), "\n".join(comments), "\n".join(likes), str(sum(map(int, likes))) |
|
|
|
except Exception as e: |
|
logger.error(f"Error in analyze_post: {e}", exc_info=True) |
|
return f"Error: {str(e)}", "", "", "", "0" |
|
|
|
|
|
iface = gr.Interface( |
|
fn=analyze_post, |
|
inputs=[ |
|
gr.Radio(choices=["Photo", "Video"], label="Content Type", value="Photo"), |
|
gr.Textbox(label="Link to Post"), |
|
gr.Number(label="Likes", value=0), |
|
gr.Textbox(label="Post Date"), |
|
gr.Textbox(label="Description", lines=3), |
|
gr.Number(label="Total Comment Count", value=0), |
|
gr.Textbox(label="All Comments", lines=10) |
|
], |
|
outputs=[ |
|
gr.Textbox(label="Analytics Summary", lines=20), |
|
gr.Textbox(label="Usernames"), |
|
gr.Textbox(label="Comments"), |
|
gr.Textbox(label="Likes Chronology"), |
|
gr.Textbox(label="Total Likes on Comments") |
|
], |
|
title="Enhanced Instagram Comment Analyzer", |
|
description="Анализатор комментариев Instagram с расширенной аналитикой и CSV-форматированием" |
|
) |
|
|
|
if __name__ == "__main__": |
|
iface.launch() |