Загрузка данных


Ты работаешь на корпоративном MacBook.

Нужно реализовать проект adt-markdownify-meta почти с самого начала, опираясь на пример проекта hr-resume-scrapper-master.

Проект-пример:
Путь к примеру:
укажи фактический путь к hr-resume-scrapper-master на этом MacBook

Новый проект:
adt-markdownify-meta

Задача нового проекта:
Сделать skill для выгрузки страниц META в Markdown.

META — это корпоративная веб-система, открывается через SberBrowser.
Нужно получить HTML открытой страницы META, конвертировать его в Markdown и сохранить результат в output/.

Важно:
- Не использовать API META.
- Не добавлять endpoint-ы.
- Не использовать requests/httpx/aiohttp/urllib.
- Не использовать token/cookie/login/password.
- Не сохранять cookies.
- Не сохранять localStorage/sessionStorage.
- Не делать автоматическую авторизацию.
- Не скачивать PDF.
- Не реализовывать логику резюме.
- Новый проект должен работать именно с HTML страницы META.
- Код должен быть простым, читаемым и похожим по структуре на hr-resume-scrapper.

Главная идея:
Переиспользовать из hr-resume-scrapper:
1. Структуру проекта.
2. YAML-конфиг.
3. Поиск конфигурационного файла.
4. Запуск SberBrowser через Playwright.
5. Настройки browser.type.
6. Настройки path_to_exe.
7. headless, slow_mo, viewport, timeout.
8. retries и delays.
9. Сохранение результатов в output.
10. README.md с инструкцией.
11. SKILL.md с описанием skill.

Не переносить:
1. PDF download.
2. expect_download.
3. pdfplumber.
4. markdownify для PDF.
5. resume_urls как обязательный формат резюме.
6. Логику поиска кнопки “Резюме”.
7. Логику кандидатов.

Новый проект должен поддерживать два режима:

1. browser
Автоматически запускает SberBrowser через Playwright.
Используется на macOS как основной простой режим.

2. cdp_existing
Подключается к уже открытому SberBrowser через remote-debugging-port=9222.
Этот режим оставить как резервный.

Основной режим для корпоративного MacBook сейчас: browser.

Структура проекта должна быть такая:

adt-markdownify-meta/
├── README.md
├── SKILL.md
├── requirements.txt
├── meta_exporter.yaml.example
├── meta_urls.txt
├── run_exporter.py
├── scripts/
│   ├── __init__.py
│   ├── config_loader.py
│   ├── scrape_meta.py
│   ├── html_to_markdown.py
│   └── markdown_writer.py
├── output/
└── temp_html/

Если файлов нет — создать.
Если файлы есть — можно аккуратно перезаписать чистой реализацией.
Не удалять .venv.
Не удалять весь проект целиком.
Не запускать полный scraping автоматически после реализации.
После реализации выполнить только проверку синтаксиса.

============================================================
1. requirements.txt
============================================================

Сделать requirements.txt:

playwright==1.40.0
PyYAML==6.0.1
beautifulsoup4==4.12.3
html2text==2024.2.26

Важно:
Версии взять максимально близко к старому проекту, где использовался playwright==1.40.0.
Если в старом проекте используется другая точная версия PyYAML — можно использовать её.
Не добавлять requests/httpx/aiohttp/urllib.

============================================================
2. .gitignore
============================================================

Создать .gitignore:

.venv/
venv/
__pycache__/
*.pyc
.DS_Store

meta_exporter.yaml

output/
temp_html/

.env
*.log

Важно:
meta_exporter.yaml не коммитить.
meta_exporter.yaml.example коммитить.

============================================================
3. meta_exporter.yaml.example
============================================================

Сделать конфиг:

mode: "browser"

meta_urls_file: "meta_urls.txt"
output_dir: "output"
temp_html_dir: "temp_html"

browser:
  type: "sberbrowser"
  path_to_exe: "/Applications/SberBrowser.app/Contents/MacOS/SberBrowser"

browser_settings:
  headless: false
  slow_mo: 1000
  viewport_width: 1920
  viewport_height: 1080
  wait_until: "domcontentloaded"
  page_load_timeout: 60000

