269 lines
10 KiB
Python
269 lines
10 KiB
Python
|
|
#!/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'<script[^>]*>.*?</script>', ' ', html, flags=re.DOTALL | re.IGNORECASE)
|
|||
|
|
text = re.sub(r'<style[^>]*>.*?</style>', ' ', 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()
|