#!/usr/bin/env python3 """ Создание hotel_website_processed для Санкт-Петербурга ЭТАП 1: Очистка HTML через регулярки + многопоточность (как в Ореле/Чукотке) """ import psycopg2 import psycopg2.extras import logging import re import html as html_module from concurrent.futures import ThreadPoolExecutor, as_completed import time from typing import Dict, List, Any from urllib.parse import unquote # Настройка логирования logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('spb_processed_regex.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') } # Многопоточность MAX_WORKERS = 10 # Количество потоков для обработки (как в Ореле) class SpbProcessor: def __init__(self): self.conn = None self.cur = None def connect_db(self): """Подключение к БД""" try: self.conn = psycopg2.connect(**DB_CONFIG) self.cur = self.conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) logger.info("✅ Подключение к БД установлено") except Exception as e: logger.error(f"❌ Ошибка подключения к БД: {e}") raise def close_db(self): """Закрытие соединения с БД""" if self.cur: self.cur.close() if self.conn: self.conn.close() logger.info("🔌 Соединение с БД закрыто") def clean_html_with_regex(self, html: str) -> str: """Очистка HTML через регулярки (как в Ореле)""" if not html: return "" try: # Удаляем script и style теги text = re.sub(r']*>.*?', ' ', html, flags=re.DOTALL | re.IGNORECASE) text = re.sub(r']*>.*?', ' ', text, flags=re.DOTALL | re.IGNORECASE) # Удаляем все HTML теги text = re.sub(r'<[^>]+>', ' ', text) # Декодируем HTML entities text = html_module.unescape(text) # Убираем лишние пробелы и переносы строк text = re.sub(r'\s+', ' ', text).strip() return text except Exception as e: logger.error(f"❌ Ошибка очистки HTML: {e}") return "" def process_page(self, page_data: Dict[str, Any]) -> Dict[str, Any]: """Обработка одной страницы""" try: page_id = page_data['id'] url = page_data['url'] html = page_data['html'] hotel_id = page_data['hotel_id'] # Очищаем HTML cleaned_text = self.clean_html_with_regex(html) if len(cleaned_text) < 100: return { 'success': False, 'page_id': page_id, 'error': 'Слишком короткий текст', 'hotel_id': hotel_id, 'url': url } return { 'success': True, 'page_id': page_id, 'hotel_id': hotel_id, 'url': url, 'cleaned_text': cleaned_text, 'length': len(cleaned_text) } except Exception as e: return { 'success': False, 'page_id': page_data.get('id', 'unknown'), 'error': str(e), 'hotel_id': page_data.get('hotel_id', 'unknown'), 'url': page_data.get('url', 'unknown') } def process_hotel_pages(self, hotel_id: str) -> int: """Обработка всех страниц одного отеля (многопоточно)""" try: # Получаем HTML страницы отеля self.cur.execute(""" SELECT id, url, html FROM hotel_website_raw WHERE hotel_id = %s AND html IS NOT NULL ORDER BY id """, (hotel_id,)) pages = self.cur.fetchall() if not pages: logger.warning(f"⚠️ Нет HTML для отеля {hotel_id}") return 0 logger.info(f"📄 Найдено {len(pages)} страниц для отеля") # Подготавливаем данные для многопоточности page_data_list = [] for page in pages: page_data_list.append({ 'id': page['id'], 'url': page['url'], 'html': page['html'], 'hotel_id': hotel_id }) processed_count = 0 # Многопоточная обработка страниц with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: # Отправляем задачи future_to_page = { executor.submit(self.process_page, page_data): page_data for page_data in page_data_list } # Обрабатываем результаты for future in as_completed(future_to_page): result = future.result() if result['success']: # Сохраняем в hotel_website_processed self.cur.execute(""" INSERT INTO hotel_website_processed (hotel_id, url, cleaned_text) VALUES (%s, %s, %s) ON CONFLICT DO NOTHING """, (result['hotel_id'], result['url'], result['cleaned_text'])) processed_count += 1 logger.info(f" ✅ Страница {result['page_id']}: {result['length']} символов") else: logger.warning(f" ⚠️ Страница {result['page_id']}: {result['error']}") logger.info(f"✅ Отель обработан: {processed_count}/{len(pages)} страниц") return processed_count except Exception as e: logger.error(f"❌ Ошибка обработки отеля {hotel_id}: {e}") return 0 def get_hotels_to_process(self) -> List[str]: """Получаем список отелей для обработки""" try: # Получаем отели из СПб, у которых есть HTML но нет обработанного текста self.cur.execute(""" SELECT DISTINCT hwr.hotel_id FROM hotel_website_raw hwr LEFT JOIN hotel_website_processed hwp ON hwr.hotel_id = hwp.hotel_id WHERE hwr.hotel_id::text LIKE 'spb_%' AND hwr.html IS NOT NULL AND hwp.hotel_id IS NULL ORDER BY hwr.hotel_id """) hotels = [row['hotel_id'] for row in self.cur.fetchall()] logger.info(f"📊 Найдено {len(hotels)} отелей для обработки") return hotels except Exception as e: logger.error(f"❌ Ошибка получения списка отелей: {e}") return [] def run(self): """Основной процесс обработки""" try: logger.info("🚀 Запуск обработки СПб через регулярки + многопоточность") # Подключаемся к БД self.connect_db() # Получаем список отелей hotels = self.get_hotels_to_process() if not hotels: logger.info("✅ Нет отелей для обработки") return total_hotels = len(hotels) processed_hotels = 0 total_pages = 0 logger.info(f"📊 Начинаем обработку {total_hotels} отелей") # Обрабатываем каждый отель for i, hotel_id in enumerate(hotels, 1): logger.info(f"🏨 [{i}/{total_hotels}] Обработка отеля: {hotel_id}") pages_count = self.process_hotel_pages(hotel_id) total_pages += pages_count processed_hotels += 1 # Коммитим каждые 10 отелей if processed_hotels % 10 == 0: self.conn.commit() logger.info(f"💾 Сохранено {processed_hotels} отелей, {total_pages} страниц") # Небольшая пауза между отелями time.sleep(0.1) # Финальный коммит self.conn.commit() logger.info(f"🎉 Обработка завершена!") logger.info(f"📊 Статистика:") logger.info(f" - Обработано отелей: {processed_hotels}/{total_hotels}") logger.info(f" - Обработано страниц: {total_pages}") except Exception as e: logger.error(f"❌ Критическая ошибка: {e}") if self.conn: self.conn.rollback() finally: self.close_db() def main(): """Главная функция""" processor = SpbProcessor() processor.run() if __name__ == "__main__": main()