scraping:
  max_retries: 3
  retry_delay: 2
  request_delay: 1

cdp:
  endpoint_url: "http://127.0.0.1:9222"
  target_url_contains: "sberbank.ru"
  fallback_title: "META page"
  wait_timeout_seconds: 60
  poll_interval_seconds: 2

Важно:
Для macOS основной mode: "browser".
Путь к SberBrowser должен быть таким же, как в старом проекте hr-resume-scrapper, если там указан другой путь.
Если путь отличается, взять путь из старого проекта.

============================================================
4. meta_urls.txt
============================================================

Создать пример:

# Формат:
# Название страницы - URL
# или просто URL

SberGeo Functional Subsystem - https://meta.sberbank.ru/meta/earp/functionalSubsystem/236500

Файл должен поддерживать:
- пустые строки;
- комментарии через #;
- строки формата "Название - URL";
- строки, где указан только URL.

============================================================
5. scripts/config_loader.py
============================================================

Реализовать config_loader.py по аналогии со старым проектом.

Нужны функции:

find_config(start_dir: Path, max_depth: int = 3) -> Path

Логика:
- ищет meta_exporter.yaml от текущей директории вверх до 3 уровней;
- если найден только meta_exporter.yaml.example, не использовать его как рабочий конфиг;
- вывести понятную ошибку:
  "Найден только meta_exporter.yaml.example. Скопируйте его в meta_exporter.yaml"

load_config(path: Path) -> dict

Логика:
- читает YAML;
- если файл пустой — ValueError;
- если YAML не dict — ValueError.

get_config() -> dict

Логика:
- вызывает find_config(Path.cwd());
- вызывает load_config().

validate_config(config: dict)

Проверяет:
- mode;
- meta_urls_file;
- output_dir;
- temp_html_dir.

Если mode == "browser", проверить:
- browser.type;
- browser.path_to_exe;
- browser_settings.headless;
- browser_settings.slow_mo;
- browser_settings.viewport_width;
- browser_settings.viewport_height;
- browser_settings.wait_until;
- browser_settings.page_load_timeout;
- scraping.max_retries;
- scraping.retry_delay;
- scraping.request_delay.

Если mode == "cdp_existing", проверить:
- cdp.endpoint_url;
- cdp.target_url_contains;
- cdp.wait_timeout_seconds;
- cdp.poll_interval_seconds.

Ошибки должны быть понятными.

============================================================
6. scripts/markdown_writer.py
============================================================

Реализовать:

safe_filename(name: str) -> str

Требования:
- заменить запрещённые символы \ / : * ? " < > | на "_";
- заменить двоеточия и слэши;
- схлопнуть пробелы;
- убрать пробелы по краям;
- если имя пустое — "meta_page";
- ограничить длину имени до 120 символов.

write_markdown(output_dir: str, title: str, url: str, markdown_body: str) -> Path

Формат файла:

# <title>

Источник: <url>

---

<markdown_body>

write_index(output_dir: str, exported_files: list) -> Path

Создать output/README.md:

# META export

## Выгруженные страницы

- [title](filename.md) — url

Если список пустой:
Нет выгруженных страниц.

============================================================
7. scripts/html_to_markdown.py
============================================================

Реализовать конвертацию HTML в Markdown.

Использовать:
- BeautifulSoup для очистки HTML;
- html2text для конвертации.

Функции:

clean_html(html: str) -> str

Удалить:
- script;
- style;
- noscript;
- svg;
- canvas.

extract_title(html: str, fallback: str = "META page") -> str

Логика:
1. Если есть h1 — использовать его.
2. Иначе title.
3. Иначе fallback.

normalize_blank_lines(text: str) -> str

Оставлять максимум две пустые строки подряд.

html_to_markdown(html: str) -> str

Настройки html2text:
- ignore_links = False;
- ignore_images = True;
- body_width = 0.

build_meta_markdown(html: str, url: str) -> dict

Возвращает:

{
  "title": title,
  "markdown": markdown
}

В начало markdown добавить блок:

