210 lines
7.4 KiB
Python
Executable File
210 lines
7.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
Скрипт для получения статистики по главам EPUB книги.
|
||
|
||
Использование:
|
||
python statistics.py <путь_к_epub_файлу>
|
||
python statistics.py book.epub
|
||
"""
|
||
import sys
|
||
import json
|
||
import argparse
|
||
import re
|
||
from pathlib import Path
|
||
from typing import Dict, Any, List
|
||
from ebooklib import epub
|
||
|
||
# Импортируем функции из app.py
|
||
from app import parse_epub_content, calculate_chapter_tokens
|
||
|
||
|
||
def sanitize_filename(title: str, max_length: int = 100) -> str:
|
||
"""Очищает название книги для использования в имени файла.
|
||
|
||
Удаляет недопустимые символы и ограничивает длину имени файла.
|
||
|
||
Args:
|
||
title: Название книги для очистки.
|
||
max_length: Максимальная длина имени файла.
|
||
|
||
Returns:
|
||
Безопасное имя файла.
|
||
"""
|
||
if not title:
|
||
return 'book_statistics'
|
||
|
||
# Удаляем недопустимые символы для имени файла
|
||
# Оставляем буквы, цифры, пробелы, дефисы, подчеркивания
|
||
sanitized = re.sub(r'[<>:"/\\|?*]', '', title)
|
||
|
||
# Заменяем множественные пробелы на один
|
||
sanitized = re.sub(r'\s+', ' ', sanitized)
|
||
|
||
# Убираем пробелы в начале и конце
|
||
sanitized = sanitized.strip()
|
||
|
||
# Ограничиваем длину
|
||
if len(sanitized) > max_length:
|
||
sanitized = sanitized[:max_length].rstrip()
|
||
|
||
# Если после очистки ничего не осталось, используем дефолтное имя
|
||
if not sanitized:
|
||
sanitized = 'book_statistics'
|
||
|
||
return sanitized
|
||
|
||
|
||
def format_statistics(title: str, author: str, chapters: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||
"""Форматирует статистику по книге и главам.
|
||
|
||
Args:
|
||
title: Название книги.
|
||
author: Автор книги.
|
||
chapters: Список глав с данными.
|
||
|
||
Returns:
|
||
Словарь со статистикой в формате JSON.
|
||
"""
|
||
# Статистика по главам
|
||
chapters_stats: List[Dict[str, Any]] = []
|
||
total_chars = 0
|
||
total_tokens = 0
|
||
|
||
for chapter in chapters:
|
||
chapter_text = chapter.get('text', '')
|
||
chapter_title = chapter.get('chapterTitle', 'Глава без названия')
|
||
chapter_number = chapter.get('chapterNumber', 0)
|
||
footnotes_count = chapter.get('footnotesCount', 0)
|
||
token_count = chapter.get('tokenCount', 0)
|
||
|
||
# Подсчитываем символы (включая пробелы)
|
||
char_count = len(chapter_text)
|
||
|
||
# Если tokenCount не был подсчитан, подсчитываем сейчас
|
||
if token_count == 0 and chapter_text:
|
||
chapter_data = {
|
||
'text': chapter_text,
|
||
'chapterTitle': chapter_title,
|
||
'chapterId': chapter.get('chapterId', ''),
|
||
'chapterNumber': chapter_number,
|
||
'filePath': chapter.get('filePath', ''),
|
||
'footnotes': chapter.get('footnotes', [])
|
||
}
|
||
token_count = calculate_chapter_tokens(chapter_data)
|
||
|
||
total_chars += char_count
|
||
total_tokens += token_count
|
||
|
||
chapters_stats.append({
|
||
'chapterNumber': chapter_number,
|
||
'chapterTitle': chapter_title,
|
||
'characters': char_count,
|
||
'tokens': token_count,
|
||
'footnotesCount': footnotes_count,
|
||
'filePath': chapter.get('filePath', '')
|
||
})
|
||
|
||
# Общая статистика
|
||
result: Dict[str, Any] = {
|
||
'book': {
|
||
'title': title,
|
||
'author': author,
|
||
'totalChapters': len(chapters),
|
||
'totalCharacters': total_chars,
|
||
'totalTokens': total_tokens
|
||
},
|
||
'chapters': chapters_stats
|
||
}
|
||
|
||
return result
|
||
|
||
|
||
def main():
|
||
"""Основная функция скрипта."""
|
||
parser = argparse.ArgumentParser(
|
||
description='Получение статистики по главам EPUB книги',
|
||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||
epilog="""
|
||
Примеры использования:
|
||
python statistics.py book.epub
|
||
python statistics.py /path/to/book.epub --output stats.json
|
||
python statistics.py book.epub --stdout
|
||
"""
|
||
)
|
||
parser.add_argument(
|
||
'epub_file',
|
||
type=str,
|
||
help='Путь к EPUB файлу'
|
||
)
|
||
parser.add_argument(
|
||
'-o', '--output',
|
||
type=str,
|
||
default=None,
|
||
help='Путь к выходному JSON файлу (если не указан, создается файл с названием книги)'
|
||
)
|
||
parser.add_argument(
|
||
'--stdout',
|
||
action='store_true',
|
||
help='Вывести результат в stdout вместо сохранения в файл'
|
||
)
|
||
parser.add_argument(
|
||
'--compact',
|
||
action='store_true',
|
||
help='Сохранить JSON в компактном формате (без отступов)'
|
||
)
|
||
|
||
args = parser.parse_args()
|
||
|
||
# Проверяем существование файла
|
||
epub_path = Path(args.epub_file)
|
||
if not epub_path.exists():
|
||
print(f"Ошибка: Файл '{epub_path}' не найден.", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
if not epub_path.is_file():
|
||
print(f"Ошибка: '{epub_path}' не является файлом.", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
try:
|
||
# Читаем и парсим EPUB
|
||
book = epub.read_epub(str(epub_path))
|
||
title, author, metadata, chapters = parse_epub_content(book)
|
||
|
||
# Формируем статистику
|
||
statistics = format_statistics(title, author, chapters)
|
||
|
||
# Выводим результат
|
||
if args.compact:
|
||
json_output = json.dumps(statistics, ensure_ascii=False)
|
||
else:
|
||
json_output = json.dumps(statistics, ensure_ascii=False, indent=2)
|
||
|
||
# Определяем путь к выходному файлу
|
||
if args.stdout:
|
||
# Выводим в stdout
|
||
print(json_output)
|
||
else:
|
||
if args.output:
|
||
# Используем указанный путь
|
||
output_path = Path(args.output)
|
||
else:
|
||
# Создаем имя файла на основе названия книги
|
||
safe_title = sanitize_filename(title)
|
||
output_path = Path(f"{safe_title}_statistics.json")
|
||
|
||
# Сохраняем в файл
|
||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||
with open(output_path, 'w', encoding='utf-8') as f:
|
||
f.write(json_output)
|
||
print(f"Статистика сохранена в файл: {output_path}")
|
||
|
||
except Exception as e:
|
||
print(f"Ошибка при обработке EPUB файла: {e}", file=sys.stderr)
|
||
import traceback
|
||
traceback.print_exc()
|
||
sys.exit(1)
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|