one start
This commit is contained in:
182
.gitignore
vendored
Normal file
182
.gitignore
vendored
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
# Файлы и папки конфигурации Obsidian, создаваемые редактором заметок.
|
||||||
|
.obsidian
|
||||||
|
/.obsidian/
|
||||||
|
.obsidian/*
|
||||||
|
|
||||||
|
# Отдельные файлы (например, для хранения задач)
|
||||||
|
TODOS.md
|
||||||
|
|
||||||
|
# Зависимости, генерируемые NPM и yarn, а также сборочные артефакты.
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Конфигурации IDE и редакторов, чтобы не засорять репозиторий личными настройками.
|
||||||
|
.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
.fleet/
|
||||||
|
|
||||||
|
# Конфигурационные файлы для Visual Studio Code, кроме файлов настроек проекта.
|
||||||
|
.vs
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# Системные файлы, генерируемые разными ОС.
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Лог-файлы, создаваемые NPM и yarn в случае ошибок.
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Виртуальные окружения Python.
|
||||||
|
.venv
|
||||||
|
/venv
|
||||||
|
.env
|
||||||
|
/env
|
||||||
|
|
||||||
|
# Файлы SQLite базы данных.
|
||||||
|
db.sqlite3
|
||||||
|
|
||||||
|
# Файлы логов, используемые для отладки локального сервера.
|
||||||
|
*.log
|
||||||
|
*.log.*
|
||||||
|
|
||||||
|
# Исключения файлов, заканчивающихся на "_00"
|
||||||
|
*_00
|
||||||
|
|
||||||
|
# ---> Python
|
||||||
|
|
||||||
|
# Скомпилированные и оптимизированные файлы, которые не следует коммитить.
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C-расширения.
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Артефакты сборки и папки для управления пакетами Python.
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# Файлы PyInstaller, содержащие шаблоны для создания exe.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Логи установщика пакетов.
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Отчеты о покрытии тестами и кэши тестирования.
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Файлы локализации.
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Файлы, относящиеся к Django, Flask и другим фреймворкам.
|
||||||
|
local_settings.py
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Файлы, генерируемые при сборке документации Sphinx.
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# Директория PyBuilder.
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Папки для кэша и контрольных точек в Jupyter Notebook.
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# Файлы и папки IPython.
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# Управление версиями Python (pyenv).
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# Конфигурации pipenv, poetry, pdm и pytype.
|
||||||
|
.pdm.toml
|
||||||
|
__pypackages__/
|
||||||
|
poetry.lock
|
||||||
|
pdm.lock
|
||||||
|
|
||||||
|
# Файлы, генерируемые системами очередей и кэшей (например, Celery).
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# Символы отладки Cython.
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# Кэш анализа типов.
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
.pyre/
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Папки и файлы конфигураций, создаваемые PyCharm и другими JetBrains IDE.
|
||||||
|
.idea/
|
||||||
|
.history/*
|
||||||
|
.site
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# Папки и файлы конфигурации, связанные с медиаконтентом.
|
||||||
|
mediafiles/*
|
||||||
|
migrations/
|
||||||
|
*.migrations/
|
||||||
|
migrations/*
|
||||||
|
*/migrations/
|
||||||
|
*/migrations/*
|
||||||
|
# Исключить все файлы в каталогах migrations, но оставить структуру директорий
|
||||||
|
migrations/**/*
|
||||||
|
*/**/migrations/**/*
|
||||||
|
!migrations/**/.keep
|
||||||
|
0001_*
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
.session
|
||||||
|
/session
|
||||||
|
*.session
|
||||||
|
*.csv
|
||||||
|
|
||||||
446
main.py
Normal file
446
main.py
Normal file
@@ -0,0 +1,446 @@
|
|||||||
|
"""
|
||||||
|
# Конфигурационные данные
|
||||||
|
api_id = '1******1' # Замените на ваш API ID
|
||||||
|
api_hash = 'd******************************a' # Замените на ваш API Hash
|
||||||
|
phone_number = '+7999*******' # Замените на ваш номер телефона
|
||||||
|
channel_username_or_id = '-100**********' # Замените на username или ID группы
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import csv
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import datetime
|
||||||
|
from telethon import TelegramClient, events
|
||||||
|
from telethon.tl.functions.channels import JoinChannelRequest
|
||||||
|
from telethon.tl.types import Channel, ChatForbidden, PeerChannel
|
||||||
|
from telethon.errors import ChannelPrivateError # Добавлен импорт
|
||||||
|
import logging
|
||||||
|
from tqdm import tqdm
|
||||||
|
import getpass # Для безопасного ввода пароля
|
||||||
|
|
||||||
|
# Настройка логирования
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||||
|
handlers=[
|
||||||
|
logging.FileHandler("telegram_parser.log", encoding='utf-8'),
|
||||||
|
logging.StreamHandler()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Конфигурация
|
||||||
|
API_ID = '1******1'
|
||||||
|
API_HASH = 'd******************************a'
|
||||||
|
PHONE_NUMBER = '+7999*******'
|
||||||
|
# CHANNEL_IDENTIFIER теперь используется как значение по умолчанию при выборе диалога
|
||||||
|
CONFIG_CHANNEL_IDENTIFIER = '-**********'
|
||||||
|
CSV_FILENAME = 'telegram_data.csv'
|
||||||
|
# Дополнительные настройки
|
||||||
|
LIMIT_MESSAGES = None # None для всех сообщений, или число для ограничения
|
||||||
|
DAYS_LIMIT = None # None для всех сообщений, или число дней назад для ограничения
|
||||||
|
|
||||||
|
async def parse_message(text: str) -> dict | None:
|
||||||
|
"""
|
||||||
|
Парсит текстовое сообщение с использованием регулярных выражений.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Текст сообщения.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Словарь с извлеченными данными, или None, если не удалось найти необходимые поля.
|
||||||
|
"""
|
||||||
|
|
||||||
|
raw_text_preview = text[:250] + ('...' if len(text) > 250 else '') # Немного увеличил превью
|
||||||
|
result = {'Raw Message': raw_text_preview}
|
||||||
|
|
||||||
|
# Определяем тип сообщения
|
||||||
|
is_order = "Order #" in text and "Purchaser information:" in text
|
||||||
|
is_request_details = "Request details:" in text
|
||||||
|
|
||||||
|
# --- Общая информация (обычно в конце) ---
|
||||||
|
additional_info_text = ""
|
||||||
|
# Ищем блок "Additional information:"
|
||||||
|
additional_info_match = re.search(r"Additional information:\s*\n(.*?)(?:-----|$)", text, re.DOTALL | re.IGNORECASE)
|
||||||
|
if additional_info_match:
|
||||||
|
additional_info_text = additional_info_match.group(1)
|
||||||
|
else:
|
||||||
|
# Если нет явного блока "Additional information:", но есть URL в конце,
|
||||||
|
# попробуем взять текст после последнего "-----\n" если он есть, или весь текст для поиска URL
|
||||||
|
parts = text.split("-----")
|
||||||
|
if len(parts) > 1:
|
||||||
|
potential_additional_info = parts[-1]
|
||||||
|
if "Transaction ID:" in potential_additional_info or "Block ID:" in potential_additional_info or "Form Name:" in potential_additional_info:
|
||||||
|
additional_info_text = potential_additional_info
|
||||||
|
|
||||||
|
if additional_info_text:
|
||||||
|
transaction_id_match = re.search(r"Transaction ID:([^\n]+)", additional_info_text, re.IGNORECASE)
|
||||||
|
if transaction_id_match: result['Transaction ID'] = transaction_id_match.group(1).strip()
|
||||||
|
|
||||||
|
block_id_match = re.search(r"Block ID:([^\n]+)", additional_info_text, re.IGNORECASE)
|
||||||
|
if block_id_match: result['Block ID'] = block_id_match.group(1).strip()
|
||||||
|
|
||||||
|
form_name_match = re.search(r"Form Name:([^\n]+)", additional_info_text, re.IGNORECASE)
|
||||||
|
if form_name_match: result['Form Name'] = form_name_match.group(1).strip()
|
||||||
|
|
||||||
|
# URL часто идет после Form Name или как отдельная строка в этом блоке
|
||||||
|
url_match_after_form_name = re.search(r"Form Name:[^\n]+\n\s*(`?(https?://\S+)`?)", additional_info_text, re.IGNORECASE)
|
||||||
|
if url_match_after_form_name:
|
||||||
|
result['URL'] = url_match_after_form_name.group(1).strip('`')
|
||||||
|
else:
|
||||||
|
# Ищем URL в любой строке блока Additional Information
|
||||||
|
url_fallback_match = re.search(r"^`?(https?://\S+)`?$", additional_info_text, re.MULTILINE | re.IGNORECASE)
|
||||||
|
if url_fallback_match:
|
||||||
|
result['URL'] = url_fallback_match.group(1).strip('`')
|
||||||
|
|
||||||
|
if is_order:
|
||||||
|
result['Type'] = 'Order'
|
||||||
|
|
||||||
|
order_id_match = re.search(r"Order #(\S+)", text, re.IGNORECASE)
|
||||||
|
if order_id_match: result['Order ID'] = order_id_match.group(1).strip()
|
||||||
|
|
||||||
|
payment_amount_match = re.search(r"Payment Amount:([^\n]+)", text, re.IGNORECASE)
|
||||||
|
if payment_amount_match: result['Payment Amount'] = payment_amount_match.group(1).strip()
|
||||||
|
|
||||||
|
# Информация о покупателе
|
||||||
|
purchaser_info_match = re.search(r"Purchaser information:\s*\n(.*?)(?:Additional information:|$)", text, re.DOTALL | re.IGNORECASE)
|
||||||
|
if purchaser_info_match:
|
||||||
|
purchaser_text = purchaser_info_match.group(1)
|
||||||
|
|
||||||
|
name_match = re.search(r"(?:Name|Имя|ФИО|фио)[\s:]*([^\n]+)", purchaser_text, re.IGNORECASE)
|
||||||
|
if name_match: result['Name'] = name_match.group(1).strip()
|
||||||
|
|
||||||
|
phone_match = re.search(r"(?:Phone|телефон|тел|номер)[\s:]*(\+?[0-9\s()\-]{10,})", purchaser_text, re.IGNORECASE)
|
||||||
|
if phone_match: result['Phone'] = phone_match.group(1).strip()
|
||||||
|
|
||||||
|
email_match = re.search(r"(?:Email|почта|e-mail)[\s:]*([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)", purchaser_text, re.IGNORECASE)
|
||||||
|
if email_match: result['Email'] = email_match.group(1).strip()
|
||||||
|
|
||||||
|
# Дата рождения или просто дата
|
||||||
|
date_match = re.search(r"(?:Дата_Рождения|Date)[\s:]*(\d{1,2}[-./]\d{1,2}[-./]\d{2,4})", purchaser_text, re.IGNORECASE)
|
||||||
|
if date_match: result['Date'] = date_match.group(1).strip()
|
||||||
|
|
||||||
|
elif is_request_details: # Явно указано "Request details:"
|
||||||
|
result['Type'] = 'Request'
|
||||||
|
|
||||||
|
# Извлекаем текст из секции "Request details:"
|
||||||
|
request_details_section_text = text
|
||||||
|
match_rd_section = re.search(r"Request details:\s*\n(.*?)(?:Additional information:|$)", text, re.DOTALL | re.IGNORECASE)
|
||||||
|
if match_rd_section:
|
||||||
|
request_details_section_text = match_rd_section.group(1)
|
||||||
|
|
||||||
|
# Паттерны для "Request details"
|
||||||
|
request_patterns = {
|
||||||
|
'Name': r'(?:Name|имя|ФИО|фио|namestep)[\s:]*([^\n]+)',
|
||||||
|
'Phone': r'(?:Phone|телефон|тел|номер|phonenamber|phonestep)[\s:]*(\+?[0-9\s()\-]{10,})',
|
||||||
|
'Email': r'(?:Email|почта|e-mail)[\s:]*([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)',
|
||||||
|
'Date': r'(?:Date|дата)[\s:]*(\d{1,2}[-./]\d{1,2}[-./]\d{2,4})',
|
||||||
|
'Checkbox': r'(?:Checkbox|чекбокс|галочка)[\s:]*([^\n]+)',
|
||||||
|
'Selectbox': r'Selectbox[\s:]*([^\n]+)',
|
||||||
|
'Request Type': r'^type[\s:]*([^\n]+)', # Добавил ^ для точности, если 'type' в начале строки
|
||||||
|
'Persons': r'^Person[\s:]*([^\n]+)',
|
||||||
|
'Helper': r'^helper[\s:]*([^\n]+)',
|
||||||
|
'Connection': r'^connection[\s:]*([^\n]+)',
|
||||||
|
'Comment': r'(?:comment|комментарий)[\s:]*([^\n]+)',
|
||||||
|
'Address': r'(?:address|адрес)[\s:]*([^\n]+)',
|
||||||
|
'Product': r'(?:product|товар|продукт)[\s:]*([^\n]+)',
|
||||||
|
'Price': r'(?:price|цена|стоимость)[\s:]*([^\n]+)'
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, pattern_regex in request_patterns.items():
|
||||||
|
match = re.search(pattern_regex, request_details_section_text, re.IGNORECASE | re.MULTILINE)
|
||||||
|
if match:
|
||||||
|
result[key] = match.group(1).strip()
|
||||||
|
else:
|
||||||
|
# Если нет явных признаков "Order" или "Request details",
|
||||||
|
# но есть поля, похожие на контактные данные в начале.
|
||||||
|
# Это может быть случай, когда "Request details:" отсутствует.
|
||||||
|
# Попробуем общие паттерны для имени и телефона.
|
||||||
|
# logger.debug(f"Сообщение без явных 'Order #' или 'Request details:'. Попытка общего парсинга: {raw_text_preview}")
|
||||||
|
|
||||||
|
# Паттерны для "Request details"
|
||||||
|
fallback_patterns = {
|
||||||
|
'Name': r'(?:Name|имя|ФИО|фио|namestep)[\s:]*([^\n]+)',
|
||||||
|
'Phone': r'(?:Phone|телефон|тел|номер|phonenamber|phonestep)[\s:]*(\+?[0-9\s()\-]{10,})',
|
||||||
|
'Email': r'(?:Email|почта|e-mail)[\s:]*([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)',
|
||||||
|
'Date': r'(?:Date|дата)[\s:]*(\d{1,2}[-./]\d{1,2}[-./]\d{2,4})',
|
||||||
|
}
|
||||||
|
found_in_fallback = False
|
||||||
|
for key, pattern_regex in fallback_patterns.items():
|
||||||
|
match = re.search(pattern_regex, text, re.IGNORECASE | re.MULTILINE)
|
||||||
|
if match:
|
||||||
|
# Проверяем, чтобы значение не было частью "Additional Information" если оно уже разобрано
|
||||||
|
if key not in result: # Чтобы не перезаписывать уже найденное из Additional Info
|
||||||
|
result[key] = match.group(1).strip()
|
||||||
|
found_in_fallback = True
|
||||||
|
if found_in_fallback:
|
||||||
|
result['Type'] = 'Request (Fallback)'
|
||||||
|
|
||||||
|
|
||||||
|
# Если URL не был найден в "Additional Information", попробуем найти его в основном тексте
|
||||||
|
if 'URL' not in result:
|
||||||
|
url_global_fallback_match = re.search(r"`?(https?://\S+)`?", text)
|
||||||
|
if url_global_fallback_match:
|
||||||
|
result['URL'] = url_global_fallback_match.group(1).strip('`')
|
||||||
|
|
||||||
|
# Проверяем, есть ли хотя бы имя или телефон, или это заказ с ID
|
||||||
|
if result.get('Name') or result.get('Phone') or (is_order and result.get('Order ID')):
|
||||||
|
if 'Checkbox' in result and isinstance(result['Checkbox'], str):
|
||||||
|
result['Checkbox'] = result['Checkbox'].lower()
|
||||||
|
return result
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def choose_dialog(client, configured_identifier):
|
||||||
|
"""
|
||||||
|
Позволяет пользователю выбрать целевой чат/канал.
|
||||||
|
"""
|
||||||
|
logger.info("Выберите целевой чат/канал:")
|
||||||
|
print("\nКак выбрать целевой чат/канал?")
|
||||||
|
print(f"1. Использовать ID из конфигурации: {configured_identifier}")
|
||||||
|
print("2. Ввести ID, username (@имя) или полную ссылку на чат/канал вручную.")
|
||||||
|
print("3. Выбрать из списка недавних диалогов (до 20).")
|
||||||
|
|
||||||
|
choice = input("Ваш выбор (1-3): ")
|
||||||
|
identifier_to_use = None
|
||||||
|
|
||||||
|
if choice == '1':
|
||||||
|
identifier_to_use = configured_identifier
|
||||||
|
if not identifier_to_use:
|
||||||
|
logger.warning("ID в конфигурации не указан.")
|
||||||
|
# Можно предложить ввести вручную или выбрать из списка
|
||||||
|
choice = '2' # Переходим к ручному вводу
|
||||||
|
|
||||||
|
if choice == '2': # Также используется, если выбор '1' не удался
|
||||||
|
identifier_to_use = input("Введите ID, username (@имя) или ссылку: ").strip()
|
||||||
|
elif choice == '3':
|
||||||
|
try:
|
||||||
|
dialogs = await client.get_dialogs(limit=20)
|
||||||
|
if not dialogs:
|
||||||
|
logger.warning("Не найдено недавних диалогов.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
print("\nПоследние 20 диалогов:")
|
||||||
|
print("-" * 70)
|
||||||
|
print(f"{'№':<3} | {'Тип':<15} | {'Название':<30} | ID")
|
||||||
|
print("-" * 70)
|
||||||
|
for i, dialog in enumerate(dialogs):
|
||||||
|
entity_type = "Канал" if dialog.is_channel else "Группа" if dialog.is_group else "Пользователь"
|
||||||
|
name = dialog.name if hasattr(dialog, 'name') and dialog.name else "N/A"
|
||||||
|
name_short = name[:27] + "..." if len(name) > 30 else name
|
||||||
|
print(f"{i+1:<3} | {entity_type:<15} | {name_short:<30} | {dialog.id}")
|
||||||
|
print("-" * 70)
|
||||||
|
|
||||||
|
dialog_choice_str = input(f"Выберите номер диалога (1-{len(dialogs)}) или 0 для отмены: ")
|
||||||
|
dialog_choice_num = int(dialog_choice_str)
|
||||||
|
|
||||||
|
if 1 <= dialog_choice_num <= len(dialogs):
|
||||||
|
identifier_to_use = dialogs[dialog_choice_num-1].id # Используем ID напрямую
|
||||||
|
elif dialog_choice_num == 0:
|
||||||
|
logger.info("Выбор отменен.")
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
logger.warning("Неверный номер диалога.")
|
||||||
|
return None
|
||||||
|
except ValueError:
|
||||||
|
logger.warning("Неверный ввод для номера диалога.")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при получении списка диалогов: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not identifier_to_use: # Если ни один из вариантов не дал идентификатор
|
||||||
|
logger.error("Идентификатор чата/канала не был указан или выбран.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info(f"Попытка получить информацию о сущности: {identifier_to_use}")
|
||||||
|
# Telethon get_entity очень гибкий: принимает int (ID), str (username, link, phone)
|
||||||
|
# Если это строка, которая выглядит как число (особенно отрицательное для channel_id), преобразуем в int
|
||||||
|
if isinstance(identifier_to_use, str):
|
||||||
|
if identifier_to_use.startswith('-') and identifier_to_use[1:].isdigit():
|
||||||
|
entity = await client.get_entity(int(identifier_to_use))
|
||||||
|
elif identifier_to_use.isdigit(): # Для обычных user ID или chat ID
|
||||||
|
entity = await client.get_entity(int(identifier_to_use))
|
||||||
|
else: # Для username или https://t.me/joinchat/... или https://t.me/channel_name
|
||||||
|
entity = await client.get_entity(identifier_to_use)
|
||||||
|
else: # Если это уже int (например, из dialog.id)
|
||||||
|
entity = await client.get_entity(identifier_to_use)
|
||||||
|
return entity
|
||||||
|
except ValueError as e:
|
||||||
|
logger.error(f"Не удалось найти сущность по '{identifier_to_use}'. Убедитесь, что ID/имя пользователя/ссылка верны и у вас есть доступ. Ошибка: {e}")
|
||||||
|
return None
|
||||||
|
except TypeError as e: # Например, если передан None в get_entity
|
||||||
|
logger.error(f"Некорректный тип идентификатора для get_entity: {identifier_to_use}. Ошибка: {e}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Непредвиденная ошибка при получении сущности '{identifier_to_use}': {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""
|
||||||
|
Основная функция для работы с Telegram и парсинга данных.
|
||||||
|
"""
|
||||||
|
logger.info("Запуск парсера Telegram")
|
||||||
|
|
||||||
|
os.makedirs('sessions', exist_ok=True)
|
||||||
|
client = TelegramClient('sessions/telegram_session', API_ID, API_HASH)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await client.connect()
|
||||||
|
logger.info("Подключение к Telegram API установлено")
|
||||||
|
|
||||||
|
if not await client.is_user_authorized():
|
||||||
|
logger.info("Требуется авторизация")
|
||||||
|
await client.send_code_request(PHONE_NUMBER)
|
||||||
|
code = input('Введите код из Telegram: ')
|
||||||
|
try:
|
||||||
|
await client.sign_in(PHONE_NUMBER, code)
|
||||||
|
logger.info("Авторизация с кодом успешна")
|
||||||
|
except Exception as e: # Изначально ловим общее исключение
|
||||||
|
if "SESSION_PASSWORD_NEEDED" in str(e) or "Two-steps verification is enabled" in str(e): # Более точная проверка
|
||||||
|
logger.info("Требуется двухфакторная аутентификация (пароль)")
|
||||||
|
password = getpass.getpass('Введите пароль двухфакторной аутентификации: ')
|
||||||
|
try:
|
||||||
|
await client.sign_in(password=password)
|
||||||
|
logger.info("Авторизация с паролем успешна")
|
||||||
|
except Exception as e_pw:
|
||||||
|
logger.error(f"Ошибка авторизации с паролем: {e_pw}")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
logger.error(f"Ошибка авторизации с кодом: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
entity = await choose_dialog(client, CONFIG_CHANNEL_IDENTIFIER)
|
||||||
|
|
||||||
|
if not entity:
|
||||||
|
logger.error("Не удалось определить целевой чат/канал. Завершение работы.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Проверка типа сущности и членства
|
||||||
|
if isinstance(entity, Channel):
|
||||||
|
logger.info(f"Тип: Канал/Супергруппа")
|
||||||
|
logger.info(f"Название: {entity.title}")
|
||||||
|
logger.info(f"ID: {entity.id}")
|
||||||
|
# logger.info(f"Доступ: {'публичный' if entity.megagroup else 'приватный'}") # megagroup не всегда показатель публичности
|
||||||
|
|
||||||
|
# Проверка членства в группе/канале
|
||||||
|
# Для каналов/супергрупп, `entity.left` может быть True, если вы не участник.
|
||||||
|
# Для приватных каналов, к которым вы не присоединились, get_entity может не вернуть полную информацию или выдать ошибку ранее.
|
||||||
|
# Попытка присоединиться, если необходимо и возможно
|
||||||
|
try:
|
||||||
|
# Попытка получить полную информацию, чтобы проверить 'left' атрибут
|
||||||
|
full_entity = await client.get_input_entity(entity) # или get_dialogs с entity
|
||||||
|
# Проверка на участие может быть сложной, так как 'left' не всегда есть или актуален
|
||||||
|
# Простой способ - попытаться прочитать сообщения. Если не получится - значит нет доступа.
|
||||||
|
logger.info("Проверка доступа к сообщениям...")
|
||||||
|
async for _ in client.iter_messages(entity, limit=1):
|
||||||
|
break
|
||||||
|
logger.info("Доступ к сообщениям есть.")
|
||||||
|
|
||||||
|
except (ChatForbidden, ChannelPrivateError) as e: # Telethon specific errors
|
||||||
|
logger.warning(f"Нет доступа к {entity.title} или он приватный и вы не участник: {e}")
|
||||||
|
logger.info(f"Попытка присоединиться к {entity.title}...")
|
||||||
|
try:
|
||||||
|
await client(JoinChannelRequest(entity))
|
||||||
|
logger.info(f"Успешно присоединились к {entity.title}.")
|
||||||
|
except Exception as e_join:
|
||||||
|
logger.error(f"Ошибка при попытке присоединиться: {e_join}")
|
||||||
|
return
|
||||||
|
except Exception as e_check:
|
||||||
|
logger.warning(f"Не удалось точно определить статус участия, но сущность получена. Ошибка: {e_check}")
|
||||||
|
|
||||||
|
|
||||||
|
elif isinstance(entity, ChatForbidden): # Это обычно означает, что get_entity не смогла получить доступ
|
||||||
|
logger.error(f"ОШИБКА: Нет доступа к сущности '{entity.title if hasattr(entity, 'title') else CONFIG_CHANNEL_IDENTIFIER}' (запрещено или не существует)")
|
||||||
|
return
|
||||||
|
else: # User, Chat (обычная группа)
|
||||||
|
logger.info(f"Тип: {type(entity).__name__}, Название: {getattr(entity, 'title', getattr(entity, 'username', entity.id))}, ID: {entity.id}")
|
||||||
|
|
||||||
|
|
||||||
|
date_filter = None
|
||||||
|
if DAYS_LIMIT:
|
||||||
|
date_filter = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=DAYS_LIMIT)
|
||||||
|
logger.info(f"Будут обработаны сообщения не старше {DAYS_LIMIT} дней (с {date_filter.strftime('%d.%m.%Y %H:%M:%S %Z')})")
|
||||||
|
|
||||||
|
# Настройка для прогресс-бара
|
||||||
|
messages_iterator = client.iter_messages(entity, limit=LIMIT_MESSAGES, reverse=True)
|
||||||
|
# Если LIMIT_MESSAGES is None, tqdm total будет None, что нормально.
|
||||||
|
# Если LIMIT_MESSAGES задан, используем его.
|
||||||
|
progress_bar_total = LIMIT_MESSAGES
|
||||||
|
|
||||||
|
if LIMIT_MESSAGES is None:
|
||||||
|
logger.info("Обработка всех сообщений. Общее количество для прогресс-бара будет неизвестно.")
|
||||||
|
else:
|
||||||
|
logger.info(f"Будет обработано до {LIMIT_MESSAGES} сообщений.")
|
||||||
|
|
||||||
|
parsed_data = []
|
||||||
|
total_messages_processed = 0
|
||||||
|
skipped_messages_by_date = 0
|
||||||
|
|
||||||
|
with tqdm(total=progress_bar_total, desc="Обработка сообщений", unit="сообщ.") as progress_bar:
|
||||||
|
async for message in messages_iterator:
|
||||||
|
progress_bar.update(1)
|
||||||
|
|
||||||
|
if date_filter and message.date < date_filter:
|
||||||
|
skipped_messages_by_date += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if message.text:
|
||||||
|
parsed = await parse_message(message.text)
|
||||||
|
if parsed:
|
||||||
|
parsed['Message Date'] = message.date.strftime('%d.%m.%Y %H:%M:%S')
|
||||||
|
parsed_data.append(parsed)
|
||||||
|
total_messages_processed += 1
|
||||||
|
|
||||||
|
logger.info(f"\nСтатистика:")
|
||||||
|
logger.info(f"Всего обработано сообщений в итераторе: {total_messages_processed}")
|
||||||
|
if DAYS_LIMIT:
|
||||||
|
logger.info(f"Пропущено сообщений (старше {DAYS_LIMIT} дней): {skipped_messages_by_date}")
|
||||||
|
logger.info(f"Найдено подходящих записей для CSV: {len(parsed_data)}")
|
||||||
|
|
||||||
|
if parsed_data:
|
||||||
|
all_fields = set()
|
||||||
|
for entry in parsed_data:
|
||||||
|
all_fields.update(entry.keys())
|
||||||
|
|
||||||
|
fieldnames = sorted(list(all_fields))
|
||||||
|
# Перемещаем основные поля в начало
|
||||||
|
preferred_order = [
|
||||||
|
'Message Date', 'Type', 'Name', 'Phone', 'Email', 'Date',
|
||||||
|
'Form Name', 'Order ID', 'Payment Amount', 'Product', 'Price',
|
||||||
|
'Selectbox', 'Checkbox', 'Request Type', 'Persons', 'Helper', 'Connection',
|
||||||
|
'Transaction ID', 'Block ID', 'Address', 'Comment', 'URL', 'Raw Message'
|
||||||
|
]
|
||||||
|
final_fieldnames = [f for f in preferred_order if f in fieldnames] + \
|
||||||
|
[f for f in fieldnames if f not in preferred_order]
|
||||||
|
|
||||||
|
with open(CSV_FILENAME, 'w', newline='', encoding='utf-8-sig') as f:
|
||||||
|
writer = csv.DictWriter(f, fieldnames=final_fieldnames)
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(parsed_data)
|
||||||
|
logger.info(f"Файл сохранен: {os.path.abspath(CSV_FILENAME)}")
|
||||||
|
else:
|
||||||
|
logger.warning("Нет данных для сохранения. Проверьте сообщения в чате или настройки парсера.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Произошла ошибка при обработке сообщений: {e}", exc_info=True)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Произошла ошибка при подключении или начальной настройке: {e}", exc_info=True)
|
||||||
|
finally:
|
||||||
|
if client.is_connected():
|
||||||
|
await client.disconnect()
|
||||||
|
logger.info("Работа парсера завершена")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Для Windows может потребоваться это для asyncio с tqdm в некоторых случаях
|
||||||
|
if os.name == 'nt':
|
||||||
|
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||||
|
try:
|
||||||
|
asyncio.run(main())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.info("Работа программы прервана пользователем")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Критическая ошибка в __main__: {e}", exc_info=True)
|
||||||
Reference in New Issue
Block a user