Проект аудита отелей: основные скрипты и документация
- Краулеры: 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:
320
check_rkn_registry.py
Normal file
320
check_rkn_registry.py
Normal file
@@ -0,0 +1,320 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Проверка отелей в реестре операторов персональных данных Роскомнадзора
|
||||
Проверяет только отели с сайтами
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
from playwright.async_api import async_playwright
|
||||
from urllib.parse import unquote
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import re
|
||||
|
||||
# Настройка логирования
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler(f'rkn_check_{datetime.now().strftime("%Y%m%d_%H%M%S")}.log'),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Конфигурация БД
|
||||
DB_CONFIG = {
|
||||
'host': "147.45.189.234",
|
||||
'port': 5432,
|
||||
'database': "default_db",
|
||||
'user': "gen_user",
|
||||
'password': unquote("2~~9_%5EkVsU%3F2%5CS")
|
||||
}
|
||||
|
||||
# Конфигурация
|
||||
REQUEST_DELAY = 3 # Задержка между запросами (секунды)
|
||||
PAGE_TIMEOUT = 30000
|
||||
|
||||
|
||||
class RKNChecker:
|
||||
"""Проверка в реестре РКН"""
|
||||
|
||||
def __init__(self):
|
||||
self.db_conn = None
|
||||
self.browser = None
|
||||
self.page = None
|
||||
|
||||
def connect_db(self):
|
||||
"""Подключение к БД"""
|
||||
try:
|
||||
self.db_conn = psycopg2.connect(**DB_CONFIG)
|
||||
logger.info("✓ Подключено к PostgreSQL")
|
||||
except Exception as e:
|
||||
logger.error(f"✗ Ошибка подключения к БД: {e}")
|
||||
raise
|
||||
|
||||
def close_db(self):
|
||||
"""Закрытие соединения с БД"""
|
||||
if self.db_conn:
|
||||
self.db_conn.close()
|
||||
|
||||
async def init_browser(self):
|
||||
"""Инициализация браузера"""
|
||||
playwright = await async_playwright().start()
|
||||
self.browser = await playwright.chromium.launch(headless=True)
|
||||
self.page = await self.browser.new_page()
|
||||
|
||||
await self.page.set_viewport_size({"width": 1920, "height": 1080})
|
||||
await self.page.set_extra_http_headers({
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||
})
|
||||
|
||||
logger.info("✓ Браузер инициализирован")
|
||||
|
||||
async def close_browser(self):
|
||||
"""Закрытие браузера"""
|
||||
if self.browser:
|
||||
await self.browser.close()
|
||||
|
||||
async def check_inn_in_registry(self, inn: str) -> dict:
|
||||
"""Проверка ИНН в реестре РКН"""
|
||||
if not inn or inn == '-':
|
||||
return {
|
||||
'found': False,
|
||||
'status': 'no_inn',
|
||||
'message': 'ИНН не указан'
|
||||
}
|
||||
|
||||
try:
|
||||
# Формируем URL
|
||||
url = f'https://pd.rkn.gov.ru/operators-registry/operators-list/?act=search&inn={inn}'
|
||||
|
||||
logger.info(f" 🔍 Проверка ИНН: {inn}")
|
||||
|
||||
# Задержка перед запросом
|
||||
await asyncio.sleep(REQUEST_DELAY)
|
||||
|
||||
# Загружаем страницу
|
||||
response = await self.page.goto(url, timeout=PAGE_TIMEOUT, wait_until='networkidle')
|
||||
|
||||
if response.status != 200:
|
||||
return {
|
||||
'found': False,
|
||||
'status': 'error',
|
||||
'message': f'HTTP {response.status}'
|
||||
}
|
||||
|
||||
# Ждем загрузки
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Получаем текст страницы
|
||||
text = await self.page.evaluate('() => document.body.innerText')
|
||||
|
||||
# Проверяем наличие результатов
|
||||
if 'Не найдено' in text or 'не найдено' in text.lower():
|
||||
logger.info(f" ❌ Не найден в реестре")
|
||||
return {
|
||||
'found': False,
|
||||
'status': 'not_found',
|
||||
'message': 'Не найден в реестре РКН'
|
||||
}
|
||||
|
||||
# Пытаемся извлечь данные
|
||||
# Ищем регистрационный номер (разные форматы: 41-14-000746 или 10-0107355)
|
||||
reg_number_match = re.search(r'(\d{2}-\d{2,4}-\d{6,7})', text)
|
||||
reg_number = reg_number_match.group(1) if reg_number_match else None
|
||||
|
||||
# Ищем дату регистрации
|
||||
date_match = re.search(r'Приказ.*?(\d{2}\.\d{2}\.\d{4})', text)
|
||||
reg_date = date_match.group(1) if date_match else None
|
||||
|
||||
# Ищем название организации
|
||||
org_match = re.search(r'(?:Общество|Индивидуальный предприниматель|Акционерное общество).*?(?=ИНН:|$)', text, re.IGNORECASE)
|
||||
org_name = org_match.group(0).strip() if org_match else None
|
||||
|
||||
if reg_number:
|
||||
logger.info(f" ✅ Найден: {reg_number} ({reg_date})")
|
||||
return {
|
||||
'found': True,
|
||||
'status': 'found',
|
||||
'reg_number': reg_number,
|
||||
'reg_date': reg_date,
|
||||
'org_name': org_name,
|
||||
'message': f'Зарегистрирован: {reg_number}'
|
||||
}
|
||||
else:
|
||||
logger.info(f" ⚠️ Страница загружена, но данные не распознаны")
|
||||
return {
|
||||
'found': None,
|
||||
'status': 'unclear',
|
||||
'message': 'Результат неясен'
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f" ✗ Ошибка проверки: {e}")
|
||||
return {
|
||||
'found': False,
|
||||
'status': 'error',
|
||||
'message': str(e)
|
||||
}
|
||||
|
||||
def save_result(self, hotel_id: str, result: dict):
|
||||
"""Сохранение результата в БД"""
|
||||
try:
|
||||
cur = self.db_conn.cursor()
|
||||
|
||||
cur.execute('''
|
||||
UPDATE hotel_main
|
||||
SET
|
||||
rkn_registry_status = %s,
|
||||
rkn_registry_number = %s,
|
||||
rkn_registry_date = %s,
|
||||
rkn_checked_at = %s
|
||||
WHERE id = %s
|
||||
''', (
|
||||
result['status'],
|
||||
result.get('reg_number'),
|
||||
result.get('reg_date'),
|
||||
datetime.now(),
|
||||
hotel_id
|
||||
))
|
||||
|
||||
self.db_conn.commit()
|
||||
cur.close()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f" ✗ Ошибка сохранения в БД: {e}")
|
||||
self.db_conn.rollback()
|
||||
|
||||
async def process_hotels(self, region_name=None):
|
||||
"""Обработка отелей"""
|
||||
|
||||
# Получаем отели с сайтами
|
||||
cur = self.db_conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
where_clause = ""
|
||||
params = []
|
||||
if region_name:
|
||||
where_clause = "AND h.region_name ILIKE %s"
|
||||
params = [f'%{region_name}%']
|
||||
|
||||
query = f'''
|
||||
SELECT DISTINCT h.id, h.full_name, h.owner_inn, h.website_address, h.region_name
|
||||
FROM hotel_main h
|
||||
WHERE h.owner_inn IS NOT NULL
|
||||
AND h.owner_inn != ''
|
||||
AND h.owner_inn != '-'
|
||||
AND (h.rkn_checked_at IS NULL OR h.rkn_checked_at < NOW() - INTERVAL '30 days')
|
||||
{where_clause}
|
||||
ORDER BY h.region_name, h.full_name
|
||||
'''
|
||||
|
||||
cur.execute(query, params)
|
||||
hotels = cur.fetchall()
|
||||
cur.close()
|
||||
|
||||
logger.info(f"\n{'='*70}")
|
||||
logger.info(f"🏨 Отелей для проверки: {len(hotels)}")
|
||||
logger.info(f"⏱️ Примерное время: {len(hotels) * REQUEST_DELAY / 60:.1f} минут")
|
||||
logger.info(f"{'='*70}\n")
|
||||
|
||||
# Обрабатываем отели
|
||||
results = {
|
||||
'found': 0,
|
||||
'not_found': 0,
|
||||
'error': 0,
|
||||
'unclear': 0,
|
||||
'no_inn': 0
|
||||
}
|
||||
|
||||
for i, hotel in enumerate(hotels, 1):
|
||||
logger.info(f"\n[{i}/{len(hotels)}] {'='*50}")
|
||||
logger.info(f"🏨 {hotel['full_name']}")
|
||||
logger.info(f"📍 {hotel['region_name']}")
|
||||
logger.info(f"🌐 {hotel['website_address']}")
|
||||
logger.info(f"🔢 ИНН: {hotel['owner_inn']}")
|
||||
|
||||
# Проверяем в реестре
|
||||
result = await self.check_inn_in_registry(hotel['owner_inn'])
|
||||
|
||||
# Сохраняем результат
|
||||
self.save_result(hotel['id'], result)
|
||||
|
||||
# Обновляем статистику
|
||||
if result['found'] == True:
|
||||
results['found'] += 1
|
||||
elif result['found'] == False:
|
||||
if result['status'] == 'no_inn':
|
||||
results['no_inn'] += 1
|
||||
elif result['status'] == 'not_found':
|
||||
results['not_found'] += 1
|
||||
else:
|
||||
results['error'] += 1
|
||||
else:
|
||||
results['unclear'] += 1
|
||||
|
||||
# Итоги
|
||||
logger.info(f"\n{'='*70}")
|
||||
logger.info("📊 ИТОГИ ПРОВЕРКИ:")
|
||||
logger.info(f" ✅ Найдено в реестре: {results['found']}")
|
||||
logger.info(f" ❌ Не найдено в реестре: {results['not_found']}")
|
||||
logger.info(f" ⚠️ Ошибки: {results['error']}")
|
||||
logger.info(f" ❓ Неясно: {results['unclear']}")
|
||||
logger.info(f" 🔢 Нет ИНН: {results['no_inn']}")
|
||||
logger.info(f"{'='*70}")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
async def main():
|
||||
"""Основная функция"""
|
||||
import sys
|
||||
|
||||
region = sys.argv[1] if len(sys.argv) > 1 else None
|
||||
|
||||
checker = RKNChecker()
|
||||
|
||||
try:
|
||||
# Подключаемся к БД
|
||||
checker.connect_db()
|
||||
|
||||
# Добавляем колонки для РКН (если их нет)
|
||||
cur = checker.db_conn.cursor()
|
||||
cur.execute('''
|
||||
ALTER TABLE hotel_main
|
||||
ADD COLUMN IF NOT EXISTS rkn_registry_status VARCHAR(50);
|
||||
''')
|
||||
cur.execute('''
|
||||
ALTER TABLE hotel_main
|
||||
ADD COLUMN IF NOT EXISTS rkn_registry_number VARCHAR(50);
|
||||
''')
|
||||
cur.execute('''
|
||||
ALTER TABLE hotel_main
|
||||
ADD COLUMN IF NOT EXISTS rkn_registry_date VARCHAR(20);
|
||||
''')
|
||||
cur.execute('''
|
||||
ALTER TABLE hotel_main
|
||||
ADD COLUMN IF NOT EXISTS rkn_checked_at TIMESTAMP;
|
||||
''')
|
||||
checker.db_conn.commit()
|
||||
cur.close()
|
||||
logger.info("✓ Колонки для РКН добавлены")
|
||||
|
||||
# Инициализируем браузер
|
||||
await checker.init_browser()
|
||||
|
||||
# Обрабатываем отели
|
||||
await checker.process_hotels(region)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Критическая ошибка: {e}")
|
||||
finally:
|
||||
await checker.close_browser()
|
||||
checker.close_db()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
Reference in New Issue
Block a user