You've already forked parser_1cmycloud
mirror of
https://github.com/vanyagoncharov/parser_1cmycloud.git
synced 2026-06-19 17:18:10 +02:00
1823 lines
90 KiB
Python
1823 lines
90 KiB
Python
"""
|
|
Парсер документации с сайта 1C MyCloud
|
|
Собирает документацию в текстовый формат для дальнейшей обработки
|
|
"""
|
|
|
|
import sys
|
|
import io
|
|
|
|
# Исправляем кодировку для Windows консоли
|
|
if sys.platform == 'win32':
|
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
|
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
|
|
|
|
import requests
|
|
from bs4 import BeautifulSoup
|
|
import time
|
|
import json
|
|
from urllib.parse import urljoin, urlparse
|
|
from typing import Set, List, Dict
|
|
import os
|
|
|
|
|
|
class DocumentationParser:
|
|
def __init__(self, base_url: str, username: str = None, password: str = None, use_selenium: bool = False, visible_browser: bool = False):
|
|
"""
|
|
Инициализация парсера
|
|
|
|
Args:
|
|
base_url: Базовый URL документации
|
|
username: Имя пользователя для авторизации (опционально)
|
|
password: Пароль для авторизации (опционально)
|
|
use_selenium: Использовать Selenium для работы с JavaScript (по умолчанию False)
|
|
visible_browser: Показывать браузер во время работы (по умолчанию False)
|
|
"""
|
|
self.base_url = base_url.rstrip('/')
|
|
self.session = requests.Session()
|
|
self.session.headers.update({
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
|
})
|
|
self.visited_urls: Set[str] = set()
|
|
self.all_content: List[Dict[str, str]] = []
|
|
self.username = username
|
|
self.password = password
|
|
self.debug_mode = False # Режим отладки для сохранения HTML
|
|
self.save_html = False # Сохранять HTML файлы для последующего парсинга
|
|
self.save_by_pages = False # Сохранять каждую страницу в отдельную папку
|
|
self.use_selenium = use_selenium
|
|
self.visible_browser = visible_browser # Показывать браузер
|
|
self.driver = None
|
|
self.html_output_dir = 'html_pages' # Директория для сохранения HTML файлов
|
|
self.pages_output_dir = 'pages' # Директория для сохранения страниц по папкам
|
|
|
|
if use_selenium:
|
|
try:
|
|
from selenium import webdriver
|
|
from selenium.webdriver.chrome.service import Service
|
|
from selenium.webdriver.chrome.options import Options
|
|
from selenium.webdriver.common.by import By
|
|
from selenium.webdriver.support.ui import WebDriverWait
|
|
from selenium.webdriver.support import expected_conditions as EC
|
|
from webdriver_manager.chrome import ChromeDriverManager
|
|
|
|
chrome_options = Options()
|
|
|
|
# Если не указан видимый режим, запускаем в headless
|
|
if not self.visible_browser:
|
|
chrome_options.add_argument('--headless') # Запуск в фоновом режиме
|
|
print("🔇 Браузер работает в фоновом режиме (headless)")
|
|
else:
|
|
print("👁️ Браузер будет виден во время работы")
|
|
|
|
chrome_options.add_argument('--no-sandbox')
|
|
chrome_options.add_argument('--disable-dev-shm-usage')
|
|
chrome_options.add_argument('--disable-gpu')
|
|
chrome_options.add_argument('--window-size=1920,1080')
|
|
chrome_options.add_argument('user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36')
|
|
|
|
# Пробуем найти Chrome в стандартных местах Windows
|
|
chrome_paths = [
|
|
r'C:\Program Files\Google\Chrome\Application\chrome.exe',
|
|
r'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe',
|
|
os.path.expanduser(r'~\AppData\Local\Google\Chrome\Application\chrome.exe'),
|
|
]
|
|
|
|
chrome_binary = None
|
|
for path in chrome_paths:
|
|
if os.path.exists(path):
|
|
chrome_binary = path
|
|
break
|
|
|
|
if chrome_binary:
|
|
chrome_options.binary_location = chrome_binary
|
|
print(f"✅ Найден Chrome: {chrome_binary}")
|
|
else:
|
|
print("⚠️ Chrome не найден в стандартных местах, пробуем использовать системный")
|
|
|
|
service = Service(ChromeDriverManager().install())
|
|
self.driver = webdriver.Chrome(service=service, options=chrome_options)
|
|
self.driver.implicitly_wait(10)
|
|
print("✅ Selenium инициализирован")
|
|
except ImportError:
|
|
print("⚠️ Selenium не установлен. Установите: pip install selenium webdriver-manager")
|
|
self.use_selenium = False
|
|
except Exception as e:
|
|
print(f"⚠️ Ошибка инициализации Selenium: {e}")
|
|
self.use_selenium = False
|
|
|
|
def login(self, login_url: str = None) -> bool:
|
|
"""
|
|
Выполняет авторизацию на сайте
|
|
|
|
Args:
|
|
login_url: URL страницы авторизации (если отличается от базового)
|
|
|
|
Returns:
|
|
True если авторизация успешна, False иначе
|
|
"""
|
|
if not self.username or not self.password:
|
|
print("⚠️ Учетные данные не предоставлены. Попытка доступа без авторизации...")
|
|
return True
|
|
|
|
# Если используется Selenium, выполняем авторизацию через браузер
|
|
if self.use_selenium and self.driver:
|
|
return self._login_selenium(login_url)
|
|
else:
|
|
return self._login_requests(login_url)
|
|
|
|
def _login_selenium(self, login_url: str = None) -> bool:
|
|
"""Авторизация через Selenium"""
|
|
from selenium.webdriver.common.by import By
|
|
from selenium.webdriver.support.ui import WebDriverWait
|
|
from selenium.webdriver.support import expected_conditions as EC
|
|
from selenium.webdriver.common.keys import Keys
|
|
|
|
try:
|
|
print("\n🔐 Начинаю авторизацию через Selenium...")
|
|
|
|
# Используем базовый URL если login_url не указан
|
|
if not login_url:
|
|
login_url = self.base_url
|
|
|
|
print(f" 🌐 Открываю страницу: {login_url}")
|
|
self.driver.get(login_url)
|
|
|
|
# Ждем загрузки страницы
|
|
print(f" ⏳ Жду загрузки страницы...")
|
|
WebDriverWait(self.driver, 15).until(
|
|
lambda d: d.execute_script('return document.readyState') == 'complete'
|
|
)
|
|
time.sleep(2) # Дополнительная задержка для загрузки JavaScript
|
|
|
|
print(f" 🔍 Ищу форму авторизации...")
|
|
|
|
# Пробуем разные способы найти поля ввода
|
|
username_field = None
|
|
password_field = None
|
|
submit_button = None
|
|
|
|
# Вариант 1: По CSS селектору, который предоставил пользователь
|
|
form_container = None
|
|
try:
|
|
form_container = self.driver.find_element(By.CSS_SELECTOR, "body > div > div > div > div.groundItem > div > div > div > div._1v20yr9f > div > div:nth-child(2) > div > div > div > div > div > div:nth-child(1)")
|
|
print(f" ✓ Найден контейнер формы по селектору")
|
|
# Ищем поля внутри контейнера
|
|
if form_container:
|
|
try:
|
|
inputs_in_container = form_container.find_elements(By.TAG_NAME, "input")
|
|
visible_inputs = [inp for inp in inputs_in_container if inp.is_displayed() and inp.is_enabled()]
|
|
if len(visible_inputs) >= 2:
|
|
username_field = visible_inputs[0]
|
|
password_field = visible_inputs[1]
|
|
print(f" ✓ Найдены поля ввода внутри контейнера формы")
|
|
except:
|
|
pass
|
|
except Exception as e:
|
|
print(f" ⚠️ Не удалось найти контейнер по селектору: {e}")
|
|
form_container = None
|
|
|
|
# Вариант 2: Ищем поля ввода по различным атрибутам
|
|
input_selectors = [
|
|
(By.CSS_SELECTOR, "input[type='text']"),
|
|
(By.CSS_SELECTOR, "input[type='email']"),
|
|
(By.CSS_SELECTOR, "input[name*='login']"),
|
|
(By.CSS_SELECTOR, "input[name*='user']"),
|
|
(By.CSS_SELECTOR, "input[name*='email']"),
|
|
(By.CSS_SELECTOR, "input[id*='login']"),
|
|
(By.CSS_SELECTOR, "input[id*='user']"),
|
|
(By.CSS_SELECTOR, "input[placeholder*='логин']"),
|
|
(By.CSS_SELECTOR, "input[placeholder*='телефон']"),
|
|
]
|
|
|
|
for by, selector in input_selectors:
|
|
try:
|
|
fields = self.driver.find_elements(by, selector)
|
|
for field in fields:
|
|
if field.is_displayed() and field.is_enabled():
|
|
field_type = field.get_attribute('type') or ''
|
|
field_name = (field.get_attribute('name') or '').lower()
|
|
field_id = (field.get_attribute('id') or '').lower()
|
|
|
|
if 'password' not in field_type and not username_field:
|
|
username_field = field
|
|
print(f" ✓ Найдено поле логина: {selector}")
|
|
break
|
|
except:
|
|
continue
|
|
|
|
if username_field:
|
|
break
|
|
|
|
# Ищем поле пароля
|
|
password_selectors = [
|
|
(By.CSS_SELECTOR, "input[type='password']"),
|
|
(By.CSS_SELECTOR, "input[name*='pass']"),
|
|
(By.CSS_SELECTOR, "input[id*='pass']"),
|
|
]
|
|
|
|
for by, selector in password_selectors:
|
|
try:
|
|
fields = self.driver.find_elements(by, selector)
|
|
for field in fields:
|
|
if field.is_displayed() and field.is_enabled():
|
|
password_field = field
|
|
print(f" ✓ Найдено поле пароля: {selector}")
|
|
break
|
|
except:
|
|
continue
|
|
|
|
if password_field:
|
|
break
|
|
|
|
if not username_field or not password_field:
|
|
print(f" ⚠️ Не удалось найти поля ввода. Пробую альтернативные методы...")
|
|
# Пробуем найти все видимые input поля
|
|
try:
|
|
all_inputs = self.driver.find_elements(By.TAG_NAME, "input")
|
|
visible_inputs = [inp for inp in all_inputs if inp.is_displayed() and inp.is_enabled()]
|
|
|
|
if len(visible_inputs) >= 2:
|
|
# Первое поле - логин, второе - пароль
|
|
username_field = visible_inputs[0]
|
|
password_field = visible_inputs[1]
|
|
print(f" ✓ Найдены поля ввода по порядку")
|
|
except Exception as e:
|
|
print(f" ❌ Ошибка при поиске полей: {e}")
|
|
return False
|
|
|
|
if not username_field or not password_field:
|
|
print(f" ❌ Не удалось найти поля для авторизации")
|
|
return False
|
|
|
|
# Заполняем поля
|
|
print(f" ⌨️ Ввожу логин...")
|
|
username_field.clear()
|
|
username_field.send_keys(self.username)
|
|
time.sleep(0.5)
|
|
|
|
print(f" ⌨️ Ввожу пароль...")
|
|
password_field.clear()
|
|
password_field.send_keys(self.password)
|
|
time.sleep(0.5)
|
|
|
|
# Ищем кнопку входа (специфично для этого сайта)
|
|
submit_selectors = [
|
|
# Специфичные селекторы для этого сайта
|
|
(By.CSS_SELECTOR, "[data-testid='sign-in-button']"),
|
|
(By.CSS_SELECTOR, "[data-component='button'][data-testid='sign-in-button']"),
|
|
(By.XPATH, "//div[@data-testid='sign-in-button']"),
|
|
(By.XPATH, "//*[contains(@class, '_z81a782')]"),
|
|
# Общие селекторы
|
|
(By.XPATH, "//*[contains(text(), 'Войти')]"),
|
|
(By.XPATH, "//*[contains(text(), 'Вход')]"),
|
|
(By.CSS_SELECTOR, "button[type='submit']"),
|
|
(By.CSS_SELECTOR, "input[type='submit']"),
|
|
(By.XPATH, "//button[contains(text(), 'Войти')]"),
|
|
(By.XPATH, "//button[contains(text(), 'Вход')]"),
|
|
(By.XPATH, "//input[@type='submit']"),
|
|
]
|
|
|
|
for by, selector in submit_selectors:
|
|
try:
|
|
buttons = self.driver.find_elements(by, selector)
|
|
|
|
for button in buttons:
|
|
# Проверяем, что кнопка видима и содержит текст "Войти"
|
|
try:
|
|
button_text = button.text.strip().lower()
|
|
is_visible = button.is_displayed()
|
|
is_enabled = button.is_enabled() if hasattr(button, 'is_enabled') else True
|
|
|
|
if is_visible and is_enabled:
|
|
# Проверяем текст кнопки или data-testid
|
|
if ('войти' in button_text or
|
|
button.get_attribute('data-testid') == 'sign-in-button' or
|
|
'sign-in' in str(button.get_attribute('data-testid')).lower()):
|
|
submit_button = button
|
|
print(f" ✓ Найдена кнопка входа: {selector} (текст: '{button.text}')")
|
|
break
|
|
except:
|
|
continue
|
|
except Exception as e:
|
|
continue
|
|
|
|
if submit_button:
|
|
break
|
|
|
|
# Если кнопка не найдена, пробуем нажать Enter на поле пароля
|
|
if not submit_button:
|
|
print(f" ⚠️ Кнопка входа не найдена, пробую нажать Enter...")
|
|
password_field.send_keys(Keys.RETURN)
|
|
time.sleep(3)
|
|
else:
|
|
print(f" 🖱️ Нажимаю кнопку входа...")
|
|
try:
|
|
# Пробуем кликнуть через JavaScript если обычный клик не работает
|
|
self.driver.execute_script("arguments[0].click();", submit_button)
|
|
print(f" ✓ Кнопка нажата через JavaScript")
|
|
except:
|
|
try:
|
|
submit_button.click()
|
|
print(f" ✓ Кнопка нажата обычным кликом")
|
|
except Exception as e:
|
|
print(f" ⚠️ Ошибка при клике: {e}, пробую Enter...")
|
|
password_field.send_keys(Keys.RETURN)
|
|
time.sleep(3)
|
|
|
|
# Проверяем, успешна ли авторизация
|
|
print(f" ⏳ Жду завершения авторизации (5 сек)...")
|
|
time.sleep(5) # Даем время на обработку авторизации
|
|
|
|
print(f" 🔍 Проверяю результат авторизации...")
|
|
current_url = self.driver.current_url
|
|
print(f" 📍 Текущий URL: {current_url}")
|
|
|
|
page_title = self.driver.title
|
|
print(f" 📄 Заголовок страницы: {page_title}")
|
|
|
|
# Проверяем наличие формы входа
|
|
try:
|
|
login_forms = self.driver.find_elements(By.CSS_SELECTOR, "input[type='password']")
|
|
visible_login_forms = [f for f in login_forms if f.is_displayed()]
|
|
print(f" 🔐 Найдено полей пароля: {len(login_forms)}, видимых: {len(visible_login_forms)}")
|
|
except Exception as e:
|
|
print(f" ⚠️ Ошибка при проверке формы: {e}")
|
|
visible_login_forms = []
|
|
|
|
# Если URL изменился или на странице нет формы входа, считаем авторизацию успешной
|
|
if 'login' not in current_url.lower() and 'auth' not in current_url.lower():
|
|
print(f" ✅ Авторизация успешна! URL изменился")
|
|
return True
|
|
|
|
if not visible_login_forms:
|
|
print(f" ✅ Авторизация успешна! Форма входа исчезла")
|
|
return True
|
|
|
|
# Проверяем наличие меню документации (признак успешной авторизации)
|
|
try:
|
|
menu = self.driver.find_elements(By.CSS_SELECTOR, ".theme-doc-sidebar-menu, .menu__list")
|
|
if menu:
|
|
print(f" ✅ Найдено меню документации - авторизация успешна!")
|
|
return True
|
|
except:
|
|
pass
|
|
|
|
print(f" ⚠️ Не удалось подтвердить успешность авторизации, но продолжаем...")
|
|
print(f" 📋 Переходим к парсингу страницы...")
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f" ❌ Ошибка при авторизации через Selenium: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
return False
|
|
|
|
def _login_requests(self, login_url: str = None) -> bool:
|
|
"""Авторизация через requests (для не-JavaScript сайтов)"""
|
|
# Попытка найти страницу входа
|
|
if not login_url:
|
|
# Пробуем стандартные пути для входа
|
|
possible_paths = [
|
|
'/login',
|
|
'/auth/login',
|
|
'/console/login',
|
|
'/account/login'
|
|
]
|
|
|
|
for path in possible_paths:
|
|
try:
|
|
test_url = urljoin(self.base_url, path)
|
|
response = self.session.get(test_url, timeout=10)
|
|
if response.status_code == 200:
|
|
login_url = test_url
|
|
break
|
|
except:
|
|
continue
|
|
|
|
if not login_url:
|
|
print("⚠️ Не удалось найти страницу входа. Продолжаем без авторизации...")
|
|
return True
|
|
|
|
try:
|
|
# Получаем страницу входа
|
|
response = self.session.get(login_url, timeout=10)
|
|
response.raise_for_status()
|
|
|
|
soup = BeautifulSoup(response.text, 'html.parser')
|
|
|
|
# Ищем форму входа
|
|
form = soup.find('form')
|
|
if not form:
|
|
print("⚠️ Форма входа не найдена. Продолжаем без авторизации...")
|
|
return True
|
|
|
|
# Собираем данные формы
|
|
form_data = {}
|
|
for input_tag in form.find_all(['input', 'select']):
|
|
name = input_tag.get('name')
|
|
if name:
|
|
value = input_tag.get('value', '')
|
|
if input_tag.get('type') != 'password':
|
|
form_data[name] = value
|
|
|
|
# Добавляем учетные данные
|
|
# Ищем поля для логина и пароля
|
|
username_field = None
|
|
password_field = None
|
|
|
|
for input_tag in form.find_all('input'):
|
|
input_type = input_tag.get('type', '').lower()
|
|
input_name = input_tag.get('name', '').lower()
|
|
input_id = input_tag.get('id', '').lower()
|
|
|
|
if input_type == 'email' or 'email' in input_name or 'email' in input_id or 'login' in input_name or 'username' in input_name:
|
|
username_field = input_tag.get('name') or input_tag.get('id')
|
|
elif input_type == 'password':
|
|
password_field = input_tag.get('name') or input_tag.get('id')
|
|
|
|
if username_field:
|
|
form_data[username_field] = self.username
|
|
if password_field:
|
|
form_data[password_field] = self.password
|
|
|
|
# Находим URL для отправки формы
|
|
form_action = form.get('action', '')
|
|
if form_action:
|
|
submit_url = urljoin(login_url, form_action)
|
|
else:
|
|
submit_url = login_url
|
|
|
|
# Отправляем форму
|
|
response = self.session.post(submit_url, data=form_data, timeout=10, allow_redirects=True)
|
|
|
|
# Проверяем успешность авторизации
|
|
if response.status_code == 200 or response.status_code == 302:
|
|
print("✅ Авторизация выполнена успешно")
|
|
return True
|
|
else:
|
|
print(f"⚠️ Авторизация может быть неуспешной. Код ответа: {response.status_code}")
|
|
return True # Продолжаем в любом случае
|
|
|
|
except Exception as e:
|
|
print(f"⚠️ Ошибка при авторизации: {e}")
|
|
print("Продолжаем без авторизации...")
|
|
return True
|
|
|
|
def extract_markdown_content(self, soup: BeautifulSoup) -> str:
|
|
"""
|
|
Извлекает контент из блока markdown и конвертирует в Markdown формат
|
|
|
|
Args:
|
|
soup: BeautifulSoup объект страницы
|
|
|
|
Returns:
|
|
Markdown текст
|
|
"""
|
|
# Ищем блок markdown
|
|
markdown_block = None
|
|
|
|
# Пробуем найти блок markdown по классам
|
|
markdown_selectors = [
|
|
{'class': 'markdown'},
|
|
{'class': 'col col--12 markdown'},
|
|
{'class': 'col', 'class': 'markdown'}, # Если классы разделены
|
|
]
|
|
|
|
for selector in markdown_selectors:
|
|
markdown_block = soup.find('div', selector)
|
|
if markdown_block:
|
|
break
|
|
|
|
# Если не нашли по классам, пробуем найти div с классом markdown
|
|
if not markdown_block:
|
|
markdown_block = soup.select_one('div.markdown, div.col.markdown, .col.col--12.markdown')
|
|
|
|
if not markdown_block:
|
|
return ""
|
|
|
|
# Конвертируем HTML в Markdown
|
|
markdown_lines = []
|
|
|
|
def convert_element_to_markdown(elem):
|
|
"""Рекурсивно конвертирует HTML элемент в Markdown"""
|
|
if not hasattr(elem, 'name') or not elem.name:
|
|
# Текстовый узел
|
|
text = str(elem).strip()
|
|
# Фильтруем CSS/JS код
|
|
if any(pattern in text for pattern in [
|
|
'#mermaid-svg', 'font-family:', 'stroke-width:', 'fill:#',
|
|
'stroke:#', '@keyframes', 'animation:', '.edge-', '.marker',
|
|
'.label', '.cluster', '.node', '.flowchart', 'background-color:',
|
|
'text-anchor:', 'opacity:', 'border-radius:', 'z-index:',
|
|
'font-size:', 'display:inline-block', 'vertical-align:',
|
|
'overflow:visible', '--mermaid-', 'stroke-linecap:',
|
|
'stroke-dasharray:', 'stroke-dashoffset:'
|
|
]):
|
|
return ""
|
|
return text if text else ""
|
|
|
|
tag_name = elem.name.lower()
|
|
|
|
# Игнорируем служебные теги
|
|
if tag_name in ['script', 'style', 'svg', 'noscript']:
|
|
return ""
|
|
|
|
# Заголовки (обрабатываем до проверки классов, т.к. заголовки могут иметь класс 'anchor')
|
|
if tag_name in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']:
|
|
# Извлекаем текст заголовка, игнорируя внутренние элементы
|
|
header_text = ""
|
|
for child in elem.children:
|
|
if hasattr(child, 'name'):
|
|
if child.name in ['a', 'div', 'span']:
|
|
child_classes = child.get('class', [])
|
|
if isinstance(child_classes, str):
|
|
child_classes = [child_classes]
|
|
if any(cls in ['hash-link', 'anchor', 'contextualHeader__intersectionMarker'] for cls in child_classes):
|
|
continue
|
|
header_text += convert_element_to_markdown(child)
|
|
else:
|
|
header_text += str(child).strip() + " "
|
|
level = int(tag_name[1])
|
|
return f"{'#' * level} {header_text.strip()}\n"
|
|
|
|
# Игнорируем служебные элементы (но не заголовки!)
|
|
elem_classes = elem.get('class', [])
|
|
if isinstance(elem_classes, str):
|
|
elem_classes = [elem_classes]
|
|
if any(cls in ['hash-link', 'contextualHeader__intersectionMarker'] for cls in elem_classes):
|
|
return ""
|
|
|
|
# Header (обертка для заголовка)
|
|
if tag_name == 'header':
|
|
header_md = ""
|
|
for child in elem.children:
|
|
header_md += convert_element_to_markdown(child)
|
|
return header_md
|
|
|
|
# Параграфы
|
|
elif tag_name == 'p':
|
|
para_md = ""
|
|
for child in elem.children:
|
|
para_md += convert_element_to_markdown(child)
|
|
return para_md.strip() + "\n\n"
|
|
|
|
# Ссылки
|
|
elif tag_name == 'a':
|
|
href = elem.get('href', '')
|
|
link_text = ""
|
|
for child in elem.children:
|
|
link_text += convert_element_to_markdown(child)
|
|
link_text = link_text.strip()
|
|
if href and link_text:
|
|
return f"[{link_text}]({href})"
|
|
return link_text
|
|
|
|
# Жирный текст
|
|
elif tag_name in ['strong', 'b']:
|
|
text = ""
|
|
for child in elem.children:
|
|
text += convert_element_to_markdown(child)
|
|
return f"**{text.strip()}**"
|
|
|
|
# Курсив
|
|
elif tag_name in ['em', 'i']:
|
|
text = ""
|
|
for child in elem.children:
|
|
text += convert_element_to_markdown(child)
|
|
return f"*{text.strip()}*"
|
|
|
|
# Код (inline)
|
|
elif tag_name == 'code':
|
|
code_text = elem.get_text()
|
|
return f"`{code_text}`"
|
|
|
|
# Блоки кода
|
|
elif tag_name == 'pre':
|
|
code_text = elem.get_text()
|
|
return f"```\n{code_text}\n```\n\n"
|
|
|
|
# Списки
|
|
elif tag_name == 'ul':
|
|
list_items = []
|
|
for li in elem.find_all('li', recursive=False):
|
|
item_text = ""
|
|
for child in li.children:
|
|
item_text += convert_element_to_markdown(child)
|
|
if item_text.strip():
|
|
list_items.append(f"- {item_text.strip()}")
|
|
return '\n'.join(list_items) + "\n\n"
|
|
|
|
elif tag_name == 'ol':
|
|
list_items = []
|
|
for idx, li in enumerate(elem.find_all('li', recursive=False), 1):
|
|
item_text = ""
|
|
for child in li.children:
|
|
item_text += convert_element_to_markdown(child)
|
|
if item_text.strip():
|
|
list_items.append(f"{idx}. {item_text.strip()}")
|
|
return '\n'.join(list_items) + "\n\n"
|
|
|
|
# Элемент списка
|
|
elif tag_name == 'li':
|
|
item_text = ""
|
|
for child in elem.children:
|
|
item_text += convert_element_to_markdown(child)
|
|
return item_text.strip()
|
|
|
|
# Блоки цитат
|
|
elif tag_name == 'blockquote':
|
|
quote_text = ""
|
|
for child in elem.children:
|
|
quote_text += convert_element_to_markdown(child)
|
|
lines = quote_text.strip().split('\n')
|
|
quoted_lines = [f"> {line}" for line in lines if line.strip()]
|
|
return '\n'.join(quoted_lines) + "\n\n"
|
|
|
|
# Горизонтальная линия
|
|
elif tag_name == 'hr':
|
|
return "---\n\n"
|
|
|
|
# Для остальных элементов просто извлекаем текст рекурсивно
|
|
else:
|
|
text = ""
|
|
for child in elem.children:
|
|
text += convert_element_to_markdown(child)
|
|
return text
|
|
|
|
# Обрабатываем все дочерние элементы блока markdown
|
|
for child in markdown_block.children:
|
|
md = convert_element_to_markdown(child)
|
|
if md.strip():
|
|
markdown_lines.append(md)
|
|
|
|
result = '\n'.join(markdown_lines)
|
|
|
|
# Очищаем лишние пустые строки (максимум 2 подряд)
|
|
import re
|
|
result = re.sub(r'\n{3,}', '\n\n', result)
|
|
|
|
return result.strip()
|
|
|
|
def extract_text_content(self, soup: BeautifulSoup) -> str:
|
|
"""
|
|
Извлекает текстовое содержимое из HTML
|
|
|
|
Args:
|
|
soup: BeautifulSoup объект страницы
|
|
|
|
Returns:
|
|
Извлеченный текст
|
|
"""
|
|
# Сохраняем копию для fallback
|
|
original_soup = soup
|
|
|
|
# Удаляем ненужные элементы (но сохраняем их для fallback)
|
|
for element in soup.find_all(['script', 'style']):
|
|
element.decompose()
|
|
|
|
# Пробуем найти основной контент разными способами
|
|
main_content = None
|
|
best_result = ""
|
|
|
|
# Вариант 1: Стандартные HTML5 теги
|
|
main_content = (soup.find('main') or
|
|
soup.find('article') or
|
|
soup.find('section'))
|
|
|
|
if main_content:
|
|
text = main_content.get_text(separator='\n', strip=True)
|
|
lines = [line.strip() for line in text.split('\n') if line.strip() and len(line.strip()) > 2]
|
|
result = '\n'.join(lines)
|
|
if len(result) > len(best_result):
|
|
best_result = result
|
|
|
|
# Вариант 2: Поиск по классам и ID (включая специфичный для этого сайта)
|
|
content_selectors = [
|
|
{'class': 'markdown'}, # Специфичный селектор для этого сайта
|
|
{'class': 'col col--12 markdown'}, # Полный селектор
|
|
{'class': 'content'},
|
|
{'class': 'documentation'},
|
|
{'class': 'doc-content'},
|
|
{'class': 'main-content'},
|
|
{'class': 'page-content'},
|
|
{'class': 'help-content'},
|
|
{'class': 'docs-content'},
|
|
{'id': 'content'},
|
|
{'id': 'main-content'},
|
|
{'id': 'documentation'},
|
|
{'id': 'doc-content'},
|
|
{'id': 'help-content'},
|
|
]
|
|
|
|
for selector in content_selectors:
|
|
main_content = soup.find('div', selector)
|
|
if main_content:
|
|
# Удаляем style и script теги перед извлечением текста
|
|
for tag in main_content.find_all(['style', 'script']):
|
|
tag.decompose()
|
|
|
|
text = main_content.get_text(separator='\n', strip=True)
|
|
lines = [line.strip() for line in text.split('\n') if line.strip() and len(line.strip()) > 2]
|
|
result = '\n'.join(lines)
|
|
if len(result) > len(best_result):
|
|
best_result = result
|
|
break
|
|
|
|
# Вариант 3: Поиск по атрибутам data-* и role
|
|
for attr_selector in [
|
|
{'data-content': True},
|
|
{'role': 'main'},
|
|
{'role': 'article'},
|
|
]:
|
|
main_content = soup.find('div', attr_selector)
|
|
if main_content:
|
|
# Удаляем style и script теги
|
|
for tag in main_content.find_all(['style', 'script']):
|
|
tag.decompose()
|
|
|
|
text = main_content.get_text(separator='\n', strip=True)
|
|
lines = [line.strip() for line in text.split('\n') if line.strip() and len(line.strip()) > 2]
|
|
result = '\n'.join(lines)
|
|
if len(result) > len(best_result):
|
|
best_result = result
|
|
break
|
|
|
|
# Вариант 4: Поиск самого большого div с текстом (но не навигации)
|
|
divs = soup.find_all('div')
|
|
if divs:
|
|
max_text_length = 0
|
|
best_div = None
|
|
for div in divs:
|
|
# Пропускаем навигацию и меню
|
|
div_classes = div.get('class', [])
|
|
div_id = div.get('id', '')
|
|
if any(skip in str(div_classes).lower() + str(div_id).lower()
|
|
for skip in ['nav', 'menu', 'sidebar', 'header', 'footer', 'aside']):
|
|
continue
|
|
|
|
text_length = len(div.get_text(strip=True))
|
|
if text_length > max_text_length and text_length > 100:
|
|
max_text_length = text_length
|
|
best_div = div
|
|
|
|
if best_div:
|
|
# Удаляем style и script теги
|
|
for tag in best_div.find_all(['style', 'script']):
|
|
tag.decompose()
|
|
|
|
text = best_div.get_text(separator='\n', strip=True)
|
|
lines = [line.strip() for line in text.split('\n') if line.strip() and len(line.strip()) > 2]
|
|
result = '\n'.join(lines)
|
|
if len(result) > len(best_result):
|
|
best_result = result
|
|
|
|
# Вариант 5: Извлекаем все параграфы и заголовки
|
|
paragraphs = []
|
|
for tag in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'li', 'td', 'th']:
|
|
for elem in soup.find_all(tag):
|
|
text = elem.get_text(strip=True)
|
|
if text and len(text) > 3:
|
|
paragraphs.append(text)
|
|
|
|
if paragraphs:
|
|
para_result = '\n'.join(paragraphs)
|
|
if len(para_result) > len(best_result):
|
|
best_result = para_result
|
|
|
|
# Fallback: Извлекаем из всего body (удаляем навигацию вручную)
|
|
if len(best_result) < 200:
|
|
body = original_soup.find('body')
|
|
if body:
|
|
# Удаляем навигационные элементы
|
|
for nav_elem in body.find_all(['nav', 'header', 'footer', 'aside', 'script', 'style']):
|
|
nav_elem.decompose()
|
|
|
|
text = body.get_text(separator='\n', strip=True)
|
|
lines = [line.strip() for line in text.split('\n')
|
|
if line.strip() and len(line.strip()) > 2]
|
|
body_result = '\n'.join(lines)
|
|
|
|
if len(body_result) > len(best_result):
|
|
best_result = body_result
|
|
|
|
# Финальная очистка: удаляем повторяющиеся строки и короткие фрагменты
|
|
if best_result:
|
|
final_lines = []
|
|
seen = set()
|
|
for line in best_result.split('\n'):
|
|
line = line.strip()
|
|
if line and len(line) > 3 and line not in seen:
|
|
seen.add(line)
|
|
final_lines.append(line)
|
|
best_result = '\n'.join(final_lines)
|
|
|
|
return best_result if best_result else ""
|
|
|
|
def build_menu_structure(self, soup: BeautifulSoup, current_url: str) -> List[Dict]:
|
|
"""
|
|
Строит структуру меню с иерархией (родитель-дети)
|
|
|
|
Args:
|
|
soup: BeautifulSoup объект страницы
|
|
current_url: Текущий URL страницы
|
|
|
|
Returns:
|
|
Список словарей с информацией о страницах и их вложенности
|
|
"""
|
|
menu_structure = []
|
|
base_domain = urlparse(self.base_url).netloc
|
|
|
|
def extract_menu_items(menu_elem, parent_url=None, level=0):
|
|
"""Рекурсивно извлекает элементы меню с сохранением иерархии"""
|
|
items = []
|
|
|
|
# Ищем все элементы меню (li)
|
|
menu_items = menu_elem.find_all('li', recursive=False)
|
|
|
|
for item in menu_items:
|
|
# Ищем все ссылки в элементе
|
|
link_tags = item.find_all('a', href=True)
|
|
|
|
# Проверяем, есть ли вложенные элементы (дочерние страницы)
|
|
nested_list = item.find('ul', class_='menu__list')
|
|
children = []
|
|
if nested_list:
|
|
children = extract_menu_items(nested_list, None, level + 1)
|
|
|
|
# Ищем ссылку на страницу (не якорь)
|
|
page_link = None
|
|
page_url = None
|
|
page_title = None
|
|
|
|
for link_tag in link_tags:
|
|
href = link_tag.get('href', '')
|
|
if href and not href.startswith('#') and href != '#':
|
|
full_url = urljoin(current_url, href)
|
|
parsed = urlparse(full_url)
|
|
|
|
# Проверяем, что это ссылка на документацию
|
|
if (parsed.netloc == base_domain or not parsed.netloc):
|
|
if '/docs/' in full_url or '/help/' in full_url:
|
|
page_link = link_tag
|
|
page_url = full_url
|
|
page_title = link_tag.get_text(strip=True)
|
|
break
|
|
|
|
# Если нашли ссылку на страницу
|
|
if page_url:
|
|
# Если есть дочерние элементы, используем текущий URL как родителя для них
|
|
if children:
|
|
for child in children:
|
|
if child.get('parent') is None:
|
|
child['parent'] = page_url
|
|
|
|
menu_item = {
|
|
'url': page_url,
|
|
'title': page_title,
|
|
'level': level,
|
|
'parent': parent_url,
|
|
'children': children
|
|
}
|
|
|
|
items.append(menu_item)
|
|
elif children:
|
|
# Нет ссылки на страницу, но есть дочерние элементы - добавляем их напрямую
|
|
# Но устанавливаем родителя для них
|
|
for child in children:
|
|
if child.get('parent') is None:
|
|
child['parent'] = parent_url
|
|
items.extend(children)
|
|
|
|
return items
|
|
|
|
# Ищем меню
|
|
menu_selectors = [
|
|
'ul.theme-doc-sidebar-menu',
|
|
'ul.menu__list',
|
|
]
|
|
|
|
for selector in menu_selectors:
|
|
try:
|
|
menu = soup.select_one(selector)
|
|
if menu:
|
|
print(f" 🔍 Строю структуру меню: {selector}")
|
|
menu_structure = extract_menu_items(menu)
|
|
if menu_structure:
|
|
print(f" ✓ Найдено {len(menu_structure)} элементов меню")
|
|
break
|
|
except Exception as e:
|
|
print(f" ⚠️ Ошибка при построении структуры меню: {e}")
|
|
continue
|
|
|
|
return menu_structure
|
|
|
|
def find_documentation_links(self, soup: BeautifulSoup, current_url: str) -> List[str]:
|
|
"""
|
|
Находит ссылки на документацию НА СЛЕДУЮЩЕМ УРОВНЕ относительно текущей страницы
|
|
|
|
Логика:
|
|
1. Ищем текущую активную страницу в меню (menu__link--active)
|
|
2. Смотрим, есть ли у неё дочерние элементы (вложенный ul.menu__list)
|
|
3. Если есть - возвращаем только их
|
|
4. Если нет - ищем следующий элемент на том же уровне
|
|
|
|
Args:
|
|
soup: BeautifulSoup объект страницы
|
|
current_url: Текущий URL страницы
|
|
|
|
Returns:
|
|
Список URL ссылок на документацию (только следующий уровень!)
|
|
"""
|
|
links = []
|
|
base_domain = urlparse(self.base_url).netloc
|
|
|
|
# Ищем меню
|
|
menu_selectors = [
|
|
'ul.theme-doc-sidebar-menu',
|
|
'ul.menu__list',
|
|
]
|
|
|
|
menu = None
|
|
for selector in menu_selectors:
|
|
menu = soup.select_one(selector)
|
|
if menu:
|
|
print(f" 🔍 Найдено меню: {selector}")
|
|
break
|
|
|
|
if not menu:
|
|
print(f" ⚠️ Меню не найдено")
|
|
return []
|
|
|
|
# Ищем активный элемент меню (текущую страницу)
|
|
active_link = menu.find('a', class_='menu__link--active')
|
|
|
|
if not active_link:
|
|
print(f" ⚠️ Активный элемент не найден")
|
|
print(f" 🔄 Собираю ВСЕ ссылки из раскрытого меню (рекурсивно)")
|
|
|
|
def collect_all_links_recursively(ul_elem):
|
|
"""Рекурсивно собирает ВСЕ ссылки из меню"""
|
|
found_links = []
|
|
for li in ul_elem.find_all('li', recursive=False):
|
|
# Ищем ссылку в текущем LI
|
|
a_tag = li.find('a', class_='menu__link', href=True)
|
|
if a_tag:
|
|
href = a_tag.get('href', '')
|
|
if href and not href.startswith('#') and href != '#':
|
|
full_url = urljoin(current_url, href)
|
|
parsed = urlparse(full_url)
|
|
if (parsed.netloc == base_domain or not parsed.netloc):
|
|
if '/docs/' in full_url or '/help/' in full_url:
|
|
found_links.append(full_url)
|
|
|
|
# Ищем вложенные UL
|
|
nested_ul = li.find('ul', class_='menu__list', recursive=False)
|
|
if nested_ul:
|
|
# Проверяем, что вложенный список виден (раскрыт)
|
|
style = nested_ul.get('style', '')
|
|
# Если нет style или display != none, значит раскрыт
|
|
if 'display: none' not in style and 'display:none' not in style:
|
|
nested_links = collect_all_links_recursively(nested_ul)
|
|
found_links.extend(nested_links)
|
|
|
|
return found_links
|
|
|
|
links = collect_all_links_recursively(menu)
|
|
print(f" ✓ Собрано {len(links)} ссылок из раскрытого меню")
|
|
return links
|
|
|
|
print(f" ✓ Нашел активную страницу в меню: {active_link.get_text(strip=True)}")
|
|
|
|
# Находим родительский LI элемент активной ссылки
|
|
active_li = active_link.find_parent('li')
|
|
if not active_li:
|
|
return []
|
|
|
|
# Проверяем, есть ли вложенный список (дочерние элементы)
|
|
nested_ul = active_li.find('ul', class_='menu__list', recursive=False)
|
|
|
|
if nested_ul:
|
|
# Есть дочерние элементы - возвращаем только их
|
|
print(f" 📂 Найдены дочерние элементы текущей страницы")
|
|
for li in nested_ul.find_all('li', recursive=False):
|
|
a_tag = li.find('a', class_='menu__link', href=True)
|
|
if a_tag:
|
|
href = a_tag.get('href', '')
|
|
if href and not href.startswith('#') and href != '#':
|
|
full_url = urljoin(current_url, href)
|
|
parsed = urlparse(full_url)
|
|
if (parsed.netloc == base_domain or not parsed.netloc):
|
|
if '/docs/' in full_url or '/help/' in full_url:
|
|
links.append(full_url)
|
|
else:
|
|
# Нет дочерних элементов - ищем следующий элемент на том же уровне
|
|
print(f" ➡️ Нет дочерних элементов, ищу следующий на том же уровне")
|
|
|
|
# Находим родительский UL
|
|
parent_ul = active_li.find_parent('ul', class_='menu__list')
|
|
if parent_ul:
|
|
# Получаем все LI на этом уровне
|
|
sibling_lis = parent_ul.find_all('li', recursive=False)
|
|
|
|
# Ищем текущий LI и берём следующий
|
|
found_current = False
|
|
for li in sibling_lis:
|
|
if found_current:
|
|
# Это следующий элемент после текущего
|
|
a_tag = li.find('a', class_='menu__link', href=True)
|
|
if a_tag:
|
|
href = a_tag.get('href', '')
|
|
if href and not href.startswith('#') and href != '#':
|
|
full_url = urljoin(current_url, href)
|
|
parsed = urlparse(full_url)
|
|
if (parsed.netloc == base_domain or not parsed.netloc):
|
|
if '/docs/' in full_url or '/help/' in full_url:
|
|
links.append(full_url)
|
|
break # Берём только один следующий элемент
|
|
|
|
if li == active_li:
|
|
found_current = True
|
|
|
|
# Если следующего элемента нет на этом уровне,
|
|
# поднимаемся на уровень выше и ищем следующий там
|
|
if not links:
|
|
print(f" ⬆️ Нет следующего элемента, поднимаюсь на уровень выше")
|
|
parent_li = parent_ul.find_parent('li')
|
|
if parent_li:
|
|
grandparent_ul = parent_li.find_parent('ul', class_='menu__list')
|
|
if grandparent_ul:
|
|
sibling_lis = grandparent_ul.find_all('li', recursive=False)
|
|
found_parent = False
|
|
for li in sibling_lis:
|
|
if found_parent:
|
|
a_tag = li.find('a', class_='menu__link', href=True)
|
|
if a_tag:
|
|
href = a_tag.get('href', '')
|
|
if href and not href.startswith('#') and href != '#':
|
|
full_url = urljoin(current_url, href)
|
|
parsed = urlparse(full_url)
|
|
if (parsed.netloc == base_domain or not parsed.netloc):
|
|
if '/docs/' in full_url or '/help/' in full_url:
|
|
links.append(full_url)
|
|
break
|
|
|
|
if li == parent_li:
|
|
found_parent = True
|
|
|
|
return links
|
|
|
|
def parse_page(self, url: str) -> Dict[str, str]:
|
|
"""
|
|
Парсит одну страницу документации
|
|
|
|
Args:
|
|
url: URL страницы для парсинга
|
|
|
|
Returns:
|
|
Словарь с URL и содержимым страницы
|
|
"""
|
|
if url in self.visited_urls:
|
|
return None
|
|
|
|
try:
|
|
print(f"📄 Парсинг: {url}")
|
|
|
|
# Используем Selenium если включен
|
|
if self.use_selenium and self.driver:
|
|
return self._parse_page_selenium(url)
|
|
else:
|
|
return self._parse_page_requests(url)
|
|
|
|
except Exception as e:
|
|
print(f"❌ Ошибка при парсинге {url}: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
return None
|
|
|
|
def _parse_page_selenium(self, url: str) -> Dict[str, str]:
|
|
"""Парсинг страницы через Selenium"""
|
|
from selenium.webdriver.common.by import By
|
|
from selenium.webdriver.support.ui import WebDriverWait
|
|
from selenium.webdriver.support import expected_conditions as EC
|
|
|
|
try:
|
|
print(f" 🌐 Открываю страницу в браузере...")
|
|
self.driver.get(url)
|
|
print(f" ✓ Страница открыта")
|
|
|
|
# Ждем загрузки контента (ждем появления основного контента)
|
|
print(f" ⏳ Ожидаю загрузки страницы...")
|
|
try:
|
|
WebDriverWait(self.driver, 20).until(
|
|
lambda d: d.execute_script('return document.readyState') == 'complete'
|
|
)
|
|
print(f" ✓ Страница загружена (readyState = complete)")
|
|
|
|
# Дополнительная задержка для загрузки JavaScript контента
|
|
print(f" ⏳ Жду выполнения JavaScript (2 сек)...")
|
|
time.sleep(2)
|
|
|
|
# Проверяем наличие контента
|
|
try:
|
|
content_div = self.driver.find_elements(By.CSS_SELECTOR, ".col.col--12.markdown, div.markdown")
|
|
if content_div:
|
|
print(f" ✓ Контент найден на странице")
|
|
else:
|
|
print(f" ⚠️ Контент не найден, но продолжаем...")
|
|
except:
|
|
pass
|
|
|
|
print(f" ✓ JavaScript выполнен")
|
|
except Exception as e:
|
|
print(f" ⚠️ Таймаут ожидания загрузки: {e}")
|
|
print(f" ⏳ Продолжаю с текущим состоянием страницы...")
|
|
time.sleep(2) # Минимальная задержка даже при ошибке
|
|
|
|
# Получаем HTML после выполнения JavaScript
|
|
print(f" 📄 Получаю HTML код страницы...")
|
|
html = self.driver.page_source
|
|
print(f" ✓ HTML получен, размер: {len(html)} символов")
|
|
|
|
# Сохраняем HTML для отладки или для последующего парсинга
|
|
if self.debug_mode or self.save_html:
|
|
import os
|
|
if self.save_html:
|
|
# Создаем директорию если её нет
|
|
if not os.path.exists(self.html_output_dir):
|
|
os.makedirs(self.html_output_dir)
|
|
|
|
# Создаем имя файла на основе URL
|
|
url_path = urlparse(url).path.strip('/')
|
|
if not url_path:
|
|
url_path = 'index'
|
|
safe_filename = url_path.replace('/', '_').replace(':', '_')[:200]
|
|
html_file = os.path.join(self.html_output_dir, f"{safe_filename}.html")
|
|
else:
|
|
html_file = f"debug_{urlparse(url).path.replace('/', '_').strip('_') or 'index'}.html"
|
|
|
|
with open(html_file, 'w', encoding='utf-8') as f:
|
|
f.write(html)
|
|
print(f" 💾 HTML сохранен в {html_file}")
|
|
|
|
print(f" 📝 Парсю HTML через BeautifulSoup...")
|
|
soup = BeautifulSoup(html, 'html.parser')
|
|
print(f" ✓ HTML распарсен")
|
|
|
|
print(f" 📝 Извлекаю текст из HTML...")
|
|
# Пробуем извлечь Markdown из блока markdown
|
|
markdown_content = self.extract_markdown_content(soup)
|
|
if markdown_content and len(markdown_content) > 100:
|
|
content = markdown_content
|
|
print(f" 📊 Извлечено в формате Markdown: {len(content)} символов")
|
|
else:
|
|
content = self.extract_text_content(soup)
|
|
print(f" 📊 Извлечено через BeautifulSoup: {len(content)} символов")
|
|
|
|
if len(content) > 0:
|
|
preview = content[:200].replace('\n', ' ')
|
|
print(f" 👀 Превью контента: {preview}...")
|
|
|
|
# Если контент короткий, пробуем дополнительные методы
|
|
if not content or len(content.strip()) < 100:
|
|
print(f" ⚠️ Контент короткий ({len(content) if content else 0} символов), пробуем Selenium селекторы...")
|
|
|
|
# Пробуем найти элементы через Selenium
|
|
try:
|
|
# Ищем основные элементы контента (включая специфичный для этого сайта)
|
|
content_elements = []
|
|
for selector in [
|
|
'.col.col--12.markdown', # Специфичный селектор для этого сайта
|
|
'div.markdown',
|
|
'main',
|
|
'article',
|
|
'[role="main"]',
|
|
'.content',
|
|
'#content',
|
|
'.documentation'
|
|
]:
|
|
try:
|
|
elements = self.driver.find_elements(By.CSS_SELECTOR, selector)
|
|
for elem in elements:
|
|
text = elem.text.strip()
|
|
if text and len(text) > 10:
|
|
content_elements.append(text)
|
|
except:
|
|
continue
|
|
|
|
if content_elements:
|
|
fallback_content = '\n'.join(content_elements)
|
|
if len(fallback_content) > len(content):
|
|
content = fallback_content
|
|
print(f" ✓ Извлечено через Selenium: {len(content)} символов")
|
|
except Exception as e:
|
|
print(f" ⚠️ Ошибка при извлечении через Selenium: {e}")
|
|
|
|
# Проверяем, что контент не пустой
|
|
if not content or len(content.strip()) < 20:
|
|
title = self.driver.title if self.driver.title else ""
|
|
if not title:
|
|
title = soup.title.string if soup.title else url
|
|
|
|
h1 = soup.find('h1')
|
|
h1_text = h1.get_text(strip=True) if h1 else ""
|
|
h2_tags = soup.find_all('h2')
|
|
h2_texts = [h2.get_text(strip=True) for h2 in h2_tags if h2.get_text(strip=True)]
|
|
|
|
fallback_parts = [title, h1_text] + h2_texts
|
|
fallback_content = '\n'.join([part for part in fallback_parts if part])
|
|
|
|
if fallback_content:
|
|
content = fallback_content
|
|
print(f" ✓ Извлечены только заголовки: {len(content)} символов")
|
|
else:
|
|
print(f"❌ Не удалось извлечь контент со страницы")
|
|
content = f"URL: {url}\nЗаголовок страницы не найден"
|
|
|
|
self.visited_urls.add(url)
|
|
|
|
title = self.driver.title if self.driver.title else (soup.title.string if soup.title else url)
|
|
print(f" ✓ Извлечено: {len(content)} символов, заголовок: {title[:50]}")
|
|
|
|
result = {
|
|
'url': url,
|
|
'content': content,
|
|
'title': title
|
|
}
|
|
|
|
# Сохраняем HTML для режима save_by_pages
|
|
if self.save_by_pages:
|
|
result['html'] = html
|
|
|
|
return result
|
|
|
|
except Exception as e:
|
|
print(f"❌ Ошибка при парсинге через Selenium {url}: {e}")
|
|
return None
|
|
|
|
def _parse_page_requests(self, url: str) -> Dict[str, str]:
|
|
"""Парсинг страницы через requests"""
|
|
response = self.session.get(url, timeout=10)
|
|
response.raise_for_status()
|
|
|
|
# Проверяем, что получили HTML
|
|
if not response.text or len(response.text) < 100:
|
|
print(f"⚠️ Страница слишком короткая или пустая: {len(response.text) if response.text else 0} символов")
|
|
if self.debug_mode:
|
|
debug_file = f"debug_{urlparse(url).path.replace('/', '_')}.html"
|
|
with open(debug_file, 'w', encoding='utf-8') as f:
|
|
f.write(response.text)
|
|
print(f" 💾 HTML сохранен в {debug_file}")
|
|
return None
|
|
|
|
# Сохраняем HTML для отладки или для последующего парсинга
|
|
if self.debug_mode or self.save_html:
|
|
import os
|
|
if self.save_html:
|
|
# Создаем директорию если её нет
|
|
if not os.path.exists(self.html_output_dir):
|
|
os.makedirs(self.html_output_dir)
|
|
|
|
# Создаем имя файла на основе URL
|
|
url_path = urlparse(url).path.strip('/')
|
|
if not url_path:
|
|
url_path = 'index'
|
|
safe_filename = url_path.replace('/', '_').replace(':', '_')[:200]
|
|
html_file = os.path.join(self.html_output_dir, f"{safe_filename}.html")
|
|
else:
|
|
html_file = f"debug_{urlparse(url).path.replace('/', '_').strip('_') or 'index'}.html"
|
|
|
|
with open(html_file, 'w', encoding='utf-8') as f:
|
|
f.write(response.text)
|
|
print(f" 💾 HTML сохранен в {html_file}")
|
|
|
|
print(f" 🌐 Отправляю HTTP запрос...")
|
|
print(f" ✓ Получен ответ: {response.status_code}")
|
|
print(f" 📏 Размер HTML: {len(response.text)} символов")
|
|
print(f" 📝 Парсю HTML через BeautifulSoup...")
|
|
|
|
soup = BeautifulSoup(response.text, 'html.parser')
|
|
print(f" 📝 Извлекаю текст из HTML...")
|
|
content = self.extract_text_content(soup)
|
|
print(f" 📊 Извлечено: {len(content)} символов")
|
|
|
|
# Если контент короткий, пробуем дополнительные методы
|
|
if not content or len(content.strip()) < 100:
|
|
print(f" ⚠️ Контент короткий ({len(content) if content else 0} символов), пробуем дополнительные методы...")
|
|
|
|
# Пробуем извлечь все текстовые элементы
|
|
all_text_elements = []
|
|
for tag in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'div', 'span', 'li', 'td', 'th', 'dd', 'dt']:
|
|
for elem in soup.find_all(tag):
|
|
text = elem.get_text(strip=True)
|
|
if text and len(text) > 5:
|
|
all_text_elements.append(text)
|
|
|
|
if all_text_elements:
|
|
# Убираем дубликаты, сохраняя порядок
|
|
seen = set()
|
|
unique_elements = []
|
|
for elem in all_text_elements:
|
|
if elem not in seen:
|
|
seen.add(elem)
|
|
unique_elements.append(elem)
|
|
|
|
fallback_content = '\n'.join(unique_elements)
|
|
if len(fallback_content) > len(content):
|
|
content = fallback_content
|
|
print(f" ✓ Извлечено через fallback: {len(content)} символов")
|
|
|
|
# Если все еще короткий, хотя бы сохраняем заголовки
|
|
if not content or len(content.strip()) < 20:
|
|
title = soup.title.string if soup.title else ""
|
|
h1 = soup.find('h1')
|
|
h1_text = h1.get_text(strip=True) if h1 else ""
|
|
h2_tags = soup.find_all('h2')
|
|
h2_texts = [h2.get_text(strip=True) for h2 in h2_tags if h2.get_text(strip=True)]
|
|
|
|
fallback_parts = [title, h1_text] + h2_texts
|
|
fallback_content = '\n'.join([part for part in fallback_parts if part])
|
|
|
|
if fallback_content:
|
|
content = fallback_content
|
|
print(f" ✓ Извлечены только заголовки: {len(content)} символов")
|
|
else:
|
|
print(f"❌ Не удалось извлечь контент со страницы")
|
|
# Сохраняем хотя бы URL и заголовок страницы
|
|
content = f"URL: {url}\nЗаголовок страницы не найден"
|
|
|
|
self.visited_urls.add(url)
|
|
|
|
title = soup.title.string if soup.title else url
|
|
print(f" ✓ Извлечено: {len(content)} символов, заголовок: {title[:50]}")
|
|
|
|
result = {
|
|
'url': url,
|
|
'content': content,
|
|
'title': title
|
|
}
|
|
|
|
# Сохраняем HTML для режима save_by_pages
|
|
if self.save_by_pages:
|
|
try:
|
|
response = self.session.get(url, timeout=10)
|
|
result['html'] = response.text
|
|
except:
|
|
result['html'] = ""
|
|
|
|
return result
|
|
|
|
def crawl(self, start_url: str = None, max_pages: int = 10000):
|
|
"""
|
|
Обходит все страницы документации, следуя структуре меню
|
|
|
|
Args:
|
|
start_url: Начальный URL для обхода (по умолчанию base_url)
|
|
max_pages: Максимальное количество страниц для парсинга
|
|
"""
|
|
if not start_url:
|
|
start_url = self.base_url
|
|
|
|
# Выполняем авторизацию
|
|
print(f"\n{'='*80}")
|
|
print(f"🔐 ШАГ 1: Авторизация")
|
|
print(f"{'='*80}")
|
|
login_result = self.login()
|
|
|
|
if login_result:
|
|
print(f"✅ Авторизация завершена успешно")
|
|
else:
|
|
print(f"⚠️ Авторизация не выполнена, продолжаем без неё...")
|
|
|
|
print(f"\n{'='*80}")
|
|
print(f"📚 ШАГ 2: Начало парсинга")
|
|
print(f"{'='*80}\n")
|
|
|
|
# Загружаем уже обработанные URL из существующих папок
|
|
if self.save_by_pages and os.path.exists(self.pages_output_dir):
|
|
print(f"🔍 Проверяю уже обработанные страницы...")
|
|
self._load_visited_urls_from_folders()
|
|
print(f" ✓ Найдено обработанных страниц: {len(self.visited_urls)}\n")
|
|
|
|
# Открываем начальную страницу
|
|
print(f"🚀 Начинаю обход страниц...")
|
|
print(f"📊 Лимит: {max_pages} страниц\n")
|
|
|
|
if not self.use_selenium or not self.driver:
|
|
print(f"❌ Для этого режима требуется Selenium!")
|
|
return
|
|
|
|
# Открываем первую страницу
|
|
from selenium.webdriver.common.by import By
|
|
self.driver.get(start_url)
|
|
time.sleep(2)
|
|
|
|
# Счетчик страниц
|
|
self.page_counter = [0] # Используем список чтобы передавать по ссылке
|
|
|
|
# Рекурсивная функция обхода меню
|
|
def process_menu_items(ul_selector, level=0):
|
|
"""Обходит все LI элементы в UL по порядку
|
|
|
|
Args:
|
|
ul_selector: CSS селектор для UL элемента (чтобы каждый раз находить заново)
|
|
level: Уровень вложенности
|
|
"""
|
|
if self.page_counter[0] >= max_pages:
|
|
return
|
|
|
|
indent = " " * level
|
|
|
|
# ВСЕГДА получаем UL заново (чтобы избежать stale element)
|
|
try:
|
|
ul_element = self.driver.find_element(By.CSS_SELECTOR, ul_selector)
|
|
except Exception as e:
|
|
print(f"{indent}⚠️ Не найден UL: {ul_selector}")
|
|
return
|
|
|
|
# Получаем все LI на текущем уровне
|
|
li_elements = ul_element.find_elements(By.CSS_SELECTOR, ':scope > li')
|
|
|
|
print(f"{indent}📂 Обрабатываю уровень {level}: найдено {len(li_elements)} элементов")
|
|
|
|
# Обрабатываем по одному элементу за раз
|
|
li_index = 0
|
|
while li_index < len(li_elements):
|
|
if self.page_counter[0] >= max_pages:
|
|
break
|
|
|
|
# КАЖДЫЙ РАЗ получаем элементы заново!
|
|
try:
|
|
ul_element = self.driver.find_element(By.CSS_SELECTOR, ul_selector)
|
|
li_elements = ul_element.find_elements(By.CSS_SELECTOR, ':scope > li')
|
|
|
|
if li_index >= len(li_elements):
|
|
break
|
|
|
|
li = li_elements[li_index]
|
|
except Exception as e:
|
|
print(f"{indent}⚠️ Ошибка получения элемента {li_index}: {e}")
|
|
li_index += 1
|
|
continue
|
|
|
|
try:
|
|
# Ищем все ссылки в текущем LI (может быть несколько)
|
|
links = li.find_elements(By.CSS_SELECTOR, 'a.menu__link[href]')
|
|
|
|
# Разделяем на обычную ссылку и кнопку раскрытия
|
|
page_link = None
|
|
page_href = None
|
|
caret_button = None
|
|
title = "???"
|
|
|
|
for link_elem in links:
|
|
classes = link_elem.get_attribute('class')
|
|
href = link_elem.get_attribute('href')
|
|
|
|
if 'menu__link--sublist-caret' in classes:
|
|
# Это кнопка раскрытия категории
|
|
caret_button = link_elem
|
|
if not title or title == "???":
|
|
title = link_elem.text.strip()
|
|
else:
|
|
# Это обычная ссылка на страницу
|
|
if href and not href.startswith('#') and href != '#':
|
|
page_link = link_elem
|
|
page_href = href
|
|
title = link_elem.text.strip()
|
|
|
|
has_children = caret_button is not None
|
|
|
|
# Список разделов для пропуска
|
|
skip_sections = [
|
|
"Устройство сервера",
|
|
"Панель управления сервера"
|
|
]
|
|
|
|
# Проверяем, нужно ли пропустить этот раздел
|
|
if title in skip_sections:
|
|
print(f"{indent} [{li_index+1}/{len(li_elements)}] ⏭️ ПРОПУСК: {title}")
|
|
li_index += 1
|
|
continue
|
|
|
|
# Определяем тип элемента для вывода
|
|
if page_href and page_link:
|
|
elem_type = "📄 Страница"
|
|
elif has_children:
|
|
elem_type = "📁 Категория"
|
|
else:
|
|
elem_type = "❓"
|
|
|
|
print(f"{indent} [{li_index+1}/{len(li_elements)}] {elem_type}: {title[:50]}")
|
|
|
|
# СНАЧАЛА: Если есть настоящая ссылка (не #) - парсим страницу
|
|
if page_href and page_link:
|
|
if page_href not in self.visited_urls:
|
|
self.page_counter[0] += 1
|
|
page_num = self.page_counter[0]
|
|
|
|
print(f"{indent} 🔗 Переходим: {page_href}")
|
|
|
|
# Кликаем по ссылке
|
|
try:
|
|
self.driver.execute_script("arguments[0].click();", page_link)
|
|
time.sleep(1)
|
|
except:
|
|
# Если клик не сработал, переходим напрямую
|
|
self.driver.get(page_href)
|
|
time.sleep(1)
|
|
|
|
# Парсим страницу
|
|
print(f"\n{'='*80}")
|
|
print(f"📄 Страница {page_num}/{max_pages}: {page_href}")
|
|
print(f"{'='*80}")
|
|
|
|
page_data = self._parse_page_selenium(page_href)
|
|
|
|
if page_data:
|
|
self.all_content.append(page_data)
|
|
print(f" ✅ Страница успешно обработана!")
|
|
print(f" 📝 Контент: {len(page_data['content'])} символов")
|
|
|
|
if self.save_by_pages:
|
|
self._save_page_to_folder(page_data, page_num)
|
|
else:
|
|
print(f" ❌ Не удалось обработать страницу")
|
|
|
|
print() # Пустая строка перед следующим элементом
|
|
time.sleep(0.2)
|
|
else:
|
|
print(f"{indent} ⏭️ Уже обработана")
|
|
|
|
# ПОТОМ: Если есть вложенные элементы (категория) - раскрываем и обрабатываем
|
|
if has_children:
|
|
print(f"{indent} 📖 Раскрываю категорию '{title[:30]}'...")
|
|
|
|
# Проверяем, раскрыты ли уже
|
|
is_expanded = caret_button.get_attribute('aria-expanded') == 'true'
|
|
|
|
if not is_expanded:
|
|
# Раскрываем - КЛИКАЕМ по кнопке
|
|
try:
|
|
self.driver.execute_script("arguments[0].click();", caret_button)
|
|
time.sleep(0.5) # Даём время на анимацию раскрытия
|
|
print(f"{indent} ✓ Категория раскрыта")
|
|
except Exception as e:
|
|
print(f"{indent} ⚠️ Ошибка раскрытия: {e}")
|
|
else:
|
|
print(f"{indent} ✓ Категория уже раскрыта")
|
|
|
|
# Получаем ID текущего LI для создания уникального селектора
|
|
# Используем nth-child для надежного поиска вложенного UL
|
|
try:
|
|
# Формируем селектор для вложенного UL
|
|
nested_selector = f"{ul_selector} > li:nth-child({li_index+1}) > ul.menu__list"
|
|
|
|
# Проверяем что вложенный UL существует
|
|
nested_ul = self.driver.find_element(By.CSS_SELECTOR, nested_selector)
|
|
|
|
# Рекурсивно обрабатываем вложенный уровень (передаем СЕЛЕКТОР, а не элемент!)
|
|
process_menu_items(nested_selector, level + 1)
|
|
except Exception as e:
|
|
print(f"{indent} ⚠️ Не найден вложенный UL: {e}")
|
|
|
|
# Переходим к следующему элементу
|
|
li_index += 1
|
|
|
|
except Exception as e:
|
|
print(f"{indent} ⚠️ Ошибка обработки элемента: {e}")
|
|
li_index += 1
|
|
continue
|
|
|
|
# Ищем главное меню
|
|
try:
|
|
main_menu_selector = 'ul.theme-doc-sidebar-menu'
|
|
main_menu = self.driver.find_element(By.CSS_SELECTOR, main_menu_selector)
|
|
print(f"✓ Найдено главное меню\n")
|
|
|
|
# Запускаем рекурсивный обход (передаем СЕЛЕКТОР, а не элемент!)
|
|
process_menu_items(main_menu_selector, level=0)
|
|
|
|
except Exception as e:
|
|
print(f"❌ Ошибка поиска главного меню: {e}")
|
|
|
|
print(f"\n{'='*80}")
|
|
print(f"✅ Парсинг завершен!")
|
|
print(f"📊 Статистика:")
|
|
print(f" - Обработано страниц: {len(self.all_content)}")
|
|
print(f" - Всего символов: {sum(len(p.get('content', '')) for p in self.all_content)}")
|
|
print(f"{'='*80}\n")
|
|
|
|
def save_to_text(self, output_file: str = 'documentation.txt'):
|
|
"""
|
|
Сохраняет собранную документацию в текстовый файл
|
|
|
|
Args:
|
|
output_file: Путь к выходному файлу
|
|
"""
|
|
if not self.all_content:
|
|
print("⚠️ Нет данных для сохранения!")
|
|
return
|
|
|
|
with open(output_file, 'w', encoding='utf-8') as f:
|
|
for i, page in enumerate(self.all_content, 1):
|
|
f.write(f"\n{'='*80}\n")
|
|
f.write(f"СТРАНИЦА {i}: {page['title']}\n")
|
|
f.write(f"URL: {page['url']}\n")
|
|
f.write(f"{'='*80}\n\n")
|
|
|
|
# Проверяем, что контент не пустой
|
|
content = page.get('content', '').strip()
|
|
if content:
|
|
f.write(content)
|
|
else:
|
|
f.write("[Контент не удалось извлечь]")
|
|
|
|
f.write(f"\n\n")
|
|
|
|
print(f"✅ Документация сохранена в файл: {output_file}")
|
|
print(f" Сохранено страниц: {len(self.all_content)}")
|
|
|
|
def save_to_json(self, output_file: str = 'documentation.json'):
|
|
"""
|
|
Сохраняет собранную документацию в JSON файл
|
|
|
|
Args:
|
|
output_file: Путь к выходному JSON файлу
|
|
"""
|
|
with open(output_file, 'w', encoding='utf-8') as f:
|
|
json.dump(self.all_content, f, ensure_ascii=False, indent=2)
|
|
|
|
print(f"✅ Документация сохранена в JSON файл: {output_file}")
|
|
|
|
def _load_visited_urls_from_folders(self):
|
|
"""
|
|
Загружает список уже обработанных URL из documentation.json
|
|
Если файл не существует, сканирует папки
|
|
"""
|
|
import os
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
|
|
# Вариант 1: Загружаем из documentation.json (быстро)
|
|
json_file = 'documentation.json'
|
|
if os.path.exists(json_file):
|
|
try:
|
|
print(f" 📄 Загружаю из {json_file}...")
|
|
with open(json_file, 'r', encoding='utf-8') as f:
|
|
data = json.load(f)
|
|
for page in data:
|
|
if 'url' in page:
|
|
self.visited_urls.add(page['url'])
|
|
print(f" ✓ Загружено из JSON: {len(self.visited_urls)} URL")
|
|
return
|
|
except Exception as e:
|
|
print(f" ⚠️ Ошибка чтения JSON: {e}")
|
|
print(f" 🔄 Переключаюсь на сканирование папок...")
|
|
|
|
# Вариант 2: Сканируем папки (медленно, но надежно)
|
|
pages_path = Path(self.pages_output_dir)
|
|
if not pages_path.exists():
|
|
return
|
|
|
|
print(f" 📁 Сканирую папки...")
|
|
count = 0
|
|
# Рекурсивно ищем все page.html файлы
|
|
for html_file in pages_path.rglob('page.html'):
|
|
try:
|
|
with open(html_file, 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
# Ищем canonical URL
|
|
match = re.search(r'<link rel="canonical" href="([^"]+)"', content)
|
|
if match:
|
|
url = match.group(1)
|
|
self.visited_urls.add(url)
|
|
count += 1
|
|
if count % 100 == 0:
|
|
print(f" ... обработано {count} файлов")
|
|
except Exception as e:
|
|
# Игнорируем ошибки чтения отдельных файлов
|
|
pass
|
|
print(f" ✓ Загружено из папок: {len(self.visited_urls)} URL")
|
|
|
|
def _save_page_to_folder(self, page_data: Dict[str, str], page_num: int) -> str:
|
|
"""
|
|
Сохраняет страницу в отдельную папку с HTML и текстом
|
|
|
|
Args:
|
|
page_data: Данные страницы (url, content, title)
|
|
page_num: Номер страницы
|
|
"""
|
|
import os
|
|
import re
|
|
|
|
try:
|
|
# Создаем безопасное имя папки из заголовка
|
|
title = page_data.get('title', 'untitled')
|
|
# Убираем недопустимые символы для имени папки
|
|
safe_title = re.sub(r'[<>:"/\\|?*]', '_', title)
|
|
safe_title = safe_title[:100] # Ограничиваем длину
|
|
|
|
# Создаем имя папки
|
|
folder_name = f"page_{page_num:03d}_{safe_title}"
|
|
folder_path = os.path.join(self.pages_output_dir, folder_name)
|
|
|
|
# Создаем папку если её нет
|
|
if not os.path.exists(folder_path):
|
|
os.makedirs(folder_path)
|
|
|
|
# Сохраняем HTML
|
|
html_content = page_data.get('html', '')
|
|
if not html_content:
|
|
if self.use_selenium and self.driver:
|
|
html_content = self.driver.page_source
|
|
else:
|
|
# Пробуем получить HTML из сессии
|
|
try:
|
|
response = self.session.get(page_data['url'], timeout=10)
|
|
html_content = response.text
|
|
except:
|
|
html_content = f"<html><head><title>{title}</title></head><body><h1>{title}</h1><p>HTML не удалось получить</p></body></html>"
|
|
|
|
# Сохраняем HTML файл
|
|
html_file = os.path.join(folder_path, 'page.html')
|
|
with open(html_file, 'w', encoding='utf-8') as f:
|
|
f.write(html_content)
|
|
|
|
# Сохраняем текстовый файл в формате Markdown
|
|
text_file = os.path.join(folder_path, 'content.md')
|
|
with open(text_file, 'w', encoding='utf-8') as f:
|
|
content = page_data.get('content', '')
|
|
# Если контент уже в Markdown формате, сохраняем как есть
|
|
# Иначе пробуем конвертировать
|
|
f.write(content)
|
|
|
|
print(f" 💾 Страница сохранена в папку: {folder_path}")
|
|
print(f" 📄 page.html")
|
|
print(f" 📝 content.md")
|
|
|
|
return folder_path
|
|
|
|
except Exception as e:
|
|
print(f" ⚠️ Ошибка при сохранении страницы в папку: {e}")
|
|
return None
|
|
|
|
def main():
|
|
"""Основная функция для запуска парсера"""
|
|
import argparse
|
|
|
|
parser = argparse.ArgumentParser(description='Парсер документации 1C MyCloud')
|
|
parser.add_argument('--url', type=str,
|
|
default='https://1cmycloud.com/console/help/element/8.1/docs/topics/1c-element-overview/',
|
|
help='URL документации для парсинга')
|
|
parser.add_argument('--username', type=str, help='Имя пользователя для авторизации')
|
|
parser.add_argument('--password', type=str, help='Пароль для авторизации')
|
|
parser.add_argument('--output', type=str, default='documentation.txt',
|
|
help='Имя выходного файла')
|
|
parser.add_argument('--max-pages', type=int, default=1000,
|
|
help='Максимальное количество страниц для парсинга')
|
|
parser.add_argument('--use-selenium', action='store_true',
|
|
help='Использовать Selenium для работы с JavaScript (требует установки Chrome)')
|
|
parser.add_argument('--visible-browser', action='store_true',
|
|
help='Показывать браузер во время работы (только с --use-selenium)')
|
|
parser.add_argument('--debug', action='store_true',
|
|
help='Включить режим отладки (сохранение HTML файлов)')
|
|
parser.add_argument('--save-by-pages', action='store_true',
|
|
help='Сохранять каждую страницу в отдельную папку (HTML + текст)')
|
|
parser.add_argument('--pages-dir', type=str, default='pages',
|
|
help='Директория для сохранения страниц по папкам (по умолчанию: pages)')
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Создаем парсер
|
|
doc_parser = DocumentationParser(
|
|
base_url=args.url,
|
|
username=args.username,
|
|
password=args.password,
|
|
use_selenium=args.use_selenium,
|
|
visible_browser=args.visible_browser
|
|
)
|
|
|
|
# Включаем режим отладки если указан
|
|
doc_parser.debug_mode = args.debug
|
|
doc_parser.save_by_pages = args.save_by_pages
|
|
doc_parser.pages_output_dir = args.pages_dir
|
|
|
|
try:
|
|
# Запускаем парсинг
|
|
doc_parser.crawl(start_url=args.url, max_pages=args.max_pages)
|
|
|
|
# Сохраняем результаты
|
|
doc_parser.save_to_text(args.output)
|
|
doc_parser.save_to_json(args.output.replace('.txt', '.json'))
|
|
finally:
|
|
# Закрываем Selenium драйвер если использовался
|
|
if doc_parser.driver:
|
|
doc_parser.driver.quit()
|
|
print("✅ Selenium драйвер закрыт")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
|