Import Vector lab project
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
# Скрипт парсинга спецдонесений МЧС
|
||||
|
||||
## Описание
|
||||
|
||||
`parse_reports.py` - скрипт для автоматического извлечения структурированных данных из спецдонесений МЧС Беларуси в формате .docx с использованием Claude API.
|
||||
|
||||
## Возможности
|
||||
|
||||
- ✅ Чтение .docx файлов через python-docx
|
||||
- ✅ Извлечение 18 полей данных через Claude API
|
||||
- ✅ Оценка уверенности (confidence) для каждого поля
|
||||
- ✅ Автоматическая фильтрация по качеству данных
|
||||
- ✅ Сохранение в PostgreSQL (таблицы cases + raw_documents)
|
||||
- ✅ Режим dry-run для тестирования
|
||||
- ✅ Детальное логирование и отчеты
|
||||
|
||||
## Извлекаемые поля
|
||||
|
||||
1. `age_years` - возраст в годах
|
||||
2. `gender` - пол (мужской/женский)
|
||||
3. `has_diagnosis` - наличие диагноза/заболевания
|
||||
4. `diagnosis_type` - тип диагноза
|
||||
5. `has_transport` - наличие транспорта
|
||||
6. `season` - сезон (winter/spring/summer/autumn)
|
||||
7. `time_of_day` - время суток (morning/day/evening/night)
|
||||
8. `elapsed_before_report_h` - часов до сообщения
|
||||
9. `last_seen_direction` - направление последнего наблюдения
|
||||
10. `terrain_primary` - основной тип местности
|
||||
11. `water_nearby` - водоем рядом
|
||||
12. `road_nearby` - дорога рядом
|
||||
13. `found_alive` - найден живым
|
||||
14. `found_distance_km` - расстояние обнаружения (км)
|
||||
15. `found_direction` - направление обнаружения
|
||||
16. `found_location_type` - тип места обнаружения
|
||||
17. `search_duration_hours` - длительность поиска (часы)
|
||||
18. `who_found` - кто нашел
|
||||
|
||||
Каждое поле имеет:
|
||||
- `value` - значение или null
|
||||
- `confidence` - уровень уверенности (high/medium/low)
|
||||
|
||||
## Установка
|
||||
|
||||
### 1. Создание таблиц в БД
|
||||
|
||||
```bash
|
||||
cd /root/sar-mchs/scripts
|
||||
|
||||
# Подключитесь к PostgreSQL и выполните:
|
||||
psql -U postgres -d sar_mchs -f create_tables.sql
|
||||
```
|
||||
|
||||
Или через Docker:
|
||||
```bash
|
||||
docker exec -i sar-mchs-postgres psql -U postgres -d sar_mchs < create_tables.sql
|
||||
```
|
||||
|
||||
### 2. Установка зависимостей
|
||||
|
||||
Зависимости уже установлены в backend/requirements.txt:
|
||||
- python-docx
|
||||
- httpx
|
||||
- sqlalchemy[asyncio]
|
||||
- asyncpg
|
||||
|
||||
### 3. Настройка переменных окружения
|
||||
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY="your_api_key_here"
|
||||
export DATABASE_URL="postgresql+asyncpg://postgres:postgres@localhost:5432/sar_mchs"
|
||||
```
|
||||
|
||||
## Использование
|
||||
|
||||
### Базовое использование
|
||||
|
||||
```bash
|
||||
cd /root/sar-mchs/scripts
|
||||
|
||||
# Dry-run (без записи в БД)
|
||||
python3 parse_reports.py --dir /path/to/docx/files --dry-run
|
||||
|
||||
# Запись в БД
|
||||
python3 parse_reports.py \
|
||||
--dir /path/to/docx/files \
|
||||
--db "postgresql+asyncpg://postgres:postgres@localhost:5432/sar_mchs"
|
||||
```
|
||||
|
||||
### Параметры
|
||||
|
||||
- `--dir` (обязательный) - путь к папке с .docx файлами
|
||||
- `--db` (опциональный) - DATABASE_URL для PostgreSQL
|
||||
- `--dry-run` (флаг) - режим тестирования без записи в БД
|
||||
|
||||
### Примеры
|
||||
|
||||
**1. Тестовый запуск:**
|
||||
```bash
|
||||
python3 parse_reports.py --dir ./test_reports --dry-run
|
||||
```
|
||||
|
||||
**2. Обработка с записью в БД:**
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY="sk-ant-..."
|
||||
python3 parse_reports.py \
|
||||
--dir /data/mchs_reports \
|
||||
--db "postgresql+asyncpg://postgres:postgres@postgres:5432/sar_mchs"
|
||||
```
|
||||
|
||||
**3. Обработка из Docker контейнера:**
|
||||
```bash
|
||||
docker exec -it sar-mchs-backend bash
|
||||
cd /app/scripts
|
||||
python3 parse_reports.py --dir /data/reports --db $DATABASE_URL
|
||||
```
|
||||
|
||||
## Логика работы
|
||||
|
||||
1. **Чтение файлов**: Сканирует директорию на наличие .docx файлов
|
||||
2. **Извлечение текста**: Использует python-docx для чтения содержимого
|
||||
3. **Анализ через Claude**: Отправляет текст в Claude API с промптом извлечения
|
||||
4. **Валидация**: Подсчитывает поля с high confidence
|
||||
5. **Фильтрация**: Пропускает файлы с < 6 полей high confidence
|
||||
6. **Сохранение**: Записывает в таблицы raw_documents и cases
|
||||
7. **Отчет**: Генерирует parse_report.json с детальной статистикой
|
||||
|
||||
## Выходные данные
|
||||
|
||||
### Консольный вывод
|
||||
|
||||
```
|
||||
2026-05-01 10:15:23 - INFO - Найдено файлов: 15
|
||||
2026-05-01 10:15:23 - INFO - Режим: ЗАПИСЬ В БД
|
||||
2026-05-01 10:15:24 - INFO - Обработка: report_001.docx
|
||||
2026-05-01 10:15:28 - INFO - Полей с high confidence: 12/18
|
||||
2026-05-01 10:15:29 - INFO - ✓ Сохранено в БД: report_001.docx (doc_id=1)
|
||||
...
|
||||
2026-05-01 10:20:15 - INFO - ============================================================
|
||||
2026-05-01 10:20:15 - INFO - ИТОГОВАЯ СТАТИСТИКА
|
||||
2026-05-01 10:20:15 - INFO - ============================================================
|
||||
2026-05-01 10:20:15 - INFO - Всего файлов: 15
|
||||
2026-05-01 10:20:15 - INFO - Успешно обработано: 12
|
||||
2026-05-01 10:20:15 - INFO - Пропущено (< 6 high conf): 2
|
||||
2026-05-01 10:20:15 - INFO - Ошибки: 1
|
||||
```
|
||||
|
||||
### Файл отчета (parse_report.json)
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"file": "report_001.docx",
|
||||
"status": "success",
|
||||
"high_confidence_count": 12
|
||||
},
|
||||
{
|
||||
"file": "report_002.docx",
|
||||
"status": "skipped",
|
||||
"reason": "Недостаточно полей с high confidence (4 < 6)",
|
||||
"high_confidence_count": 4
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Структура БД
|
||||
|
||||
### Таблица raw_documents
|
||||
- `id` - первичный ключ
|
||||
- `filename` - имя файла
|
||||
- `raw_text` - исходный текст
|
||||
- `extracted_json` - JSON с извлеченными данными
|
||||
- `created_at`, `updated_at` - временные метки
|
||||
|
||||
### Таблица cases
|
||||
- `id` - первичный ключ
|
||||
- `raw_document_id` - ссылка на raw_documents
|
||||
- 18 полей данных (см. выше)
|
||||
- `created_at`, `updated_at` - временные метки
|
||||
|
||||
## Ограничения и рекомендации
|
||||
|
||||
- **Rate limiting**: Скрипт делает паузу 1 секунду между запросами к Claude API
|
||||
- **Длина текста**: Ограничена 8000 символами для промпта
|
||||
- **Минимальный порог**: Файлы с < 6 полей high confidence пропускаются
|
||||
- **Стоимость**: Каждый файл = 1 запрос к Claude API (~$0.003-0.015 за файл)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Ошибка: "ANTHROPIC_API_KEY не установлен"**
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY="your_key_here"
|
||||
```
|
||||
|
||||
**Ошибка подключения к БД**
|
||||
```bash
|
||||
# Проверьте DATABASE_URL
|
||||
echo $DATABASE_URL
|
||||
|
||||
# Проверьте доступность PostgreSQL
|
||||
docker exec sar-mchs-postgres pg_isready
|
||||
```
|
||||
|
||||
**Файлы не найдены**
|
||||
```bash
|
||||
# Проверьте путь и наличие .docx файлов
|
||||
ls -la /path/to/docx/files/*.docx
|
||||
```
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] Добавить поддержку .pdf файлов
|
||||
- [ ] Batch обработка для снижения стоимости API
|
||||
- [ ] Retry логика при ошибках API
|
||||
- [ ] Прогресс-бар для больших объемов
|
||||
- [ ] Экспорт в CSV/Excel
|
||||
- [ ] Web-интерфейс для мониторинга
|
||||
@@ -0,0 +1,58 @@
|
||||
-- Таблица для хранения сырых документов
|
||||
CREATE TABLE IF NOT EXISTS raw_documents (
|
||||
id SERIAL PRIMARY KEY,
|
||||
filename VARCHAR(255) NOT NULL,
|
||||
raw_text TEXT NOT NULL,
|
||||
extracted_json JSONB,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Таблица для хранения структурированных данных о случаях
|
||||
CREATE TABLE IF NOT EXISTS cases (
|
||||
id SERIAL PRIMARY KEY,
|
||||
raw_document_id INTEGER REFERENCES raw_documents(id),
|
||||
|
||||
-- Данные о пропавшем
|
||||
age INTEGER,
|
||||
gender VARCHAR(50),
|
||||
has_diagnosis BOOLEAN,
|
||||
diagnosis_type VARCHAR(255),
|
||||
has_transport BOOLEAN,
|
||||
|
||||
-- Условия
|
||||
season VARCHAR(50),
|
||||
time_of_day VARCHAR(50),
|
||||
elapsed_before_report_h FLOAT,
|
||||
|
||||
-- Местоположение
|
||||
last_seen_direction VARCHAR(100),
|
||||
terrain_primary VARCHAR(100),
|
||||
water_nearby BOOLEAN,
|
||||
road_nearby BOOLEAN,
|
||||
|
||||
-- Результаты поиска
|
||||
found_alive BOOLEAN,
|
||||
found_distance_km FLOAT,
|
||||
found_direction VARCHAR(100),
|
||||
found_location_type VARCHAR(255),
|
||||
search_duration_hours FLOAT,
|
||||
who_found VARCHAR(255),
|
||||
|
||||
-- Метаданные
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Индексы для ускорения запросов
|
||||
CREATE INDEX IF NOT EXISTS idx_cases_age ON cases(age);
|
||||
CREATE INDEX IF NOT EXISTS idx_cases_season ON cases(season);
|
||||
CREATE INDEX IF NOT EXISTS idx_cases_terrain ON cases(terrain_primary);
|
||||
CREATE INDEX IF NOT EXISTS idx_cases_found_alive ON cases(found_alive);
|
||||
CREATE INDEX IF NOT EXISTS idx_cases_found_distance ON cases(found_distance_km);
|
||||
|
||||
-- Индекс для JSONB поиска
|
||||
CREATE INDEX IF NOT EXISTS idx_raw_documents_json ON raw_documents USING gin(extracted_json);
|
||||
|
||||
COMMENT ON TABLE raw_documents IS 'Сырые документы спецдонесений МЧС';
|
||||
COMMENT ON TABLE cases IS 'Структурированные данные о случаях пропажи людей';
|
||||
Executable
+381
@@ -0,0 +1,381 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Скрипт для парсинга спецдонесений МЧС Беларуси из .docx файлов.
|
||||
Использует Claude API для извлечения структурированных данных.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import httpx
|
||||
from docx import Document
|
||||
from sqlalchemy import create_engine, text
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
# Настройка логирования
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Список полей для извлечения
|
||||
FIELDS = [
|
||||
"age_years",
|
||||
"gender",
|
||||
"has_diagnosis",
|
||||
"diagnosis_type",
|
||||
"has_transport",
|
||||
"season",
|
||||
"time_of_day",
|
||||
"elapsed_before_report_h",
|
||||
"last_seen_direction",
|
||||
"terrain_primary",
|
||||
"water_nearby",
|
||||
"road_nearby",
|
||||
"found_alive",
|
||||
"found_distance_km",
|
||||
"found_direction",
|
||||
"found_location_type",
|
||||
"search_duration_hours",
|
||||
"who_found"
|
||||
]
|
||||
|
||||
|
||||
EXTRACTION_PROMPT = """Ты — система извлечения данных из спецдонесений МЧС Беларуси. Извлеки поля и верни ТОЛЬКО JSON.
|
||||
|
||||
Поля для извлечения:
|
||||
- age_years: возраст в годах (число)
|
||||
- gender: пол (мужской/женский)
|
||||
- has_diagnosis: есть ли диагноз/заболевание (true/false)
|
||||
- diagnosis_type: тип диагноза если есть (строка)
|
||||
- has_transport: был ли транспорт (true/false)
|
||||
- season: сезон (winter/spring/summer/autumn)
|
||||
- time_of_day: время суток (morning/day/evening/night)
|
||||
- elapsed_before_report_h: часов прошло до сообщения (число)
|
||||
- last_seen_direction: направление последнего наблюдения (строка)
|
||||
- terrain_primary: основной тип местности (forest/field/urban/mountain/water)
|
||||
- water_nearby: водоем рядом (true/false)
|
||||
- road_nearby: дорога рядом (true/false)
|
||||
- found_alive: найден живым (true/false)
|
||||
- found_distance_km: расстояние обнаружения в км (число)
|
||||
- found_direction: направление обнаружения (север/юг/восток/запад и т.д.)
|
||||
- found_location_type: тип места обнаружения (строка)
|
||||
- search_duration_hours: длительность поиска в часах (число)
|
||||
- who_found: кто нашел (спасатели/волонтеры/родственники/сам вышел и т.д.)
|
||||
|
||||
Для каждого поля верни объект:
|
||||
{
|
||||
"value": <значение или null>,
|
||||
"confidence": "high" | "medium" | "low"
|
||||
}
|
||||
|
||||
Если поле не упомянуто в тексте — value: null, confidence: "low".
|
||||
|
||||
Верни ТОЛЬКО валидный JSON в формате:
|
||||
{
|
||||
"age_years": {"value": 65, "confidence": "high"},
|
||||
"gender": {"value": "мужской", "confidence": "high"},
|
||||
...
|
||||
}
|
||||
|
||||
ТЕКСТ ДОНЕСЕНИЯ:
|
||||
{text}
|
||||
|
||||
Ответь ТОЛЬКО JSON без дополнительного текста."""
|
||||
|
||||
|
||||
async def extract_text_from_docx(file_path: Path) -> str:
|
||||
"""Извлекает текст из .docx файла."""
|
||||
try:
|
||||
doc = Document(file_path)
|
||||
text = "\n".join([paragraph.text for paragraph in doc.paragraphs])
|
||||
return text.strip()
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка чтения {file_path}: {e}")
|
||||
return ""
|
||||
|
||||
|
||||
async def extract_data_with_claude(text: str, api_key: str) -> Optional[Dict]:
|
||||
"""Извлекает структурированные данные из текста с помощью Claude API."""
|
||||
prompt = EXTRACTION_PROMPT.format(text=text[:8000]) # Ограничиваем длину
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=120.0) as client:
|
||||
response = await client.post(
|
||||
"https://api.anthropic.com/v1/messages",
|
||||
headers={
|
||||
"x-api-key": api_key,
|
||||
"anthropic-version": "2023-06-01",
|
||||
"content-type": "application/json"
|
||||
},
|
||||
json={
|
||||
"model": "claude-sonnet-4-20250514",
|
||||
"max_tokens": 4096,
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": prompt
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(f"Claude API error: {response.status_code} - {response.text}")
|
||||
return None
|
||||
|
||||
result = response.json()
|
||||
content = result["content"][0]["text"]
|
||||
|
||||
# Убираем markdown блоки если есть
|
||||
if "```json" in content:
|
||||
content = content.split("```json")[1].split("```")[0].strip()
|
||||
elif "```" in content:
|
||||
content = content.split("```")[1].split("```")[0].strip()
|
||||
|
||||
data = json.loads(content)
|
||||
return data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка извлечения данных: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def count_high_confidence_fields(data: Dict) -> int:
|
||||
"""Подсчитывает количество полей с high confidence."""
|
||||
count = 0
|
||||
for field in FIELDS:
|
||||
if field in data and data[field].get("confidence") == "high":
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
async def save_to_database(
|
||||
file_path: Path,
|
||||
raw_text: str,
|
||||
extracted_data: Dict,
|
||||
db_url: str
|
||||
) -> bool:
|
||||
"""Сохраняет данные в PostgreSQL."""
|
||||
try:
|
||||
# Создаем async engine
|
||||
engine = create_async_engine(db_url, echo=False)
|
||||
async_session = sessionmaker(
|
||||
engine, class_=AsyncSession, expire_on_commit=False
|
||||
)
|
||||
|
||||
async with async_session() as session:
|
||||
# Сохраняем raw документ
|
||||
raw_doc_query = text("""
|
||||
INSERT INTO raw_documents (filename, raw_text, extracted_json, created_at)
|
||||
VALUES (:filename, :raw_text, :extracted_json, NOW())
|
||||
RETURNING id
|
||||
""")
|
||||
|
||||
result = await session.execute(
|
||||
raw_doc_query,
|
||||
{
|
||||
"filename": file_path.name,
|
||||
"raw_text": raw_text,
|
||||
"extracted_json": json.dumps(extracted_data, ensure_ascii=False)
|
||||
}
|
||||
)
|
||||
doc_id = result.scalar_one()
|
||||
|
||||
# Подготавливаем данные для cases
|
||||
case_data = {
|
||||
"raw_document_id": doc_id,
|
||||
"age": extracted_data.get("age_years", {}).get("value"),
|
||||
"gender": extracted_data.get("gender", {}).get("value"),
|
||||
"has_diagnosis": extracted_data.get("has_diagnosis", {}).get("value"),
|
||||
"diagnosis_type": extracted_data.get("diagnosis_type", {}).get("value"),
|
||||
"has_transport": extracted_data.get("has_transport", {}).get("value"),
|
||||
"season": extracted_data.get("season", {}).get("value"),
|
||||
"time_of_day": extracted_data.get("time_of_day", {}).get("value"),
|
||||
"elapsed_before_report_h": extracted_data.get("elapsed_before_report_h", {}).get("value"),
|
||||
"last_seen_direction": extracted_data.get("last_seen_direction", {}).get("value"),
|
||||
"terrain_primary": extracted_data.get("terrain_primary", {}).get("value"),
|
||||
"water_nearby": extracted_data.get("water_nearby", {}).get("value"),
|
||||
"road_nearby": extracted_data.get("road_nearby", {}).get("value"),
|
||||
"found_alive": extracted_data.get("found_alive", {}).get("value"),
|
||||
"found_distance_km": extracted_data.get("found_distance_km", {}).get("value"),
|
||||
"found_direction": extracted_data.get("found_direction", {}).get("value"),
|
||||
"found_location_type": extracted_data.get("found_location_type", {}).get("value"),
|
||||
"search_duration_hours": extracted_data.get("search_duration_hours", {}).get("value"),
|
||||
"who_found": extracted_data.get("who_found", {}).get("value")
|
||||
}
|
||||
|
||||
# Сохраняем case
|
||||
case_query = text("""
|
||||
INSERT INTO cases (
|
||||
raw_document_id, age, gender, has_diagnosis, diagnosis_type,
|
||||
has_transport, season, time_of_day, elapsed_before_report_h,
|
||||
last_seen_direction, terrain_primary, water_nearby, road_nearby,
|
||||
found_alive, found_distance_km, found_direction, found_location_type,
|
||||
search_duration_hours, who_found, created_at
|
||||
)
|
||||
VALUES (
|
||||
:raw_document_id, :age, :gender, :has_diagnosis, :diagnosis_type,
|
||||
:has_transport, :season, :time_of_day, :elapsed_before_report_h,
|
||||
:last_seen_direction, :terrain_primary, :water_nearby, :road_nearby,
|
||||
:found_alive, :found_distance_km, :found_direction, :found_location_type,
|
||||
:search_duration_hours, :who_found, NOW()
|
||||
)
|
||||
""")
|
||||
|
||||
await session.execute(case_query, case_data)
|
||||
await session.commit()
|
||||
|
||||
logger.info(f"✓ Сохранено в БД: {file_path.name} (doc_id={doc_id})")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка сохранения в БД: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def process_file(
|
||||
file_path: Path,
|
||||
api_key: str,
|
||||
db_url: Optional[str],
|
||||
dry_run: bool
|
||||
) -> Dict:
|
||||
"""Обрабатывает один файл."""
|
||||
logger.info(f"Обработка: {file_path.name}")
|
||||
|
||||
# Извлекаем текст
|
||||
text = await extract_text_from_docx(file_path)
|
||||
if not text:
|
||||
return {
|
||||
"file": file_path.name,
|
||||
"status": "error",
|
||||
"reason": "Не удалось извлечь текст"
|
||||
}
|
||||
|
||||
# Извлекаем данные через Claude
|
||||
extracted_data = await extract_data_with_claude(text, api_key)
|
||||
if not extracted_data:
|
||||
return {
|
||||
"file": file_path.name,
|
||||
"status": "error",
|
||||
"reason": "Ошибка извлечения данных"
|
||||
}
|
||||
|
||||
# Подсчитываем high confidence поля
|
||||
high_conf_count = count_high_confidence_fields(extracted_data)
|
||||
|
||||
logger.info(f" Полей с high confidence: {high_conf_count}/{len(FIELDS)}")
|
||||
|
||||
# Проверяем минимальный порог
|
||||
if high_conf_count < 6:
|
||||
logger.warning(f" ⚠ Пропущено: недостаточно полей с high confidence ({high_conf_count} < 6)")
|
||||
return {
|
||||
"file": file_path.name,
|
||||
"status": "skipped",
|
||||
"reason": f"Недостаточно полей с high confidence ({high_conf_count} < 6)",
|
||||
"high_confidence_count": high_conf_count
|
||||
}
|
||||
|
||||
# Сохраняем в БД если не dry-run
|
||||
if not dry_run and db_url:
|
||||
success = await save_to_database(file_path, text, extracted_data, db_url)
|
||||
status = "success" if success else "db_error"
|
||||
else:
|
||||
logger.info(f" [DRY RUN] Данные не сохранены в БД")
|
||||
status = "dry_run"
|
||||
|
||||
return {
|
||||
"file": file_path.name,
|
||||
"status": status,
|
||||
"high_confidence_count": high_conf_count,
|
||||
"extracted_data": extracted_data if dry_run else None
|
||||
}
|
||||
|
||||
|
||||
async def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Парсинг спецдонесений МЧС из .docx файлов"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dir",
|
||||
required=True,
|
||||
help="Папка с .docx файлами"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--db",
|
||||
help="DATABASE_URL для PostgreSQL"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Не записывать в БД, только показать результаты"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Проверяем API ключ
|
||||
api_key = os.getenv("ANTHROPIC_API_KEY")
|
||||
if not api_key:
|
||||
logger.error("ANTHROPIC_API_KEY не установлен в переменных окружения")
|
||||
sys.exit(1)
|
||||
|
||||
# Проверяем директорию
|
||||
dir_path = Path(args.dir)
|
||||
if not dir_path.exists() or not dir_path.is_dir():
|
||||
logger.error(f"Директория не найдена: {args.dir}")
|
||||
sys.exit(1)
|
||||
|
||||
# Получаем список .docx файлов
|
||||
docx_files = list(dir_path.glob("*.docx"))
|
||||
if not docx_files:
|
||||
logger.warning(f"Не найдено .docx файлов в {args.dir}")
|
||||
sys.exit(0)
|
||||
|
||||
logger.info(f"Найдено файлов: {len(docx_files)}")
|
||||
logger.info(f"Режим: {'DRY RUN' if args.dry_run else 'ЗАПИСЬ В БД'}")
|
||||
|
||||
# Обрабатываем файлы
|
||||
results = []
|
||||
for file_path in docx_files:
|
||||
result = await process_file(file_path, api_key, args.db, args.dry_run)
|
||||
results.append(result)
|
||||
|
||||
# Небольшая пауза между запросами к API
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Выводим итоговую статистику
|
||||
logger.info("\n" + "="*60)
|
||||
logger.info("ИТОГОВАЯ СТАТИСТИКА")
|
||||
logger.info("="*60)
|
||||
|
||||
success_count = sum(1 for r in results if r["status"] == "success")
|
||||
skipped_count = sum(1 for r in results if r["status"] == "skipped")
|
||||
error_count = sum(1 for r in results if r["status"] == "error")
|
||||
dry_run_count = sum(1 for r in results if r["status"] == "dry_run")
|
||||
|
||||
logger.info(f"Всего файлов: {len(results)}")
|
||||
logger.info(f"Успешно обработано: {success_count}")
|
||||
logger.info(f"Пропущено (< 6 high conf): {skipped_count}")
|
||||
logger.info(f"Ошибки: {error_count}")
|
||||
if dry_run_count > 0:
|
||||
logger.info(f"Dry run: {dry_run_count}")
|
||||
|
||||
# Сохраняем детальный отчет
|
||||
report_path = Path("parse_report.json")
|
||||
with open(report_path, "w", encoding="utf-8") as f:
|
||||
json.dump(results, f, ensure_ascii=False, indent=2)
|
||||
|
||||
logger.info(f"\nДетальный отчет сохранен в: {report_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user