## Проверка разделов META

- Иерархия АС > ФП > Модуль > Подмодуль > ТК: Не проверено
- Точки взаимодействия + API: Не проверено
- Интеграционные взаимодействия + API: Не проверено
- Стенды: Не проверено
- Технические ресурсы: Не проверено

---

Потом сам Markdown страницы.

============================================================
8. scripts/scrape_meta.py
============================================================

Главный файл логики.

Нужны импорты:
- time;
- Path;
- sync_playwright;
- TimeoutError as PlaywrightTimeoutError;
- build_meta_markdown;
- write_markdown;
- write_index;
- safe_filename.

Функция read_meta_urls(meta_urls_file: str) -> list

Формат:
- пустые строки пропускать;
- строки с # пропускать;
- "Название - URL" поддерживать;
- просто URL поддерживать;
- возвращать список dict:
  {"name": name, "url": url}

Функция save_html(temp_html_dir: str, title: str, html: str) -> Path

Сохранять raw HTML в temp_html/<safe_title>.html.

Функция launch_browser(playwright, config: dict)

Логика:
- browser_type = config["browser"]["type"];
- path_to_exe = config["browser"].get("path_to_exe");
- headless = config["browser_settings"]["headless"];
- slow_mo = config["browser_settings"]["slow_mo"];

Если browser_type == "sberbrowser":
- проверить, что path_to_exe указан;
- передать executable_path=path_to_exe в chromium.launch.

Если browser_type == "chromium":
- запускать chromium.launch без executable_path.

Нужно использовать browser_options:

browser_options = {
    "headless": headless,
    "slow_mo": slow_mo,
}

Если path_to_exe есть и browser_type == "sberbrowser":
    browser_options["executable_path"] = path_to_exe

Вернуть browser.

Функция export_one_page(page, name: str, url: str, config: dict) -> dict

Логика:
1. Взять:
   - output_dir;
   - temp_html_dir;
   - wait_until;
   - page_load_timeout.
2. Открыть страницу:

page.goto(
    url,
    wait_until=wait_until,
    timeout=page_load_timeout,
)

3. После загрузки сделать небольшую паузу 2 секунды, чтобы SPA успела отрендериться.
4. Получить html = page.content().
5. Получить title:
   - сначала page.title().strip();
   - если пусто, использовать name;
   - если name пустое, использовать "META page".
6. Сохранить HTML через save_html.
7. Конвертировать HTML через build_meta_markdown.
8. Сохранить Markdown через write_markdown.
9. Вернуть dict:
   {
     "title": markdown_title,
     "url": url,
     "path": filepath
   }

Функция export_meta(config: dict)

Это основной browser mode.

Логика:
1. Прочитать meta_urls_file.
2. Если ссылок нет — ValueError.
3. Прочитать настройки:
   - max_retries;
   - retry_delay;
   - request_delay;
   - viewport_width;
   - viewport_height.
4. Запустить Playwright.
5. Запустить browser через launch_browser.
6. Создать context:

context = browser.new_context(
    viewport={
        "width": viewport_width,
        "height": viewport_height,
    },
    user_agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
)

7. Создать одну страницу:

page = context.new_page()

8. Для каждой ссылки из meta_urls:
   - делать retry до max_retries;
   - если ошибка одной ссылки — не падать всем проектом;
   - печатать понятную ошибку;
   - переходить к следующей ссылке.
9. Между ссылками делать time.sleep(request_delay).
10. После всех ссылок создать output/README.md через write_index.
11. Закрыть browser через browser.close() только в browser mode.

Важно:
browser.close() разрешён только в browser mode.
В cdp_existing browser.close() запрещён.

Функция find_existing_page(browser, target_url_contains: str)

Для cdp_existing:
- вывести все открытые вкладки;
- найти вкладку, где target_url_contains in page.url;
- вернуть первую найденную;
- если нет — вернуть None.

Функция export_meta_from_existing_browser(config: dict)

Резервный режим.

