1
0
mirror of https://github.com/vanyagoncharov/parser_1cmycloud.git synced 2026-06-19 17:18:10 +02:00
Files
2026-01-13 10:31:04 +03:00

1630 lines
82 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]:
"""
Находит все ссылки на документацию на странице
Args:
soup: BeautifulSoup объект страницы
current_url: Текущий URL страницы
Returns:
Список URL ссылок на документацию
"""
links = []
base_domain = urlparse(self.base_url).netloc
seen_links = set()
# Вариант 1: Ищем ссылки в меню (специфично для этого сайта)
# Рекурсивно обходим все уровни меню
menu_selectors = [
'ul.theme-doc-sidebar-menu',
'ul.menu__list',
'.navbar-sidebar__item.menu',
'.menu',
]
def extract_links_from_menu_element(menu_elem, level=0):
"""Рекурсивно извлекает ссылки из элементов меню всех уровней"""
found_links = []
# Ищем ссылки меню с классом menu__link
# Возвращаем как обычные ссылки, так и ссылки-категории (--sublist)
for a_tag in menu_elem.find_all('a', href=True):
# Проверяем, что это элемент меню с правильным классом
classes = a_tag.get('class', [])
if 'menu__link' not in classes:
continue
href = a_tag.get('href', '')
# Пропускаем пустые ссылки
if not href:
continue
# Для ссылок-категорий (--sublist) пропускаем якоря (#)
# Но если это ссылка на реальную страницу категории - берем её
is_category = 'menu__link--sublist' in classes
if is_category and (href.startswith('#') or href == '#'):
# Это просто раскрывалка без реальной страницы
continue
# Остальные ссылки (включая ссылки-категории с реальным href) добавляем
if not href.startswith('#'):
full_url = urljoin(current_url, href)
parsed = urlparse(full_url)
# Проверяем, что это ссылка на документацию
if (parsed.netloc == base_domain or not parsed.netloc) and full_url not in seen_links:
if '/docs/' in full_url or '/help/' in full_url:
found_links.append(full_url)
seen_links.add(full_url)
# Рекурсивно обрабатываем вложенные списки меню всех уровней
# Ищем все ul.menu__list внутри текущего элемента (включая глубоко вложенные)
nested_lists = menu_elem.find_all('ul', class_='menu__list', recursive=True)
for nested_list in nested_lists:
# Проверяем, что это действительно вложенный список (не сам menu_elem)
if nested_list != menu_elem:
# Извлекаем ссылки из вложенного списка рекурсивно
nested_links = extract_links_from_menu_element(nested_list, level + 1)
found_links.extend(nested_links)
return found_links
for selector in menu_selectors:
try:
menu = soup.select_one(selector)
if menu:
print(f" 🔍 Найдено меню: {selector}")
# Рекурсивно извлекаем все ссылки из меню
menu_links = extract_links_from_menu_element(menu)
links.extend(menu_links)
if menu_links:
print(f" ✓ Найдено {len(menu_links)} ссылок в меню (все уровни)")
# Не прерываемся, продолжаем искать в других селекторах для полноты
except Exception as e:
print(f" ⚠️ Ошибка при поиске в меню {selector}: {e}")
continue
# Не ищем ссылки в контенте страницы - только в меню!
# Иначе будут найдены все ссылки из текста статей, которые не являются частью структуры навигации
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")
# Очередь URL для обхода
urls_to_visit = [start_url]
print(f"🚀 Начинаю обход страниц...")
print(f"📋 В очереди: {len(urls_to_visit)} страниц(ы)")
print(f"📊 Лимит: {max_pages} страниц\n")
page_num = 0
while urls_to_visit and len(self.visited_urls) < max_pages:
current_url = urls_to_visit.pop(0)
page_num += 1
if current_url in self.visited_urls:
print(f"⏭️ Страница {page_num}: {current_url} (уже обработана, пропускаю)")
continue
print(f"\n{'='*80}")
print(f"📄 Страница {page_num}/{max_pages}: {current_url}")
print(f"{'='*80}")
page_data = self.parse_page(current_url)
if page_data:
self.all_content.append(page_data)
print(f" ✅ Страница успешно обработана!")
print(f" 📝 Контент: {len(page_data['content'])} символов")
print(f" 📋 Всего обработано: {len(self.all_content)} страниц")
# Сохраняем страницу в отдельную папку если включен режим
if self.save_by_pages:
self._save_page_to_folder(page_data, page_num)
# Находим новые ссылки на текущей странице
# Это позволяет обходить иерархию: категория → её дочерние элементы
print(f" 🔍 Поиск ссылок на текущей странице...")
try:
if self.use_selenium and self.driver:
# Не раскрываем меню - просто берем HTML страницы
# Меню автоматически раскрывается для текущей категории
# Берем HTML после раскрытия меню
html = self.driver.page_source
soup = BeautifulSoup(html, 'html.parser')
else:
response = self.session.get(current_url, timeout=10)
soup = BeautifulSoup(response.text, 'html.parser')
# Находим ссылки из меню текущей страницы
new_links = self.find_documentation_links(soup, current_url)
print(f" 🔗 Найдено новых ссылок: {len(new_links)}")
# Добавляем новые ссылки в НАЧАЛО очереди (для иерархического обхода)
# Это обеспечит порядок: родитель → дочерние → следующий родитель
added = 0
for link in reversed(new_links): # reversed чтобы сохранить порядок при insert(0)
if link not in self.visited_urls and link not in urls_to_visit:
urls_to_visit.insert(0, link)
added += 1
if added > 0:
print(f" ➕ Добавлено в начало очереди: {added} ссылок")
print(f" 📋 Всего в очереди: {len(urls_to_visit)} страниц")
except Exception as e:
print(f" ⚠️ Ошибка при поиске ссылок: {e}")
else:
print(f" ❌ Не удалось обработать страницу")
# Небольшая задержка между запросами
if page_num < max_pages:
print(f" ⏳ Пауза 0.2 сек перед следующей страницей...\n")
time.sleep(0.2)
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()