Проект аудита отелей: основные скрипты и документация
- Краулеры: smart_crawler.py, regional_crawler.py - Аудит: audit_orel_to_excel.py, audit_chukotka_to_excel.py - РКН проверка: check_rkn_registry.py, recheck_unclear_rkn.py - Отчёты: create_orel_horizontal_report.py - Обработка: process_all_hotels_embeddings.py - Документация: README.md, DB_SCHEMA_REFERENCE.md
This commit is contained in:
241
natasha_ner_api.py
Normal file
241
natasha_ner_api.py
Normal file
@@ -0,0 +1,241 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
FastAPI сервис для Natasha NER (Named Entity Recognition)
|
||||
Извлекает организации, адреса, имена из текста
|
||||
Для использования в n8n через HTTP Request
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Header, Depends
|
||||
from fastapi.security import APIKeyHeader
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
import uvicorn
|
||||
import os
|
||||
|
||||
# Natasha для NER
|
||||
from natasha import (
|
||||
Segmenter,
|
||||
MorphVocab,
|
||||
NewsEmbedding,
|
||||
NewsMorphTagger,
|
||||
NewsSyntaxParser,
|
||||
NewsNERTagger,
|
||||
Doc
|
||||
)
|
||||
|
||||
app = FastAPI(
|
||||
title="Natasha NER API",
|
||||
description="Извлечение сущностей из русского текста",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
# 🔐 API KEY для защиты доступа
|
||||
API_KEY = "CH2BAYBYGYDDSWpaEd_CvJrH04DoVSGtZi_mah2nXbw"
|
||||
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
|
||||
|
||||
def verify_api_key(api_key: str = Depends(api_key_header)) -> bool:
|
||||
"""Проверка API ключа"""
|
||||
if api_key is None or api_key != API_KEY:
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="Неверный или отсутствующий API ключ. Используйте заголовок X-API-Key"
|
||||
)
|
||||
return True
|
||||
|
||||
# Инициализация Natasha при старте
|
||||
print("🔧 Инициализация Natasha...")
|
||||
print(f"🔐 API защищён ключом: {API_KEY[:10]}...")
|
||||
segmenter = Segmenter()
|
||||
morph_vocab = MorphVocab()
|
||||
emb = NewsEmbedding()
|
||||
morph_tagger = NewsMorphTagger(emb)
|
||||
syntax_parser = NewsSyntaxParser(emb)
|
||||
ner_tagger = NewsNERTagger(emb)
|
||||
print("✅ Natasha готова!")
|
||||
|
||||
|
||||
class NERRequest(BaseModel):
|
||||
text: str
|
||||
max_length: int = 5000 # Ограничение длины текста для производительности
|
||||
|
||||
|
||||
class Entity(BaseModel):
|
||||
type: str # ORG, PER, LOC
|
||||
text: str
|
||||
start: int
|
||||
end: int
|
||||
|
||||
|
||||
class NERResponse(BaseModel):
|
||||
organizations: List[str] # ORG - организации
|
||||
persons: List[str] # PER - люди
|
||||
locations: List[str] # LOC - локации/адреса
|
||||
entities: List[Entity] # Все сущности с позициями
|
||||
total_entities: int
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""Информация о сервисе"""
|
||||
return {
|
||||
"service": "Natasha NER API",
|
||||
"version": "1.1.0",
|
||||
"description": "Извлечение сущностей из русского текста",
|
||||
"security": "Требуется API ключ в заголовке X-API-Key",
|
||||
"endpoints": {
|
||||
"/extract": "POST - извлечь сущности из текста (требует API ключ)",
|
||||
"/extract_simple": "POST - упрощённое извлечение (требует API ключ)",
|
||||
"/health": "GET - проверка здоровья сервиса (без ключа)"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
"""Проверка здоровья сервиса"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"natasha": "ready"
|
||||
}
|
||||
|
||||
|
||||
@app.post("/extract", response_model=NERResponse)
|
||||
async def extract_entities(request: NERRequest, authenticated: bool = Depends(verify_api_key)):
|
||||
"""
|
||||
Извлечение сущностей из текста (требует API ключ)
|
||||
|
||||
Возвращает:
|
||||
- organizations: список названий организаций
|
||||
- persons: список имён людей
|
||||
- locations: список локаций/адресов
|
||||
- entities: все сущности с позициями
|
||||
"""
|
||||
try:
|
||||
# Ограничиваем длину текста для производительности
|
||||
text = request.text[:request.max_length]
|
||||
|
||||
# Обработка текста Natasha
|
||||
doc = Doc(text)
|
||||
doc.segment(segmenter)
|
||||
doc.tag_morph(morph_tagger)
|
||||
doc.parse_syntax(syntax_parser)
|
||||
doc.tag_ner(ner_tagger)
|
||||
|
||||
# Извлекаем сущности
|
||||
organizations = []
|
||||
persons = []
|
||||
locations = []
|
||||
entities = []
|
||||
|
||||
for span in doc.spans:
|
||||
entity = Entity(
|
||||
type=span.type,
|
||||
text=span.text,
|
||||
start=span.start,
|
||||
end=span.stop
|
||||
)
|
||||
entities.append(entity)
|
||||
|
||||
if span.type == 'ORG':
|
||||
organizations.append(span.text)
|
||||
elif span.type == 'PER':
|
||||
persons.append(span.text)
|
||||
elif span.type == 'LOC':
|
||||
locations.append(span.text)
|
||||
|
||||
return NERResponse(
|
||||
organizations=list(set(organizations)), # Уникальные
|
||||
persons=list(set(persons)),
|
||||
locations=list(set(locations)),
|
||||
entities=entities,
|
||||
total_entities=len(entities)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Ошибка NER: {str(e)}")
|
||||
|
||||
|
||||
@app.post("/extract_simple")
|
||||
async def extract_simple(request: NERRequest, authenticated: bool = Depends(verify_api_key)):
|
||||
"""
|
||||
Упрощённое извлечение - только списки сущностей
|
||||
Для удобного использования в n8n (требует API ключ)
|
||||
С умной фильтрацией ложноположительных результатов
|
||||
"""
|
||||
try:
|
||||
text = request.text[:request.max_length]
|
||||
|
||||
doc = Doc(text)
|
||||
doc.segment(segmenter)
|
||||
doc.tag_morph(morph_tagger)
|
||||
doc.parse_syntax(syntax_parser)
|
||||
doc.tag_ner(ner_tagger)
|
||||
|
||||
organizations = []
|
||||
persons = []
|
||||
locations = []
|
||||
|
||||
# Паттерны для фильтрации
|
||||
org_keywords = ['ип', 'ооо', 'оао', 'зао', 'ао', 'пао', 'нао', 'ндо', 'гуп', 'муп', 'фгуп', 'гбу', 'мбу']
|
||||
ignore_org_patterns = [
|
||||
r'^\d+', # Начинается с цифр (адреса)
|
||||
r'\+\d', # Содержит телефон
|
||||
r'^[А-Яа-я]{1,2}\s', # Короткие слова (предлоги)
|
||||
]
|
||||
ignore_loc_words = ['нужен', 'нужна', 'нужно', 'требуется']
|
||||
|
||||
for span in doc.spans:
|
||||
entity_text = span.text.strip()
|
||||
entity_lower = entity_text.lower()
|
||||
|
||||
if span.type == 'ORG':
|
||||
# Проверяем, что это действительно организация
|
||||
is_valid_org = False
|
||||
|
||||
# Проверка 1: содержит ключевые слова юрлиц
|
||||
if any(keyword in entity_lower for keyword in org_keywords):
|
||||
is_valid_org = True
|
||||
|
||||
# Проверка 2: не содержит паттерны адресов/телефонов
|
||||
import re
|
||||
has_ignore_pattern = any(re.search(pattern, entity_text) for pattern in ignore_org_patterns)
|
||||
|
||||
if is_valid_org and not has_ignore_pattern:
|
||||
organizations.append(entity_text)
|
||||
|
||||
elif span.type == 'PER':
|
||||
persons.append(entity_text)
|
||||
|
||||
elif span.type == 'LOC':
|
||||
# Фильтруем мусорные "локации"
|
||||
if entity_lower not in ignore_loc_words and len(entity_text) > 2:
|
||||
locations.append(entity_text)
|
||||
|
||||
# Уникальные значения
|
||||
organizations = list(set(organizations))
|
||||
persons = list(set(persons))
|
||||
locations = list(set(locations))
|
||||
|
||||
return {
|
||||
"organizations": organizations,
|
||||
"persons": persons,
|
||||
"locations": locations,
|
||||
"has_organizations": len(organizations) > 0,
|
||||
"has_persons": len(persons) > 0,
|
||||
"has_locations": len(locations) > 0,
|
||||
"total": len(organizations) + len(persons) + len(locations)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Ошибка NER: {str(e)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🚀 Запуск Natasha NER API на порту 8004...")
|
||||
uvicorn.run(app, host="0.0.0.0", port=8004)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user