Логика:
- подключиться через pw.chromium.connect_over_cdp(endpoint_url);
- не запускать SberBrowser;
- не создавать new_page;
- не делать page.goto;
- найти существующую вкладку;
- ждать до 60 секунд;
- получить page.content;
- сохранить HTML и Markdown;
- создать README;
- вызвать browser.disconnect();
- не вызывать browser.close().

============================================================
9. run_exporter.py
============================================================

Реализовать:

from scripts.config_loader import get_config, validate_config

def main():
    print("Запуск выгрузки META в Markdown...")
    config = get_config()
    validate_config(config)

    mode = config.get("mode", "browser")

    if mode == "browser":
        from scripts.scrape_meta import export_meta
        export_meta(config)
    elif mode == "cdp_existing":
        from scripts.scrape_meta import export_meta_from_existing_browser
        export_meta_from_existing_browser(config)
    else:
        raise ValueError(f"Неизвестный mode: {mode}")

    print("Выгрузка завершена.")

if __name__ == "__main__":
    try:
        main()
    except FileNotFoundError as e:
        print(f"[CONFIG ERROR] {e}")
    except ValueError as e:
        print(f"[VALUE ERROR] {e}")
    except ImportError as e:
        print(f"[IMPORT ERROR] {e}")
        print("Проверьте зависимости: pip install -r requirements.txt")
    except Exception as e:
        print(f"[ERROR] {e}")

============================================================
10. README.md
============================================================

Сделать README.md с такими разделами:

# adt-markdownify-meta

## Назначение

Проект выгружает страницы META в Markdown.

## Основной режим на корпоративном MacBook: browser

В этом режиме скрипт сам запускает SberBrowser через Playwright по пути из конфига.

## Установка

python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python -m playwright install chromium

Если используется SberBrowser через executable_path, отдельная установка chromium может быть не нужна, но playwright должен быть установлен как Python-библиотека.

## Настройка

cp meta_exporter.yaml.example meta_exporter.yaml

Проверить путь:

browser:
  type: "sberbrowser"
  path_to_exe: "/Applications/SberBrowser.app/Contents/MacOS/SberBrowser"

Если на корпоративном MacBook путь отличается, указать фактический путь.

## Входные ссылки

Файл meta_urls.txt:

Название страницы - URL

## Запуск

source .venv/bin/activate
python run_exporter.py

## Результат

- temp_html/*.html
- output/*.md
- output/README.md

## Резервный режим cdp_existing

Если автоматический запуск SberBrowser не подходит, можно запустить SberBrowser вручную с remote-debugging-port=9222 и использовать mode: cdp_existing.

## Ограничения

- API не используется.
- Авторизация автоматом не выполняется.
- Cookies/tokens не сохраняются.
- Скрипт работает только с тем, что доступно после ручной/корпоративной авторизации в SberBrowser.

============================================================
11. SKILL.md
============================================================

Сделать краткий SKILL.md:

# Skill: adt-markdownify-meta

## Назначение
Выгружает страницы META в Markdown.

## Вход
Список URL страниц META в meta_urls.txt или открытая вкладка META при cdp_existing.

## Выход
Markdown-файлы в output/ и raw HTML в temp_html/.

## Ограничения
- Не использует API.
- Не сохраняет cookies/tokens.
- Не выполняет автоматическую авторизацию.

============================================================
12. Проверка после реализации
============================================================

Выполнить только:

python -m py_compile run_exporter.py scripts/config_loader.py scripts/scrape_meta.py scripts/html_to_markdown.py scripts/markdown_writer.py

Не запускать python run_exporter.py автоматически.

В конце ответа показать:

1. Какие файлы созданы или переписаны.
2. Полный код run_exporter.py.
3. Полный код scripts/scrape_meta.py.
4. Полный код scripts/config_loader.py.
5. Полный код meta_exporter.yaml.example.
6. Краткую инструкцию запуска на корпоративном MacBook.
7. Подтверждение:
   - browser mode запускает SberBrowser через executable_path;
   - cdp_existing не закрывает SberBrowser;
   - cdp_existing не вызывает page.goto;
   - cdp_existing не вызывает new_page;
   - API не используется;
   - cookies/tokens не сохраняются.